diff --git a/.gitignore b/.gitignore
index ce6a363fe35fe4c328b28c79407bc424681116a5..1bf9a47aef6de8a156e6ffb0e34c87de23efc445 100644
--- a/.gitignore
+++ b/.gitignore
@@ -30,6 +30,7 @@
 /config/secrets.yml
 /config/sidekiq.yml
 /coverage/*
+/coverage-javascript/
 /db/*.sqlite3
 /db/*.sqlite3-journal
 /db/data.yml
diff --git a/.gitlab-ci.yml b/.gitlab-ci.yml
index 2eda2a6007d1e1bd01a8e97a83b6704a8f21a121..be5614520a58d9b4afda6df81d064107d2e184b8 100644
--- a/.gitlab-ci.yml
+++ b/.gitlab-ci.yml
@@ -1,7 +1,7 @@
-image: "ruby:2.1"
+image: "ruby:2.3.1"
 
 cache:
-  key: "ruby21"
+  key: "ruby-231"
   paths:
   - vendor/apt
   - vendor/ruby
@@ -15,6 +15,7 @@ variables:
   USE_DB: "true"
   USE_BUNDLE_INSTALL: "true"
   GIT_DEPTH: "20"
+  PHANTOMJS_VERSION: "2.1.1"
 
 before_script:
   - source ./scripts/prepare_build.sh
@@ -138,57 +139,57 @@ spinach 7 10: *spinach-knapsack
 spinach 8 10: *spinach-knapsack
 spinach 9 10: *spinach-knapsack
 
-# Execute all testing suites against Ruby 2.3
-.ruby-23: &ruby-23
-  image: "ruby:2.3"
+# Execute all testing suites against Ruby 2.1
+.ruby-21: &ruby-21
+  image: "ruby:2.1"
   <<: *use-db
   only:
     - master
   cache:
-    key: "ruby-23"
+    key: "ruby21"
     paths:
       - vendor/apt
       - vendor/ruby
 
-.rspec-knapsack-ruby23: &rspec-knapsack-ruby23
+.rspec-knapsack-ruby21: &rspec-knapsack-ruby21
   <<: *rspec-knapsack
-  <<: *ruby-23
+  <<: *ruby-21
 
-.spinach-knapsack-ruby23: &spinach-knapsack-ruby23
+.spinach-knapsack-ruby21: &spinach-knapsack-ruby21
   <<: *spinach-knapsack
-  <<: *ruby-23
+  <<: *ruby-21
 
-rspec 0 20 ruby23: *rspec-knapsack-ruby23
-rspec 1 20 ruby23: *rspec-knapsack-ruby23
-rspec 2 20 ruby23: *rspec-knapsack-ruby23
-rspec 3 20 ruby23: *rspec-knapsack-ruby23
-rspec 4 20 ruby23: *rspec-knapsack-ruby23
-rspec 5 20 ruby23: *rspec-knapsack-ruby23
-rspec 6 20 ruby23: *rspec-knapsack-ruby23
-rspec 7 20 ruby23: *rspec-knapsack-ruby23
-rspec 8 20 ruby23: *rspec-knapsack-ruby23
-rspec 9 20 ruby23: *rspec-knapsack-ruby23
-rspec 10 20 ruby23: *rspec-knapsack-ruby23
-rspec 11 20 ruby23: *rspec-knapsack-ruby23
-rspec 12 20 ruby23: *rspec-knapsack-ruby23
-rspec 13 20 ruby23: *rspec-knapsack-ruby23
-rspec 14 20 ruby23: *rspec-knapsack-ruby23
-rspec 15 20 ruby23: *rspec-knapsack-ruby23
-rspec 16 20 ruby23: *rspec-knapsack-ruby23
-rspec 17 20 ruby23: *rspec-knapsack-ruby23
-rspec 18 20 ruby23: *rspec-knapsack-ruby23
-rspec 19 20 ruby23: *rspec-knapsack-ruby23
+rspec 0 20 ruby21: *rspec-knapsack-ruby21
+rspec 1 20 ruby21: *rspec-knapsack-ruby21
+rspec 2 20 ruby21: *rspec-knapsack-ruby21
+rspec 3 20 ruby21: *rspec-knapsack-ruby21
+rspec 4 20 ruby21: *rspec-knapsack-ruby21
+rspec 5 20 ruby21: *rspec-knapsack-ruby21
+rspec 6 20 ruby21: *rspec-knapsack-ruby21
+rspec 7 20 ruby21: *rspec-knapsack-ruby21
+rspec 8 20 ruby21: *rspec-knapsack-ruby21
+rspec 9 20 ruby21: *rspec-knapsack-ruby21
+rspec 10 20 ruby21: *rspec-knapsack-ruby21
+rspec 11 20 ruby21: *rspec-knapsack-ruby21
+rspec 12 20 ruby21: *rspec-knapsack-ruby21
+rspec 13 20 ruby21: *rspec-knapsack-ruby21
+rspec 14 20 ruby21: *rspec-knapsack-ruby21
+rspec 15 20 ruby21: *rspec-knapsack-ruby21
+rspec 16 20 ruby21: *rspec-knapsack-ruby21
+rspec 17 20 ruby21: *rspec-knapsack-ruby21
+rspec 18 20 ruby21: *rspec-knapsack-ruby21
+rspec 19 20 ruby21: *rspec-knapsack-ruby21
 
-spinach 0 10 ruby23: *spinach-knapsack-ruby23
-spinach 1 10 ruby23: *spinach-knapsack-ruby23
-spinach 2 10 ruby23: *spinach-knapsack-ruby23
-spinach 3 10 ruby23: *spinach-knapsack-ruby23
-spinach 4 10 ruby23: *spinach-knapsack-ruby23
-spinach 5 10 ruby23: *spinach-knapsack-ruby23
-spinach 6 10 ruby23: *spinach-knapsack-ruby23
-spinach 7 10 ruby23: *spinach-knapsack-ruby23
-spinach 8 10 ruby23: *spinach-knapsack-ruby23
-spinach 9 10 ruby23: *spinach-knapsack-ruby23
+spinach 0 10 ruby21: *spinach-knapsack-ruby21
+spinach 1 10 ruby21: *spinach-knapsack-ruby21
+spinach 2 10 ruby21: *spinach-knapsack-ruby21
+spinach 3 10 ruby21: *spinach-knapsack-ruby21
+spinach 4 10 ruby21: *spinach-knapsack-ruby21
+spinach 5 10 ruby21: *spinach-knapsack-ruby21
+spinach 6 10 ruby21: *spinach-knapsack-ruby21
+spinach 7 10 ruby21: *spinach-knapsack-ruby21
+spinach 8 10 ruby21: *spinach-knapsack-ruby21
+spinach 9 10 ruby21: *spinach-knapsack-ruby21
 
 # Other generic tests
 
@@ -222,7 +223,22 @@ teaspoon:
   stage: test
   <<: *use-db
   script:
+    - curl --silent --location https://deb.nodesource.com/setup_6.x | bash -
+    - apt-get install --assume-yes nodejs
+    - npm install --global istanbul
     - teaspoon
+  artifacts:
+    name: coverage-javascript
+    expire_in: 31d
+    paths:
+    - coverage-javascript/default/
+
+lint-doc:
+  stage: test
+  image: "phusion/baseimage:latest"
+  before_script: []
+  script:
+    - scripts/lint-doc.sh
 
 bundler:audit:
   stage: test
@@ -252,6 +268,9 @@ coverage:
 
 notify:slack:
   stage: post-test
+  variables:
+    USE_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>"
   when: on_failure
@@ -266,10 +285,12 @@ pages:
   stage: pages
   dependencies:
     - coverage
+    - teaspoon
   script:
     - mv public/ .public/
     - mkdir public/
     - mv coverage public/coverage-ruby
+    - mv coverage-javascript/default/ public/coverage-javascript/
   artifacts:
     paths:
       - public
diff --git a/.mailmap b/.mailmap
new file mode 100644
index 0000000000000000000000000000000000000000..bd5ac22132c4131ced4fc24cad666f3e304bb585
--- /dev/null
+++ b/.mailmap
@@ -0,0 +1,35 @@
+#
+# This list is used by git-shortlog to make contributions from the
+# same person appearing to be so.
+#
+
+Achilleas Pipinellis <axilleas@axilleas.me> <axilleas@archlinux.gr>
+Achilleas Pipinellis <axilleas@axilleas.me> <axilleas@users.noreply.github.com>
+Dmitriy Zaporozhets <dzaporozhets@gitlab.com> <dmitriy.zaporozhets@gmail.com>
+Dmitriy Zaporozhets <dzaporozhets@gitlab.com> <dzaporozhets@sphereconsultinginc.com>
+Douwe Maan <douwe@gitlab.com> <douwe@selenight.nl>
+Douwe Maan <douwe@gitlab.com> <me@douwe.me>
+Grzegorz Bizon <grzegorz@gitlab.com> <grzegorz.bizon@ntsn.pl>
+Grzegorz Bizon <grzegorz@gitlab.com> <grzesiek.bizon@gmail.com>
+Jacob Vosmaer <jacob@gitlab.com> <contact@jacobvosmaer.nl>
+Jacob Vosmaer <jacob@gitlab.com> Jacob Vosmaer (GitLab) <jacob@gitlab.com>
+Jacob Schatz <jschatz@gitlab.com> <jacobschatz@Jacobs-MacBook-Pro.local>
+Jacob Schatz <jschatz@gitlab.com> <jacobschatz@Jacobs-MBP.fios-router.home>
+Jacob Schatz <jschatz@gitlab.com> <jschatz1@gmail.com>
+James Lopez <james@jameslopez.es> <james@gitlab.com>
+James Lopez <james@jameslopez.es> <james.lopez@vodafone.com>
+Kamil Trzciński <kamil@gitlab.com> <ayufan@ayufan.eu>
+Marin Jankovski <maxlazio@gmail.com> <marin@gitlab.com>
+Phil Hughes <me@iamphill.com> <theephil@gmail.com>
+Rémy Coutable <remy@rymai.me> <remy@gitlab.com>
+Robert Schilling <rschilling@student.tugraz.at> <Razer6@users.noreply.github.com>
+Robert Schilling <rschilling@student.tugraz.at> <schilling.ro@gmail.com>
+Robert Speicher <robert@gitlab.com> <rspeicher@gmail.com>
+Stan Hu <stanhu@gmail.com> <stanhu@alum.mit.edu>
+Stan Hu <stanhu@gmail.com> <stanhu@packetzoom.com>
+Stan Hu <stanhu@gmail.com> <stanhu@users.noreply.github.com>
+Stan Hu <stanhu@gmail.com> stanhu <stanhu@gmail.com>
+Sytse Sijbrandij <sytse@gitlab.com> <sytse+admin@gitlab.com>
+Sytse Sijbrandij <sytse@gitlab.com> <sytse@dosire.com>
+Sytse Sijbrandij <sytse@gitlab.com> <sytses@gmail.com>
+Sytse Sijbrandij <sytse@gitlab.com> dosire <sytse@gitlab.com>
diff --git a/.rubocop.yml b/.rubocop.yml
index 556a5d11a394a7152bced1f73bb63a8ba565f205..282f4539f03168608f9383108077623039ebe940 100644
--- a/.rubocop.yml
+++ b/.rubocop.yml
@@ -149,19 +149,19 @@ Style/EmptyLinesAroundAccessModifier:
 
 # Keeps track of empty lines around block bodies.
 Style/EmptyLinesAroundBlockBody:
-  Enabled: false
+  Enabled: true
 
 # Keeps track of empty lines around class bodies.
 Style/EmptyLinesAroundClassBody:
-  Enabled: false
+  Enabled: true
 
 # Keeps track of empty lines around module bodies.
 Style/EmptyLinesAroundModuleBody:
-  Enabled: false
+  Enabled: true
 
 # Keeps track of empty lines around method bodies.
 Style/EmptyLinesAroundMethodBody:
-  Enabled: false
+  Enabled: true
 
 # Avoid the use of END blocks.
 Style/EndBlock:
@@ -373,6 +373,10 @@ Style/SpaceAfterNot:
 Style/SpaceAfterSemicolon:
   Enabled: true
 
+# Use space around equals in parameter default
+Style/SpaceAroundEqualsInParameterDefault:
+  Enabled: true
+
 # Use a space around keywords if appropriate.
 Style/SpaceAroundKeyword:
   Enabled: true
diff --git a/.rubocop_todo.yml b/.rubocop_todo.yml
index 76ae59527533d9fbfb39e89d6427da38d52cb96b..20daf1619a7afa4f63eba5a33218eb23defe583f 100644
--- a/.rubocop_todo.yml
+++ b/.rubocop_todo.yml
@@ -339,13 +339,6 @@ Style/SingleLineBlockParams:
 Style/SingleLineMethods:
   Enabled: false
 
-# Offense count: 14
-# Cop supports --auto-correct.
-# Configuration parameters: EnforcedStyle, SupportedStyles.
-# SupportedStyles: space, no_space
-Style/SpaceAroundEqualsInParameterDefault:
-  Enabled: false
-
 # Offense count: 119
 # Cop supports --auto-correct.
 # Configuration parameters: EnforcedStyle, SupportedStyles.
diff --git a/.ruby-version b/.ruby-version
index ebf14b46981c4134412e7deaef0ccdc719a195d4..2bf1c1ccf363acd53eaf92ef33a7f11f5f4557c2 100644
--- a/.ruby-version
+++ b/.ruby-version
@@ -1 +1 @@
-2.1.8
+2.3.1
diff --git a/CHANGELOG b/CHANGELOG
index 85396cb4e4279ede9314f067bf64b45ebcc5b023..c4d2f79b1d94327960ee6db7423f69e44a3d1a57 100644
--- a/CHANGELOG
+++ b/CHANGELOG
@@ -1,52 +1,199 @@
 Please view this file on the master branch, on stable branches it's out of date.
 
-v 8.11.0 (unreleased)
+v 8.12.0 (unreleased)
+  - Change merge_error column from string to text type
+  - Optimistic locking for Issues and Merge Requests (title and description overriding prevention)
+  - Added tests for diff notes
+
+v 8.11.1 (unreleased)
+  - Fix file links on project page when default view is Files !5933
+
+v 8.11.0
+  - 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)
+  - Add delimiter to project stars and forks count (ClemMakesApps)
+  - 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)
+  - Fix badge count alignment (ClemMakesApps)
+  - 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
+  - Rendering of SVGs as blobs is now limited to SVGs with a size smaller or equal to 2MB
+  - Fix branches page dropdown sort initial state (ClemMakesApps)
   - Environments have an url to link to
+  - Various redundant database indexes have been removed
+  - 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)
+  - Fix icon alignment of star and fork buttons !5451 (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
+  - Fix branch title trailing space on hover (ClemMakesApps)
   - 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
   - Retrieve rendered HTML from cache in one request
   - Fix renaming repository when name contains invalid chararacters under project settings
+  - 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
+  - Load branches asynchronously in Cherry Pick and Revert dialogs.
   - 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 a way to send an email and create an issue based on private personal token. Find the email address from issues page. !3363
   - Add pipeline events to Slack integration !5525
+  - 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)
+  - Fix filter input alignment (ClemMakesApps)
   - Include old revision in merge request update hooks (Ben Boeckel)
   - Add build event color in HipChat messages (David Eisner)
   - Make fork counter always clickable. !5463 (winniehell)
+  - Document that webhook secret token is sent in X-Gitlab-Token HTTP header !5664 (lycoperdon)
+  - Gitlab::Highlight is now instrumented
   - All created issues, API or WebUI, can be submitted to Akismet for spam check !5333
+  - Allow users to import cross-repository pull requests from GitHub
   - The overhead of instrumented method calls has been reduced
   - Remove `search_id` of labels dropdown filter to fix 'Missleading URI for labels in Merge Requests and Issues view'. !5368 (Scott Le)
   - Load project invited groups and members eagerly in `ProjectTeam#fetch_members`
+  - Add pipeline events hook
+  - Award emoji tooltips containing more than 10 usernames are now truncated !4780 (jlogandavison)
+  - Fix duplicate "me" in award emoji tooltip !5218 (jlogandavison)
   - Bump gitlab_git to speedup DiffCollection iterations
+  - Rewrite description of a blocked user in admin settings. (Elias Werberich)
   - Make branches sortable without push permission !5462 (winniehell)
   - Check for Ci::Build artifacts at database level on pipeline partial
+  - Convert image diff background image to CSS (ClemMakesApps)
+  - Remove unnecessary index_projects_on_builds_enabled index from the projects table
   - Make "New issue" button in Issue page less obtrusive !5457 (winniehell)
   - Gitlab::Metrics.current_transaction needs to be public for RailsQueueDuration
   - Fix search for notes which belongs to deleted objects
+  - Allow Akismet to be trained by submitting issues as spam or ham !5538
   - Add GitLab Workhorse version to admin dashboard (Katarzyna Kobierska Ula Budziszewska)
+  - Fix spacing and vertical alignment on build status icon on commits page (ClemMakesApps)
+  - 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
+  - Fix button missing type (ClemMakesApps)
   - 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
+  - Move to project dropdown with infinite scroll for better performance
+  - 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
+
+v 8.10.7
+  - Upgrade Hamlit to 2.6.1. !5873
+  - Upgrade Doorkeeper to 4.2.0. !5881
+
+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.
+
+v 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
+  - 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
   - Fix Import/Export issue importing milestones and labels not associated properly. !5426
@@ -57,6 +204,7 @@ v 8.10.3
   - Fix importer for GitHub Pull Requests when a branch was removed. !5573
   - Ignore invalid IPs in X-Forwarded-For when trusted proxies are configured. !5584
   - 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.2
   - User can now search branches by name. !5144
@@ -135,6 +283,9 @@ v 8.10.0
   - Fix check for New Branch button on Issue page. !4630 (winniehell)
   - Fix GFM autocomplete not working on wiki pages
   - Fixed enter key not triggering click on first row when searching in a dropdown
+  - Updated dropdowns in issuable form to use new GitLab dropdown style
+  - Make images fit to the size of the viewport !4810
+  - Fix check for New Branch button on Issue page !4630 (winniehell)
   - Fix MR-auto-close text added to description. !4836
   - Support U2F devices in Firefox. !5177
   - Fix issue, preventing users w/o push access to sort tags. !5105 (redetection)
@@ -197,9 +348,11 @@ v 8.10.0
   - Fix new snippet style bug (elliotec)
   - Instrument Rinku usage
   - Be explicit to define merge request discussion variables
+  - Use cache for todos counter calling TodoService
   - Metrics for Rouge::Plugins::Redcarpet and Rouge::Formatters::HTMLGitlab
   - RailsCache metris now includes fetch_hit/fetch_miss and read_hit/read_miss info.
   - Allow [ci skip] to be in any case and allow [skip ci]. !4785 (simon_w)
+  - Made project list visibility icon fixed width
   - Set import_url validation to be more strict
   - Memoize MR merged/closed events retrieval
   - Don't render discussion notes when requesting diff tab through AJAX
@@ -246,6 +399,13 @@ v 8.10.0
   - Fix migration corrupting import data for old version upgrades
   - Show tooltip on GitLab export link in new project page
 
+v 8.9.8
+  - Upgrade Doorkeeper to 4.2.0. !5881
+
+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
   - Fix importing of events under notes for GitLab projects. !5154
   - Fix log statements in import/export. !5129
@@ -511,6 +671,12 @@ v 8.9.0
   - Add tooltip to pin/unpin navbar
   - Add new sub nav style to Wiki and Graphs sub navigation
 
+v 8.8.9
+  - Upgrade Doorkeeper to 4.2.0. !5881
+
+v 8.8.8
+  - Upgrade Rails to 4.2.7.1 for security fixes. !5781
+
 v 8.8.7
   - Fix privilege escalation issue with OAuth external users.
   - Ensure references to private repos aren't shown to logged-out users.
diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md
index 14ff05c9aa3b979d70bd201ae855464170b91f8e..d8093a61b4cfb1e0af2be74b2684ac19010d2dbb 100644
--- a/CONTRIBUTING.md
+++ b/CONTRIBUTING.md
@@ -41,6 +41,8 @@ abbreviation.
 If you have read this guide and want to know how the GitLab [core team]
 operates please see [the GitLab contributing process](PROCESS.md).
 
+- [GitLab Inc engineers should refer to the engineering workflow document](https://about.gitlab.com/handbook/engineering/workflow/)
+
 ## Contributor license agreement
 
 By submitting code as an individual you agree to the
@@ -334,6 +336,10 @@ request is as follows:
 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. 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.
 
 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
@@ -381,7 +387,8 @@ description area. Copy-paste it to retain the markdown format.
 
 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
@@ -459,8 +466,10 @@ merge request:
     - multi-line method chaining style **Option B**: dot `.` on previous line
     - string literal quoting style **Option A**: single quoted by default
 1.  [Rails](https://github.com/bbatsov/rails-style-guide)
+1.  [Newlines styleguide][newlines-styleguide]
 1.  [Testing](doc/development/testing.md)
-1.  [CoffeeScript](https://github.com/thoughtbot/guides/tree/master/style/coffeescript)
+1.  [JavaScript (ES6)](https://github.com/airbnb/javascript)
+1.  [JavaScript (ES5)](https://github.com/airbnb/javascript/tree/master/es5)
 1.  [SCSS styleguide][scss-styleguide]
 1.  [Shell commands](doc/development/shell_commands.md) created by GitLab
     contributors to enhance security
@@ -530,6 +539,7 @@ available at [http://contributor-covenant.org/version/1/1/0/](http://contributor
 [rss-naming]: https://github.com/bbatsov/ruby-style-guide/blob/master/README.md#naming
 [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/
diff --git a/GITLAB_SHELL_VERSION b/GITLAB_SHELL_VERSION
index e4604e3afd0dd7b7ebf8a8064658da22c365f6ef..18091983f59ddde8105e566545a0d9e4a12a4f1c 100644
--- a/GITLAB_SHELL_VERSION
+++ b/GITLAB_SHELL_VERSION
@@ -1 +1 @@
-3.2.1
+3.4.0
diff --git a/GITLAB_WORKHORSE_VERSION b/GITLAB_WORKHORSE_VERSION
index e7c7d3cc3c89ada8384d34fee65534801993b979..5b209ea2067d605a31380261d594aab1fc9fec3e 100644
--- a/GITLAB_WORKHORSE_VERSION
+++ b/GITLAB_WORKHORSE_VERSION
@@ -1 +1 @@
-0.7.8
+0.7.10
diff --git a/Gemfile b/Gemfile
index 5f247abd2fc99f794fba18099ef61e059be49280..68547b6fac8447f3c95ffa5e2385ab5774da1288 100644
--- a/Gemfile
+++ b/Gemfile
@@ -1,6 +1,6 @@
 source 'https://rubygems.org'
 
-gem 'rails', '4.2.7'
+gem 'rails', '4.2.7.1'
 gem 'rails-deprecated_sanitizer', '~> 1.0.3'
 
 # Responders respond_to and respond_with
@@ -20,7 +20,7 @@ gem 'pg', '~> 0.18.2', group: :postgres
 
 # Authentication libraries
 gem 'devise',                 '~> 4.0'
-gem 'doorkeeper',             '~> 4.0'
+gem 'doorkeeper',             '~> 4.2.0'
 gem 'omniauth',               '~> 1.3.1'
 gem 'omniauth-auth0',         '~> 1.4.1'
 gem 'omniauth-azure-oauth2',  '~> 0.0.6'
@@ -53,7 +53,7 @@ gem 'browser', '~> 2.2'
 
 # Extracting information from a git repository
 # Provide access to Gitlab::Git library
-gem 'gitlab_git', '~> 10.4.2'
+gem 'gitlab_git', '~> 10.4.7'
 
 # LDAP Auth
 # GitLab fork with several improvements to original library. For full list of changes
@@ -69,7 +69,7 @@ gem 'gollum-rugged_adapter', '~> 0.4.2', require: false
 gem 'github-linguist', '~> 4.7.0', require: 'linguist'
 
 # API
-gem 'grape',        '~> 0.13.0'
+gem 'grape',        '~> 0.15.0'
 gem 'grape-entity', '~> 0.4.2'
 gem 'rack-cors',    '~> 0.4.0', require: 'rack/cors'
 
@@ -77,7 +77,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'
@@ -154,7 +154,7 @@ gem 'settingslogic', '~> 2.0.9'
 
 # Misc
 
-gem 'version_sorter', '~> 2.0.0'
+gem 'version_sorter', '~> 2.1.0'
 
 # Cache
 gem 'redis-rails', '~> 4.0.0'
@@ -163,9 +163,6 @@ gem 'redis-rails', '~> 4.0.0'
 gem 'redis', '~> 3.2'
 gem 'connection_pool', '~> 2.0'
 
-# Campfire integration
-gem 'tinder', '~> 1.10.0'
-
 # HipChat integration
 gem 'hipchat', '~> 1.5.0'
 
@@ -204,7 +201,7 @@ gem 'licensee', '~> 8.0.0'
 gem 'rack-attack', '~> 4.3.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'
@@ -212,7 +209,8 @@ gem 'mousetrap-rails', '~> 1.4.6'
 # Detect and convert string character encoding
 gem 'charlock_holmes', '~> 0.7.3'
 
-# Parse duration
+# Parse time & duration
+gem 'chronic', '~> 0.10.2'
 gem 'chronic_duration', '~> 0.10.6'
 
 gem 'sass-rails', '~> 5.0.0'
@@ -317,6 +315,7 @@ 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'
@@ -326,7 +325,7 @@ group :production do
   gem 'gitlab_meta', '7.0'
 end
 
-gem 'newrelic_rpm', '~> 3.14'
+gem 'newrelic_rpm', '~> 3.16'
 
 gem 'octokit', '~> 4.3.0'
 
diff --git a/Gemfile.lock b/Gemfile.lock
index 7b4175ea824f68fa5078f4fca9272671a384d16d..5511d71893818bebc5cb1ae67c7c86d6ababba75 100644
--- a/Gemfile.lock
+++ b/Gemfile.lock
@@ -2,35 +2,35 @@ GEM
   remote: https://rubygems.org/
   specs:
     RedCloth (4.3.2)
-    ace-rails-ap (4.0.2)
-    actionmailer (4.2.7)
-      actionpack (= 4.2.7)
-      actionview (= 4.2.7)
-      activejob (= 4.2.7)
+    ace-rails-ap (4.1.0)
+    actionmailer (4.2.7.1)
+      actionpack (= 4.2.7.1)
+      actionview (= 4.2.7.1)
+      activejob (= 4.2.7.1)
       mail (~> 2.5, >= 2.5.4)
       rails-dom-testing (~> 1.0, >= 1.0.5)
-    actionpack (4.2.7)
-      actionview (= 4.2.7)
-      activesupport (= 4.2.7)
+    actionpack (4.2.7.1)
+      actionview (= 4.2.7.1)
+      activesupport (= 4.2.7.1)
       rack (~> 1.6)
       rack-test (~> 0.6.2)
       rails-dom-testing (~> 1.0, >= 1.0.5)
       rails-html-sanitizer (~> 1.0, >= 1.0.2)
-    actionview (4.2.7)
-      activesupport (= 4.2.7)
+    actionview (4.2.7.1)
+      activesupport (= 4.2.7.1)
       builder (~> 3.1)
       erubis (~> 2.7.0)
       rails-dom-testing (~> 1.0, >= 1.0.5)
       rails-html-sanitizer (~> 1.0, >= 1.0.2)
-    activejob (4.2.7)
-      activesupport (= 4.2.7)
+    activejob (4.2.7.1)
+      activesupport (= 4.2.7.1)
       globalid (>= 0.3.0)
-    activemodel (4.2.7)
-      activesupport (= 4.2.7)
+    activemodel (4.2.7.1)
+      activesupport (= 4.2.7.1)
       builder (~> 3.1)
-    activerecord (4.2.7)
-      activemodel (= 4.2.7)
-      activesupport (= 4.2.7)
+    activerecord (4.2.7.1)
+      activemodel (= 4.2.7.1)
+      activesupport (= 4.2.7.1)
       arel (~> 6.0)
     activerecord-session_store (1.0.0)
       actionpack (>= 4.0, < 5.1)
@@ -38,7 +38,7 @@ GEM
       multi_json (~> 1.11, >= 1.11.2)
       rack (>= 1.5.2, < 3)
       railties (>= 4.0, < 5.1)
-    activesupport (4.2.7)
+    activesupport (4.2.7.1)
       i18n (~> 0.7)
       json (~> 1.7, >= 1.7.7)
       minitest (~> 5.1)
@@ -128,6 +128,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)
@@ -175,7 +176,7 @@ GEM
     diff-lcs (1.2.5)
     diffy (3.0.7)
     docile (1.1.5)
-    doorkeeper (4.0.0)
+    doorkeeper (4.2.0)
       railties (>= 4.2)
     dropzonejs-rails (0.7.2)
       rails (> 3.1)
@@ -278,7 +279,7 @@ GEM
       diff-lcs (~> 1.1)
       mime-types (>= 1.16, < 3)
       posix-spawn (~> 0.3)
-    gitlab_git (10.4.2)
+    gitlab_git (10.4.7)
       activesupport (~> 4.0)
       charlock_holmes (~> 0.7.3)
       github-linguist (~> 4.7.0)
@@ -289,7 +290,7 @@ GEM
       omniauth (~> 1.0)
       pyu-ruby-sasl (~> 0.0.3.1)
       rubyntlm (~> 0.3)
-    globalid (0.3.6)
+    globalid (0.3.7)
       activesupport (>= 4.1.0)
     gollum-grit_adapter (1.0.1)
       gitlab-grit (~> 2.7, >= 2.7.1)
@@ -308,7 +309,7 @@ GEM
       json
       multi_json
       request_store (>= 1.0)
-    grape (0.13.0)
+    grape (0.15.0)
       activesupport
       builder
       hashie (>= 2.1.0)
@@ -321,7 +322,7 @@ GEM
     grape-entity (0.4.8)
       activesupport
       multi_json (>= 1.3.2)
-    hamlit (2.5.0)
+    hamlit (2.6.1)
       temple (~> 0.7.6)
       thor
       tilt
@@ -335,11 +336,10 @@ GEM
       activesupport (>= 2)
       nokogiri (~> 1.4)
     htmlentities (4.3.4)
-    http_parser.rb (0.5.3)
     httparty (0.13.7)
       json (~> 1.8)
       multi_xml (>= 0.5.2)
-    httpclient (2.7.0.1)
+    httpclient (2.8.2)
     i18n (0.7.0)
     ice_nine (0.11.1)
     influxdb (0.2.3)
@@ -357,6 +357,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)
@@ -404,7 +406,7 @@ GEM
     nested_form (0.3.2)
     net-ldap (0.12.1)
     net-ssh (3.0.1)
-    newrelic_rpm (3.14.1.311)
+    newrelic_rpm (3.16.0.318)
     nokogiri (1.6.8)
       mini_portile2 (~> 2.1.0)
       pkg-config (~> 1.1.7)
@@ -519,16 +521,16 @@ GEM
       rack
     rack-test (0.6.3)
       rack (>= 1.0)
-    rails (4.2.7)
-      actionmailer (= 4.2.7)
-      actionpack (= 4.2.7)
-      actionview (= 4.2.7)
-      activejob (= 4.2.7)
-      activemodel (= 4.2.7)
-      activerecord (= 4.2.7)
-      activesupport (= 4.2.7)
+    rails (4.2.7.1)
+      actionmailer (= 4.2.7.1)
+      actionpack (= 4.2.7.1)
+      actionview (= 4.2.7.1)
+      activejob (= 4.2.7.1)
+      activemodel (= 4.2.7.1)
+      activerecord (= 4.2.7.1)
+      activesupport (= 4.2.7.1)
       bundler (>= 1.3.0, < 2.0)
-      railties (= 4.2.7)
+      railties (= 4.2.7.1)
       sprockets-rails
     rails-deprecated_sanitizer (1.0.3)
       activesupport (>= 4.2.0.alpha)
@@ -538,9 +540,9 @@ GEM
       rails-deprecated_sanitizer (>= 1.0.1)
     rails-html-sanitizer (1.0.3)
       loofah (~> 2.0)
-    railties (4.2.7)
-      actionpack (= 4.2.7)
-      activesupport (= 4.2.7)
+    railties (4.2.7.1)
+      actionpack (= 4.2.7.1)
+      activesupport (= 4.2.7.1)
       rake (>= 0.8.7)
       thor (>= 0.18.1, < 2.0)
     rainbow (2.1.0)
@@ -672,7 +674,6 @@ GEM
       redis-namespace (>= 1.5.2)
       rufus-scheduler (>= 2.0.24)
       sidekiq (>= 4.0.0)
-    simple_oauth (0.1.9)
     simplecov (0.12.0)
       docile (~> 1.1.0)
       json (>= 1.8, < 3)
@@ -742,21 +743,8 @@ GEM
     tilt (2.0.5)
     timecop (0.8.1)
     timfel-krb5-auth (0.8.3)
-    tinder (1.10.1)
-      eventmachine (~> 1.0)
-      faraday (~> 0.9.0)
-      faraday_middleware (~> 0.9)
-      hashie (>= 1.0)
-      json (~> 1.8.0)
-      mime-types
-      multi_json (~> 1.7)
-      twitter-stream (~> 0.1)
     turbolinks (2.5.3)
       coffee-rails
-    twitter-stream (0.1.16)
-      eventmachine (>= 0.12.8)
-      http_parser.rb (~> 0.5.1)
-      simple_oauth (~> 0.1.4)
     tzinfo (1.2.2)
       thread_safe (~> 0.1)
     u2f (0.2.1)
@@ -778,7 +766,7 @@ GEM
     uniform_notifier (1.10.0)
     uuid (2.3.8)
       macaddr (~> 1.0)
-    version_sorter (2.0.0)
+    version_sorter (2.1.0)
     virtus (1.0.5)
       axiom-types (~> 0.1)
       coercible (~> 1.0)
@@ -811,7 +799,7 @@ 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)
   addressable (~> 2.3.8)
@@ -837,6 +825,7 @@ 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)
@@ -847,7 +836,7 @@ DEPENDENCIES
   devise (~> 4.0)
   devise-two-factor (~> 3.0.0)
   diffy (~> 3.0.3)
-  doorkeeper (~> 4.0)
+  doorkeeper (~> 4.2.0)
   dropzonejs-rails (~> 0.7.1)
   email_reply_parser (~> 0.5.8)
   email_spec (~> 1.6.0)
@@ -870,15 +859,15 @@ DEPENDENCIES
   github-linguist (~> 4.7.0)
   github-markup (~> 1.4)
   gitlab-flowdock-git-hook (~> 1.0.1)
-  gitlab_git (~> 10.4.2)
+  gitlab_git (~> 10.4.7)
   gitlab_meta (= 7.0)
   gitlab_omniauth-ldap (~> 1.2.1)
   gollum-lib (~> 4.2)
   gollum-rugged_adapter (~> 0.4.2)
   gon (~> 6.1.0)
-  grape (~> 0.13.0)
+  grape (~> 0.15.0)
   grape-entity (~> 0.4.2)
-  hamlit (~> 2.5)
+  hamlit (~> 2.6.1)
   health_check (~> 2.1.0)
   hipchat (~> 1.5.0)
   html-pipeline (~> 1.11.0)
@@ -888,6 +877,7 @@ DEPENDENCIES
   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)
@@ -902,7 +892,7 @@ DEPENDENCIES
   mysql2 (~> 0.3.16)
   nested_form (~> 0.3.2)
   net-ssh (~> 3.0.1)
-  newrelic_rpm (~> 3.14)
+  newrelic_rpm (~> 3.16)
   nokogiri (~> 1.6.7, >= 1.6.7.2)
   oauth2 (~> 1.2.0)
   octokit (~> 4.3.0)
@@ -929,7 +919,7 @@ DEPENDENCIES
   rack-attack (~> 4.3.1)
   rack-cors (~> 0.4.0)
   rack-oauth2 (~> 1.2.1)
-  rails (= 4.2.7)
+  rails (= 4.2.7.1)
   rails-deprecated_sanitizer (~> 1.0.3)
   rainbow (~> 2.1.0)
   rblineprof (~> 0.3.6)
@@ -981,7 +971,6 @@ DEPENDENCIES
   teaspoon-jasmine (~> 2.2.0)
   test_after_commit (~> 0.4.2)
   thin (~> 1.7.0)
-  tinder (~> 1.10.0)
   turbolinks (~> 2.5.0)
   u2f (~> 0.2.1)
   uglifier (~> 2.7.2)
@@ -989,7 +978,7 @@ DEPENDENCIES
   unf (~> 0.1.4)
   unicorn (~> 4.9.0)
   unicorn-worker-killer (~> 0.4.2)
-  version_sorter (~> 2.0.0)
+  version_sorter (~> 2.1.0)
   virtus (~> 1.0.1)
   vmstat (~> 2.1.1)
   web-console (~> 2.0)
diff --git a/PROCESS.md b/PROCESS.md
index fe3a963110df09793d694e8175318c2d18a2da15..8e1a3f7360f6508b35cf76ea8703dad873c72b32 100644
--- a/PROCESS.md
+++ b/PROCESS.md
@@ -8,6 +8,8 @@ treatment, etc.). And so that maintainers know what to expect from contributors
 (use the latest version, ensure that the issue is addressed, friendly treatment,
 etc.).
 
+- [GitLab Inc engineers should refer to the engineering workflow document](https://about.gitlab.com/handbook/engineering/workflow/)
+
 ## Common actions
 
 ### Issue team
diff --git a/VERSION b/VERSION
index 542e7824102447bf9fe4091b802e0713848e87c4..8a5bbc87ef9c37877aec85a7c7e1ed91768f4a7a 100644
--- a/VERSION
+++ b/VERSION
@@ -1 +1 @@
-8.11.0-pre
+8.12.0-pre
diff --git a/app/assets/images/bg-header.png b/app/assets/images/bg-header.png
deleted file mode 100644
index 639271c6fafc58a2007f22478ba2d52e57c2a2ee..0000000000000000000000000000000000000000
Binary files a/app/assets/images/bg-header.png and /dev/null differ
diff --git a/app/assets/images/bg_fallback.png b/app/assets/images/bg_fallback.png
deleted file mode 100644
index 5c55bc79dec6eb904d293440e804fcc19f7632f1..0000000000000000000000000000000000000000
Binary files a/app/assets/images/bg_fallback.png and /dev/null differ
diff --git a/app/assets/images/chosen-sprite.png b/app/assets/images/chosen-sprite.png
deleted file mode 100644
index 3d936b07d443fb420a71cac72750537bb54ad2cd..0000000000000000000000000000000000000000
Binary files a/app/assets/images/chosen-sprite.png and /dev/null differ
diff --git a/app/assets/images/diff_note_add.png b/app/assets/images/diff_note_add.png
deleted file mode 100644
index 0084422e3303593e2f067c7de013413f393b0f88..0000000000000000000000000000000000000000
Binary files a/app/assets/images/diff_note_add.png and /dev/null differ
diff --git a/app/assets/images/icon-search.png b/app/assets/images/icon-search.png
deleted file mode 100644
index 3c1c146541d456a042db5768154a307b9b535e9d..0000000000000000000000000000000000000000
Binary files a/app/assets/images/icon-search.png and /dev/null differ
diff --git a/app/assets/images/icon_sprite.png b/app/assets/images/icon_sprite.png
deleted file mode 100644
index 2e7a5023398e7aa1d2794755af4f90d59b431919..0000000000000000000000000000000000000000
Binary files a/app/assets/images/icon_sprite.png and /dev/null differ
diff --git a/app/assets/images/images.png b/app/assets/images/images.png
deleted file mode 100644
index bd60de994c41ec8059cc09d2a950679d9ee4a17d..0000000000000000000000000000000000000000
Binary files a/app/assets/images/images.png and /dev/null differ
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/move.png b/app/assets/images/move.png
deleted file mode 100644
index 6a0567f8f2534837e7280dd41e4bf4b98725a3bf..0000000000000000000000000000000000000000
Binary files a/app/assets/images/move.png and /dev/null differ
diff --git a/app/assets/images/progress_bar.gif b/app/assets/images/progress_bar.gif
deleted file mode 100644
index c3d43fa40b2fd90186d22f8a82bdc4673d8ab904..0000000000000000000000000000000000000000
Binary files a/app/assets/images/progress_bar.gif and /dev/null differ
diff --git a/app/assets/images/slider_handles.png b/app/assets/images/slider_handles.png
deleted file mode 100644
index 52ad11ab7a1424e5a2346c4b50c16639fef14814..0000000000000000000000000000000000000000
Binary files a/app/assets/images/slider_handles.png and /dev/null differ
diff --git a/app/assets/images/switch_icon.png b/app/assets/images/switch_icon.png
deleted file mode 100644
index c6b6c8d9521f64b00990ca5352c8ce269e9a3e4a..0000000000000000000000000000000000000000
Binary files a/app/assets/images/switch_icon.png and /dev/null differ
diff --git a/app/assets/images/trans_bg.gif b/app/assets/images/trans_bg.gif
deleted file mode 100644
index 1a1c9c15ec71a58db869578399068cf313c51599..0000000000000000000000000000000000000000
Binary files a/app/assets/images/trans_bg.gif and /dev/null differ
diff --git a/app/assets/javascripts/abuse_reports.js.es6 b/app/assets/javascripts/abuse_reports.js.es6
new file mode 100644
index 0000000000000000000000000000000000000000..748084b0307350b7979762a91cab7510609b1515
--- /dev/null
+++ b/app/assets/javascripts/abuse_reports.js.es6
@@ -0,0 +1,39 @@
+window.gl = window.gl || {};
+((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);
diff --git a/app/assets/javascripts/api.js b/app/assets/javascripts/api.js
index 49c2ac0dac3fb9fc469b52315616772a5f26e9be..84b292e59c643756ceefa00024be7529557b8a68 100644
--- a/app/assets/javascripts/api.js
+++ b/app/assets/javascripts/api.js
@@ -9,10 +9,11 @@
     licensePath: "/api/:version/licenses/:key",
     gitignorePath: "/api/:version/gitignores/:key",
     gitlabCiYmlPath: "/api/:version/gitlab_ci_ymls/:key",
+    issuableTemplatePath: "/:namespace_path/:project_path/templates/:type/:key",
+
     group: function(group_id, callback) {
-      var url;
-      url = Api.buildUrl(Api.groupPath);
-      url = url.replace(':id', group_id);
+      var url = Api.buildUrl(Api.groupPath)
+        .replace(':id', group_id);
       return $.ajax({
         url: url,
         data: {
@@ -24,8 +25,7 @@
       });
     },
     groups: function(query, skip_ldap, callback) {
-      var url;
-      url = Api.buildUrl(Api.groupsPath);
+      var url = Api.buildUrl(Api.groupsPath);
       return $.ajax({
         url: url,
         data: {
@@ -39,8 +39,7 @@
       });
     },
     namespaces: function(query, callback) {
-      var url;
-      url = Api.buildUrl(Api.namespacesPath);
+      var url = Api.buildUrl(Api.namespacesPath);
       return $.ajax({
         url: url,
         data: {
@@ -54,8 +53,7 @@
       });
     },
     projects: function(query, order, callback) {
-      var url;
-      url = Api.buildUrl(Api.projectsPath);
+      var url = Api.buildUrl(Api.projectsPath);
       return $.ajax({
         url: url,
         data: {
@@ -70,9 +68,8 @@
       });
     },
     newLabel: function(project_id, data, callback) {
-      var url;
-      url = Api.buildUrl(Api.labelsPath);
-      url = url.replace(':id', project_id);
+      var url = Api.buildUrl(Api.labelsPath)
+        .replace(':id', project_id);
       data.private_token = gon.api_token;
       return $.ajax({
         url: url,
@@ -86,9 +83,8 @@
       });
     },
     groupProjects: function(group_id, query, callback) {
-      var url;
-      url = Api.buildUrl(Api.groupProjectsPath);
-      url = url.replace(':id', group_id);
+      var url = Api.buildUrl(Api.groupProjectsPath)
+        .replace(':id', group_id);
       return $.ajax({
         url: url,
         data: {
@@ -102,8 +98,8 @@
       });
     },
     licenseText: function(key, data, callback) {
-      var url;
-      url = Api.buildUrl(Api.licensePath).replace(':key', key);
+      var url = Api.buildUrl(Api.licensePath)
+        .replace(':key', key);
       return $.ajax({
         url: url,
         data: data
@@ -112,19 +108,32 @@
       });
     },
     gitignoreText: function(key, callback) {
-      var url;
-      url = Api.buildUrl(Api.gitignorePath).replace(':key', key);
+      var url = Api.buildUrl(Api.gitignorePath)
+        .replace(':key', key);
       return $.get(url, function(gitignore) {
         return callback(gitignore);
       });
     },
     gitlabCiYml: function(key, callback) {
-      var url;
-      url = Api.buildUrl(Api.gitlabCiYmlPath).replace(':key', key);
+      var url = Api.buildUrl(Api.gitlabCiYmlPath)
+        .replace(':key', key);
       return $.get(url, function(file) {
         return callback(file);
       });
     },
+    issueTemplate: function(namespacePath, projectPath, key, type, callback) {
+      var url = Api.buildUrl(Api.issuableTemplatePath)
+        .replace(':key', key)
+        .replace(':type', type)
+        .replace(':project_path', projectPath)
+        .replace(':namespace_path', namespacePath);
+      $.ajax({
+        url: url,
+        dataType: 'json'
+      }).done(function(file) {
+        callback(null, file);
+      }).error(callback);
+    },
     buildUrl: function(url) {
       if (gon.relative_url_root != null) {
         url = gon.relative_url_root + url;
diff --git a/app/assets/javascripts/application.js b/app/assets/javascripts/application.js
index 127e568adc97542f62e0e5c2ee387ab399a909a2..fc354dfd67766daa150ac0ad6daee27207f78d26 100644
--- a/app/assets/javascripts/application.js
+++ b/app/assets/javascripts/application.js
@@ -26,8 +26,6 @@
 /*= require bootstrap/tooltip */
 /*= require bootstrap/popover */
 /*= require select2 */
-/*= require ace/ace */
-/*= require ace/ext-searchbox */
 /*= require underscore */
 /*= require dropzone */
 /*= require mousetrap */
@@ -41,6 +39,7 @@
 /*= require date.format */
 /*= require_directory ./behaviors */
 /*= require_directory ./blob */
+/*= require_directory ./templates */
 /*= require_directory ./commit */
 /*= require_directory ./extensions */
 /*= require_directory ./lib/utils */
@@ -152,7 +151,9 @@
       });
     });
     $('.remove-row').bind('ajax:success', function() {
-      return $(this).closest('li').fadeOut();
+      $(this).tooltip('destroy')
+        .closest('li')
+        .fadeOut();
     });
     $('.js-remove-tr').bind('ajax:before', function() {
       return $(this).hide();
@@ -223,8 +224,14 @@
       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();
+      var $this = $(this);
+      $this.toggleClass('active');
+      var notesHolders = $this.closest('.diff-file').find('.notes_holder');
+      if ($this.hasClass('active')) {
+        notesHolders.show();
+      } else {
+        notesHolders.hide();
+      }
       return e.preventDefault();
     });
     $document.off("click", '.js-confirm-danger');
@@ -287,7 +294,7 @@
       $('.page-with-sidebar').toggleClass('page-sidebar-collapsed page-sidebar-expanded').removeClass('page-sidebar-pinned');
       $('.navbar-fixed-top').removeClass('header-pinned-nav');
     }
-    return $document.off('click', '.js-nav-pin').on('click', '.js-nav-pin', function(e) {
+    $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);
@@ -315,6 +322,8 @@
       $tooltip.find('.tooltip-inner').text(tooltipText);
       return $pinBtn.attr('title', tooltipText).tooltip('fixTitle');
     });
-  });
 
+    // Custom time ago
+    gl.utils.shortTimeAgo($('.js-short-timeago'));
+  });
 }).call(this);
diff --git a/app/assets/javascripts/awards_handler.js b/app/assets/javascripts/awards_handler.js
index ea683b31f75a2ce1ef066984c01d70239af0fd74..aee1c29eee3be894b6b76a652f336985f2f89fa9 100644
--- a/app/assets/javascripts/awards_handler.js
+++ b/app/assets/javascripts/awards_handler.js
@@ -1,5 +1,6 @@
 (function() {
   this.AwardsHandler = (function() {
+    const 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) {
@@ -130,7 +131,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 {
@@ -161,23 +162,11 @@
         $emojiButton = votesBlock.find("[data-emoji=" + mutualVote + "]").parent();
         isAlreadyVoted = $emojiButton.hasClass('active');
         if (isAlreadyVoted) {
-          this.showEmojiLoader($emojiButton);
-          return this.addAward(votesBlock, awardUrl, mutualVote, false, function() {
-            return $emojiButton.removeClass('is-loading');
-          });
+          this.addAward(votesBlock, awardUrl, mutualVote, false);
         }
       }
     };
 
-    AwardsHandler.prototype.showEmojiLoader = function($emojiButton) {
-      var $loader;
-      $loader = $emojiButton.find('.fa-spinner');
-      if (!$loader.length) {
-        $emojiButton.append('<i class="fa fa-spinner fa-spin award-control-icon award-control-icon-loading"></i>');
-      }
-      return $emojiButton.addClass('is-loading');
-    };
-
     AwardsHandler.prototype.isActive = function($emojiButton) {
       return $emojiButton.hasClass('active');
     };
@@ -188,11 +177,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);
         }
@@ -216,43 +205,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);
diff --git a/app/assets/javascripts/blob/template_selector.js b/app/assets/javascripts/blob/template_selector.js
index 2cf0a6631b8d4e3ab8b90dc4283d940c3413b72d..b0a37ef0e0a4f57b1b76b51ab24c3f63e48bc432 100644
--- a/app/assets/javascripts/blob/template_selector.js
+++ b/app/assets/javascripts/blob/template_selector.js
@@ -9,6 +9,7 @@
       }
       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();
@@ -60,11 +61,26 @@
       return this.requestFile(item);
     };
 
-    TemplateSelector.prototype.requestFile = function(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) {
+    TemplateSelector.prototype.requestFileSuccess = function(file, skipFocus) {
       this.editor.setValue(file.content, 1);
-      return this.editor.focus();
+      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;
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..2afef43f3d6d53ae525b614353db51167c35d0b2
--- /dev/null
+++ b/app/assets/javascripts/blob_edit/blob_edit_bundle.js
@@ -0,0 +1,12 @@
+/*= 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 100%
rename from app/assets/javascripts/blob/edit_blob.js
rename to app/assets/javascripts/blob_edit/edit_blob.js
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..a612cf0f1aed57eac5776581db40626bfb5e0918
--- /dev/null
+++ b/app/assets/javascripts/boards/boards_bundle.js.es6
@@ -0,0 +1,57 @@
+//= require vue
+//= require vue-resource
+//= require Sortable
+//= require_tree ./models
+//= require_tree ./stores
+//= require_tree ./services
+//= require_tree ./mixins
+//= require ./components/board
+//= 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
+    },
+    data: {
+      state: Store.state,
+      loading: true,
+      endpoint: $boardApp.dataset.endpoint,
+      disabled: $boardApp.dataset.disabled === 'true',
+      issueLinkBase: $boardApp.dataset.issueLinkBase
+    },
+    init: Store.create.bind(Store),
+    created () {
+      gl.boardService = new BoardService(this.endpoint);
+    },
+    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;
+        });
+    }
+  });
+});
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..d7f4107cb021d1971aef227c1ebb4b99762af8e4
--- /dev/null
+++ b/app/assets/javascripts/boards/components/board.js.es6
@@ -0,0 +1,81 @@
+//= 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 {
+        query: '',
+        filters: Store.state.filters
+      };
+    },
+    watch: {
+      query () {
+        this.list.filters = this.getFilterData();
+        this.list.getIssues(true);
+      },
+      filters: {
+        handler () {
+          this.list.page = 1;
+          this.list.getIssues(true);
+        },
+        deep: true
+      }
+    },
+    methods: {
+      getFilterData () {
+        const filters = this.filters;
+        let queryData = { search: this.query };
+
+        Object.keys(filters).forEach((key) => { queryData[key] = filters[key]; });
+
+        return queryData;
+      }
+    },
+    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..63d72d857d9c496463cd8aaa1b235369e9c903fc
--- /dev/null
+++ b/app/assets/javascripts/boards/components/board_blank_state.js.es6
@@ -0,0 +1,49 @@
+(() => {
+  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: 'Development', color: '#5CB85C' }),
+          new ListLabel({ title: 'Testing', color: '#F0AD4E' }),
+          new ListLabel({ title: 'Production', color: '#FF5F00' }),
+          new ListLabel({ title: 'Ready', color: '#FF0000' })
+        ]
+      }
+    },
+    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..4a7cfeaeab22e29c46982ac1129f6eba76835aa1
--- /dev/null
+++ b/app/assets/javascripts/boards/components/board_card.js.es6
@@ -0,0 +1,43 @@
+(() => {
+  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
+    },
+    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();
+      }
+    }
+  });
+})();
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..34653cd48ef190289643cc651de33c3a9fa13eca
--- /dev/null
+++ b/app/assets/javascripts/boards/components/board_delete.js.es6
@@ -0,0 +1,19 @@
+(() => {
+  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..a6644e9eb8c4b0d342b7d250bd125cbedf4d3f32
--- /dev/null
+++ b/app/assets/javascripts/boards/components/board_list.js.es6
@@ -0,0 +1,87 @@
+//= require ./board_card
+
+(() => {
+  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
+    },
+    props: {
+      disabled: Boolean,
+      list: Object,
+      issues: Array,
+      loading: Boolean,
+      issueLinkBase: String
+    },
+    data () {
+      return {
+        scrollOffset: 250,
+        filters: Store.state.filters
+      };
+    },
+    watch: {
+      filters: {
+        handler () {
+          this.list.loadingMore = false;
+          this.$els.list.scrollTop = 0;
+        },
+        deep: true
+      }
+    },
+    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,
+        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/new_list_dropdown.js.es6 b/app/assets/javascripts/boards/components/new_list_dropdown.js.es6
new file mode 100644
index 0000000000000000000000000000000000000000..1a4d81579700ae7e82ec08f268eaf5e66f9c143d
--- /dev/null
+++ b/app/assets/javascripts/boards/components/new_list_dropdown.js.es6
@@ -0,0 +1,54 @@
+$(() => {
+  const Store = gl.issueBoards.BoardsStore;
+
+  $('.js-new-board-list').each(function () {
+    const $this = $(this);
+
+    new gl.CreateLabelDropdown($this.closest('.dropdown').find('.dropdown-new-label'), $this.data('project-id'));
+
+    $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,
+      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/mixins/sortable_default_options.js.es6 b/app/assets/javascripts/boards/mixins/sortable_default_options.js.es6
new file mode 100644
index 0000000000000000000000000000000000000000..44addb3ea98180700315ce1561a9b30833be0202
--- /dev/null
+++ b/app/assets/javascripts/boards/mixins/sortable_default_options.js.es6
@@ -0,0 +1,35 @@
+((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: '.has-tooltip',
+      delay: gl.issueBoards.touchEnabled ? 100 : 0,
+      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..eb082103de9ceccbc62fd4f83fbd43411e5f4392
--- /dev/null
+++ b/app/assets/javascripts/boards/models/issue.js.es6
@@ -0,0 +1,44 @@
+class ListIssue {
+  constructor (obj) {
+    this.id = obj.iid;
+    this.title = obj.title;
+    this.confidential = obj.confidential;
+    this.labels = [];
+
+    if (obj.assignee) {
+      this.assignee = new ListUser(obj.assignee);
+    }
+
+    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) );
+  }
+}
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..583829552cd398347d95bb32b0a2bcf6481d43cf
--- /dev/null
+++ b/app/assets/javascripts/boards/models/label.js.es6
@@ -0,0 +1,10 @@
+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..be2b8c568a83c5dec6e0ccbdb6842d27dd38de6a
--- /dev/null
+++ b/app/assets/javascripts/boards/models/list.js.es6
@@ -0,0 +1,125 @@
+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 = [];
+
+    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 (Math.floor(this.issues.length / 20) === this.page) {
+      this.page++;
+
+      return this.getIssues(false);
+    }
+  }
+
+  canSearch () {
+    return this.type === 'backlog';
+  }
+
+  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;
+
+        if (emptyIssues) {
+          this.issues = [];
+        }
+
+        this.createIssues(data);
+      });
+  }
+
+  createIssues (data) {
+    data.forEach((issueObj) => {
+      this.addIssue(new ListIssue(issueObj));
+    });
+  }
+
+  addIssue (issue, listFrom) {
+    this.issues.push(issue);
+
+    if (this.label) {
+      issue.addLabel(this.label);
+    }
+
+    if (listFrom) {
+      gl.boardService.moveIssue(issue.id, listFrom.id, this.id);
+    }
+  }
+
+  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) {
+        issue.removeLabel(this.label);
+      }
+
+      return !matchesRemove;
+    });
+  }
+}
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..904b3a68507a3d1e897cc8cb938bfad6001198ea
--- /dev/null
+++ b/app/assets/javascripts/boards/models/user.js.es6
@@ -0,0 +1,8 @@
+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..9b80fb2e99fd89614c331b8f4189023a5b340be6
--- /dev/null
+++ b/app/assets/javascripts/boards/services/board_service.js.es6
@@ -0,0 +1,61 @@
+class BoardService {
+  constructor (root) {
+    Vue.http.options.root = root;
+
+    this.lists = Vue.resource(`${root}/lists{/id}`, {}, {
+      generate: {
+        method: 'POST',
+        url: `${root}/lists/generate.json`
+      }
+    });
+    this.issue = Vue.resource(`${root}/issues{/id}`, {});
+    this.issues = Vue.resource(`${root}/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
+    });
+  }
+};
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..18f26a1f911177e0828af1b640dd96e8a5ee250f
--- /dev/null
+++ b/app/assets/javascripts/boards/stores/boards_store.js.es6
@@ -0,0 +1,112 @@
+(() => {
+  window.gl = window.gl || {};
+  window.gl.issueBoards = window.gl.issueBoards || {};
+
+  gl.issueBoards.BoardsStore = {
+    disabled: false,
+    state: {},
+    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[]')
+      };
+    },
+    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');
+
+      $.cookie('issue_board_welcome_hidden', 'true', {
+        expires: 365 * 10
+      });
+    },
+    welcomeIsHidden () {
+      return $.cookie('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..75f8b73019555ee8af9ba3a5e8e3ddde3f9d2ae8
--- /dev/null
+++ b/app/assets/javascripts/boards/test_utils/simulate_drag.js
@@ -0,0 +1,119 @@
+(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..f9f9f7999d4679e55313a07e0af6a8492ffa0916
--- /dev/null
+++ b/app/assets/javascripts/boards/vue_resource_interceptor.js.es6
@@ -0,0 +1,10 @@
+Vue.http.interceptors.push((request, next)  => {
+  Vue.activeResources = Vue.activeResources ? Vue.activeResources + 1 : 1;
+
+  Vue.nextTick(() => {
+    setTimeout(() => {
+      Vue.activeResources--;
+    }, 500);
+  });
+  next();
+});
diff --git a/app/assets/javascripts/build.js b/app/assets/javascripts/build.js
index e135cb92a3093e73e6a322a071eab88b3e5d6b43..0d7d29bb0d0272934685ec09289359ab08620e75 100644
--- a/app/assets/javascripts/build.js
+++ b/app/assets/javascripts/build.js
@@ -6,19 +6,26 @@
 
     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;
+    function Build(options) {
+      this.page_url = options.page_url;
+      this.build_url = options.build_url;
+      this.build_status = options.build_status;
+      this.state = options.state1;
+      this.build_stage = options.build_stage;
       this.hideSidebar = bind(this.hideSidebar, this);
       this.toggleSidebar = bind(this.toggleSidebar, this);
+      this.updateDropdown = bind(this.updateDropdown, this);
       clearInterval(Build.interval);
       this.bp = Breakpoints.get();
-      this.hideSidebar();
       $('.js-build-sidebar').niceScroll();
+
+      this.populateJobs(this.build_stage);
+      this.updateStageDropdownText(this.build_stage);
+      this.hideSidebar();
+
       $(document).off('click', '.js-sidebar-build-toggle').on('click', '.js-sidebar-build-toggle', this.toggleSidebar);
       $(window).off('resize.build').on('resize.build', this.hideSidebar);
+      $(document).off('click', '.stage-item').on('click', '.stage-item', this.updateDropdown);
       this.updateArtifactRemoveDate();
       if ($('#build-trace').length) {
         this.getInitialBuildTrace();
@@ -128,10 +135,26 @@
       $date = $('.js-artifacts-remove');
       if ($date.length) {
         date = $date.text();
-        return $date.text($.timefor(new Date(date), ' '));
+        return $date.text($.timefor(new Date(date.replace(/-/g, '/')), ' '));
       }
     };
 
+    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);
+    };
+
     return Build;
 
   })();
diff --git a/app/assets/javascripts/copy_to_clipboard.js b/app/assets/javascripts/copy_to_clipboard.js
index c82798cc6a58b079e268969b646db5dffedba3b2..c43af17442b46be1cdd0c041d486d6ac6bdff599 100644
--- a/app/assets/javascripts/copy_to_clipboard.js
+++ b/app/assets/javascripts/copy_to_clipboard.js
@@ -34,6 +34,7 @@
 
   $(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..46d1c3f00c1e599a6b24d4edd2a42103da9c3c18
--- /dev/null
+++ b/app/assets/javascripts/create_label.js.es6
@@ -0,0 +1,126 @@
+(function (w) {
+  class CreateLabelDropdown {
+    constructor ($el, projectId) {
+      this.$el = $el;
+      this.projectId = projectId;
+      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.projectId, {
+        name: 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');
+        }
+      });
+    }
+  }
+
+  if (!w.gl) {
+    w.gl = {};
+  }
+
+  gl.CreateLabelDropdown = CreateLabelDropdown;
+})(window);
diff --git a/app/assets/javascripts/diff.js b/app/assets/javascripts/diff.js
index 298f3852085da45dd299b2cbd05ed6fa398502cc..3dd7ceba92faaa61683d2304f521d12274f1b207 100644
--- a/app/assets/javascripts/diff.js
+++ b/app/assets/javascripts/diff.js
@@ -10,7 +10,7 @@
       $(document).off('click', '.js-unfold');
       $(document).on('click', '.js-unfold', (function(_this) {
         return function(event) {
-          var line_number, link, offset, old_line, params, prev_new_line, prev_old_line, ref, ref1, since, target, to, unfold, unfoldBottom;
+          var line_number, link, file, offset, old_line, params, prev_new_line, prev_old_line, ref, ref1, since, target, to, unfold, unfoldBottom;
           target = $(event.target);
           unfoldBottom = target.hasClass('js-unfold-bottom');
           unfold = true;
@@ -31,14 +31,16 @@
               unfold = false;
             }
           }
-          link = target.parents('.diff-file').attr('data-blob-diff-path');
+          file = target.parents('.diff-file');
+          link = file.data('blob-diff-path');
           params = {
             since: since,
             to: to,
             bottom: unfoldBottom,
             offset: offset,
             unfold: unfold,
-            indent: 1
+            indent: 1,
+            view: file.data('view')
           };
           return $.get(link, params, function(response) {
             return target.parent().replaceWith(response);
@@ -48,26 +50,13 @@
     }
 
     Diff.prototype.lineNumbers = function(line) {
-      var i, l, len, line_number, line_numbers, lines, results;
       if (!line.children().length) {
         return [0, 0];
       }
-      lines = line.children().slice(0, 2);
-      line_numbers = (function() {
-        var i, len, results;
-        results = [];
-        for (i = 0, len = lines.length; i < len; i++) {
-          l = lines[i];
-          results.push($(l).attr('data-linenumber'));
-        }
-        return results;
-      })();
-      results = [];
-      for (i = 0, len = line_numbers.length; i < len; i++) {
-        line_number = line_numbers[i];
-        results.push(parseInt(line_number));
-      }
-      return results;
+
+      return line.find('.diff-line-num').map(function() {
+        return parseInt($(this).data('linenumber'));
+      });
     };
 
     return Diff;
diff --git a/app/assets/javascripts/diff_notes/components/comment_resolve_btn.js.es6 b/app/assets/javascripts/diff_notes/components/comment_resolve_btn.js.es6
new file mode 100644
index 0000000000000000000000000000000000000000..48bc7d7780501e49a328b12046c88d5cd9a0ebf0
--- /dev/null
+++ b/app/assets/javascripts/diff_notes/components/comment_resolve_btn.js.es6
@@ -0,0 +1,49 @@
+((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..ad80d1118dfb85e354354109a8fdb02422fa3978
--- /dev/null
+++ b/app/assets/javascripts/diff_notes/components/jump_to_discussion.js.es6
@@ -0,0 +1,188 @@
+(() => {
+  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..be6ebc77947071eb327bc7181a8ef59016fb86c4
--- /dev/null
+++ b/app/assets/javascripts/diff_notes/components/resolve_btn.js.es6
@@ -0,0 +1,107 @@
+((w) => {
+  w.ResolveBtn = Vue.extend({
+    mixins: [
+      ButtonMixins
+    ],
+    props: {
+      noteId: Number,
+      discussionId: String,
+      resolved: Boolean,
+      namespacePath: String,
+      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.namespace, this.noteId);
+        } else {
+          promise = ResolveService
+            .resolve(this.namespace, 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..9e383b14a3e9a19a058bea516b6055d17c4892d9
--- /dev/null
+++ b/app/assets/javascripts/diff_notes/components/resolve_count.js.es6
@@ -0,0 +1,18 @@
+((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..e373b06b1ebd51662ec17282be2c32629db56d6e
--- /dev/null
+++ b/app/assets/javascripts/diff_notes/components/resolve_discussion_btn.js.es6
@@ -0,0 +1,60 @@
+((w) => {
+  w.ResolveDiscussionBtn = Vue.extend({
+    mixins: [
+      ButtonMixins
+    ],
+    props: {
+      discussionId: String,
+      mergeRequestId: Number,
+      namespacePath: String,
+      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.namespace, 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..22d9cf6c857b2c05282f502fe73678d9aaca1e54
--- /dev/null
+++ b/app/assets/javascripts/diff_notes/diff_notes_bundle.js.es6
@@ -0,0 +1,35 @@
+//= 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..a05f885201d4faadfc24b7aade073e81608abb15
--- /dev/null
+++ b/app/assets/javascripts/diff_notes/mixins/discussion.js.es6
@@ -0,0 +1,35 @@
+((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/mixins/namespace.js.es6 b/app/assets/javascripts/diff_notes/mixins/namespace.js.es6
new file mode 100644
index 0000000000000000000000000000000000000000..d278678085b99a7b27f80a75a8ca4f75c161031a
--- /dev/null
+++ b/app/assets/javascripts/diff_notes/mixins/namespace.js.es6
@@ -0,0 +1,9 @@
+((w) => {
+  w.ButtonMixins = {
+    computed: {
+      namespace: function () {
+        return `${this.namespacePath}/${this.projectPath}`;
+      }
+    }
+  };
+})(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..488714e4870acff069d9fdd393645e95883967b7
--- /dev/null
+++ b/app/assets/javascripts/diff_notes/models/discussion.js.es6
@@ -0,0 +1,87 @@
+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..f2d2d389c38cc9489ee4ba0a069e4a73d312072e
--- /dev/null
+++ b/app/assets/javascripts/diff_notes/models/note.js.es6
@@ -0,0 +1,9 @@
+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..de771ff814beae43c11066fd3e17620563493359
--- /dev/null
+++ b/app/assets/javascripts/diff_notes/services/resolve.js.es6
@@ -0,0 +1,88 @@
+((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(namespace) {
+      this.setCSRF();
+      Vue.http.options.root = `/${namespace}`;
+    }
+
+    resolve(namespace, noteId) {
+      this.prepareRequest(namespace);
+
+      return this.noteResource.save({ noteId }, {});
+    }
+
+    unresolve(namespace, noteId) {
+      this.prepareRequest(namespace);
+
+      return this.noteResource.delete({ noteId }, {});
+    }
+
+    toggleResolveForDiscussion(namespace, mergeRequestId, discussionId) {
+      const discussion = CommentsStore.state[discussionId],
+            isResolved = discussion.isResolved();
+      let promise;
+
+      if (isResolved) {
+        promise = this.unResolveAll(namespace, mergeRequestId, discussionId);
+      } else {
+        promise = this.resolveAll(namespace, 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(namespace, mergeRequestId, discussionId) {
+      const discussion = CommentsStore.state[discussionId];
+
+      this.prepareRequest(namespace);
+
+      discussion.loading = true;
+
+      return this.discussionResource.save({
+        mergeRequestId,
+        discussionId
+      }, {});
+    }
+
+    unResolveAll(namespace, mergeRequestId, discussionId) {
+      const discussion = CommentsStore.state[discussionId];
+
+      this.prepareRequest(namespace);
+
+      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..69522e1dac51d881735acec8891bb5f5c2469e9b
--- /dev/null
+++ b/app/assets/javascripts/diff_notes/stores/comments.js.es6
@@ -0,0 +1,53 @@
+((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
index 9e6901962c6b3354581bd38491048c64bc70331a..ba64d2bcf0ba6908091a5aa734bf47bc2e74ef38 100644
--- a/app/assets/javascripts/dispatcher.js
+++ b/app/assets/javascripts/dispatcher.js
@@ -20,6 +20,9 @@
       path = page.split(':');
       shortcut_handler = null;
       switch (page) {
+        case 'projects:boards:show':
+          shortcut_handler = new ShortcutsNavigation();
+          break;
         case 'projects:issues:index':
           Issuable.init();
           new IssuableBulkActions();
@@ -55,6 +58,7 @@
           shortcut_handler = new ShortcutsNavigation();
           new GLForm($('.issue-form'));
           new IssuableForm($('.issue-form'));
+          new IssuableTemplateSelectors();
           break;
         case 'projects:merge_requests:new':
         case 'projects:merge_requests:edit':
@@ -62,6 +66,7 @@
           shortcut_handler = new ShortcutsNavigation();
           new GLForm($('.merge-request-form'));
           new IssuableForm($('.merge-request-form'));
+          new IssuableTemplateSelectors();
           break;
         case 'projects:tags:new':
           new ZenMode();
@@ -86,6 +91,8 @@
           new ZenMode();
           new MergedButtons();
           break;
+        case "projects:merge_requests:conflicts":
+          window.mcui = new MergeConflictResolver()
         case 'projects:merge_requests:index':
           shortcut_handler = new ShortcutsNavigation();
           Issuable.init();
@@ -122,10 +129,12 @@
           new NotificationsDropdown();
           break;
         case 'groups:group_members:index':
+          new gl.MemberExpirationDate();
           new GroupMembers();
           new UsersSelect();
           break;
         case 'projects:project_members:index':
+          new gl.MemberExpirationDate();
           new ProjectMembers();
           new UsersSelect();
           break;
@@ -167,14 +176,15 @@
           new BuildArtifacts();
           break;
         case 'projects:group_links:index':
+          new gl.MemberExpirationDate();
           new GroupsSelect();
           break;
         case 'search:show':
           new Search();
           break;
         case 'projects:protected_branches:index':
-          new ProtectedBranchesAccessSelect($(".new_protected_branch"), false, true);
-          new ProtectedBranchesAccessSelect($(".protected-branches-list"), true, false);
+          new gl.ProtectedBranchCreate();
+          new gl.ProtectedBranchEditList();
           break;
       }
       switch (path.first()) {
@@ -186,6 +196,15 @@
               break;
             case 'projects':
               new NamespaceSelects();
+              break;
+            case 'labels':
+              switch (path[2]) {
+                case 'edit':
+                  new Labels();
+              }
+            case 'abuse_reports':
+              new gl.AbuseReports();
+              break;
           }
           break;
         case 'dashboard':
@@ -211,6 +230,7 @@
               new ProjectNew();
               break;
             case 'show':
+              new Star();
               new ProjectNew();
               new ProjectShow();
               new NotificationsDropdown();
diff --git a/app/assets/javascripts/dropzone_input.js b/app/assets/javascripts/dropzone_input.js
index 288cce04f878284377e68dd0320226d9bba46509..4a6fea929c758b55d5fa382d868e6982710258d1 100644
--- a/app/assets/javascripts/dropzone_input.js
+++ b/app/assets/javascripts/dropzone_input.js
@@ -1,5 +1,5 @@
 
-/*= require markdown_preview */
+/*= require preview_markdown */
 
 (function() {
   this.DropzoneInput = (function() {
diff --git a/app/assets/javascripts/files_comment_button.js b/app/assets/javascripts/files_comment_button.js
index 09b5eb398d4431ca2890a6e8f21b0a30c2dee997..3fb3b1a8b513a93983accbb5cf9021964e2d31c6 100644
--- a/app/assets/javascripts/files_comment_button.js
+++ b/app/assets/javascripts/files_comment_button.js
@@ -33,18 +33,19 @@
       this.render = bind(this.render, this);
       this.VIEW_TYPE = $('input#view[type=hidden]').val();
       debounce = _.debounce(this.render, DEBOUNCE_TIMEOUT_DURATION);
-      $(document).off('mouseover', LINE_COLUMN_CLASSES).off('mouseleave', LINE_COLUMN_CLASSES).on('mouseover', LINE_COLUMN_CLASSES, debounce).on('mouseleave', LINE_COLUMN_CLASSES, this.destroy);
+      $(this.filesContainerElement).off('mouseover', LINE_COLUMN_CLASSES).off('mouseleave', LINE_COLUMN_CLASSES).on('mouseover', LINE_COLUMN_CLASSES, debounce).on('mouseleave', LINE_COLUMN_CLASSES, this.destroy);
     }
 
     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 +120,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/gfm_auto_complete.js b/app/assets/javascripts/gfm_auto_complete.js.es6
similarity index 77%
rename from app/assets/javascripts/gfm_auto_complete.js
rename to app/assets/javascripts/gfm_auto_complete.js.es6
index 41f4c1914f21ecb7f950d2b8bce76f121239ca7d..3dca06d36b1f9115f2352d515d7b4ff50301c001 100644
--- a/app/assets/javascripts/gfm_auto_complete.js
+++ b/app/assets/javascripts/gfm_auto_complete.js.es6
@@ -47,8 +47,8 @@
         }
       }
     },
-    setup: function(wrap) {
-      this.input = $('.js-gfm-input');
+    setup: function(input) {
+      this.input = input || $('.js-gfm-input');
       this.destroyAtWho();
       this.setupAtWho();
       if (this.dataSource) {
@@ -223,7 +223,7 @@
           }
         }
       });
-      return this.input.atwho({
+      this.input.atwho({
         at: '~',
         alias: 'labels',
         searchKey: 'search',
@@ -249,6 +249,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');
@@ -265,6 +327,7 @@
       this.input.atwho('load', 'mergerequests', data.mergerequests);
       this.input.atwho('load', ':', data.emojis);
       this.input.atwho('load', '~', data.labels);
+      this.input.atwho('load', '/', data.commands);
       return $(':focus').trigger('keyup');
     }
   };
diff --git a/app/assets/javascripts/gl_dropdown.js b/app/assets/javascripts/gl_dropdown.js
index c5d92831fbe9364f5eb6f74f6dbe9746975dd274..0179b320a3b3672e3cbf5bd716350cbeb4b3712f 100644
--- a/app/assets/javascripts/gl_dropdown.js
+++ b/app/assets/javascripts/gl_dropdown.js
@@ -28,38 +28,42 @@
         };
       })(this));
       timeout = "";
-      this.input.on("keyup", (function(_this) {
-        return function(e) {
+      this.input
+        .on('keydown', function (e) {
+          var keyCode = e.which;
+          if (keyCode === 13 && !options.elIsInput) {
+            e.preventDefault()
+          }
+        })
+        .on('keyup', function(e) {
           var keyCode;
           keyCode = e.which;
           if (ARROW_KEY_CODES.indexOf(keyCode) >= 0) {
             return;
           }
-          if (_this.input.val() !== "" && !$inputContainer.hasClass(HAS_VALUE_CLASS)) {
+          if (this.input.val() !== "" && !$inputContainer.hasClass(HAS_VALUE_CLASS)) {
             $inputContainer.addClass(HAS_VALUE_CLASS);
-          } else if (_this.input.val() === "" && $inputContainer.hasClass(HAS_VALUE_CLASS)) {
+          } else if (this.input.val() === "" && $inputContainer.hasClass(HAS_VALUE_CLASS)) {
             $inputContainer.removeClass(HAS_VALUE_CLASS);
           }
-          if (keyCode === 13) {
+          if (keyCode === 13 && !options.elIsInput) {
             return false;
           }
-          if (_this.options.remote) {
+          if (this.options.remote) {
             clearTimeout(timeout);
             return timeout = setTimeout(function() {
-              var blur_field;
-              blur_field = _this.shouldBlur(keyCode);
-              if (blur_field && _this.filterInputBlur) {
-                _this.input.blur();
+              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);
-              });
-            }, 250);
+              return this.options.query(this.input.val(), function(data) {
+                return this.options.callback(data);
+              }.bind(this));
+            }.bind(this), 250);
           } else {
-            return _this.filter(_this.input.val());
+            return this.filter(this.input.val());
           }
-        };
-      })(this));
+        }.bind(this));
     }
 
     GitLabDropdownFilter.prototype.shouldBlur = function(keyCode) {
@@ -106,9 +110,9 @@
             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');
               }
             }
           });
@@ -174,7 +178,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";
 
@@ -186,6 +190,12 @@
 
     currentIndex = -1;
 
+    NON_SELECTABLE_CLASSES = '.divider, .separator, .dropdown-header, .dropdown-menu-empty-link, .option-hidden';
+
+    SELECTABLE_CLASSES = ".dropdown-content li:not(" + NON_SELECTABLE_CLASSES + ")";
+
+    CURSOR_SELECT_SCROLL_PADDING = 5
+
     FILTER_INPUT = '.dropdown-input .dropdown-input-field';
 
     function GitLabDropdown(el1, options) {
@@ -208,6 +218,7 @@
       if (this.options.data) {
         if (_.isObject(this.options.data) && !_.isFunction(this.options.data)) {
           this.fullData = this.options.data;
+          currentIndex = -1;
           this.parseData(this.options.data);
         } else {
           this.remote = new GitLabDropdownRemote(this.options.data, {
@@ -227,6 +238,7 @@
       }
       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,
@@ -235,7 +247,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;
               }
@@ -251,12 +263,16 @@
             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)
@@ -371,7 +387,7 @@
       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 {
@@ -382,6 +398,7 @@
 
     GitLabDropdown.prototype.opened = function() {
       var contentHtml;
+      this.resetRows();
       this.addArrowKeyEvent();
       if (this.options.setIndeterminateIds) {
         this.options.setIndeterminateIds.call(this);
@@ -404,6 +421,7 @@
 
     GitLabDropdown.prototype.hidden = function(e) {
       var $input;
+      this.resetRows();
       this.removeArrayKeyEvent();
       $input = this.dropdown.find(".dropdown-input-field");
       if (this.options.filterable) {
@@ -457,14 +475,15 @@
         return "<li class='separator'></li>";
       }
       if (data.header != null) {
-        return "<li class='dropdown-header'>" + data.header + "</li>";
+        return _.template('<li class="dropdown-header"><%- header %></li>')({ header: data.header });
       }
       if (this.options.renderRow) {
         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;
+          fieldName = typeof this.options.fieldName === 'function' ? this.options.fieldName() : this.options.fieldName;
+
           field = this.dropdown.parent().find("input[name='" + fieldName + "'][value='" + value + "']");
           if (field.length) {
             selected = true;
@@ -488,11 +507,16 @@
           text = this.highlightTextMatches(text, this.filterInput.val());
         }
         if (group) {
-          groupAttrs = "data-group='" + group + "' data-index='" + index + "'";
+          groupAttrs = 'data-group=' + group + ' data-index=' + index;
         } else {
           groupAttrs = '';
         }
-        html = "<li> <a href='" + url + "' " + groupAttrs + " class='" + cssClass + "'> " + text + " </a> </li>";
+        html = _.template('<li><a href="<%- url %>" <%- groupAttrs %> class="<%- cssClass %>"><%= text %></a></li>')({
+          url: url,
+          groupAttrs: groupAttrs,
+          cssClass: cssClass,
+          text: text
+        });
       }
       return html;
     };
@@ -514,20 +538,8 @@
       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;
       isInput = $(this.el).is('input');
       if (this.renderedData) {
         groupName = el.data('group');
@@ -539,6 +551,7 @@
           selectedObject = this.renderedData[selectedIndex];
         }
       }
+      fieldName = typeof this.options.fieldName === 'function' ? this.options.fieldName(selectedObject) : this.options.fieldName;
       value = this.options.id ? this.options.id(selectedObject, el) : selectedObject.id;
       if (isInput) {
         field = $(this.el);
@@ -553,10 +566,9 @@
           field.remove();
         }
         if (this.options.toggleLabel) {
-          return this.updateLabel(selectedObject, el, this);
-        } else {
-          return selectedObject;
+          this.updateLabel(selectedObject, el, this);
         }
+        return selectedObject;
       } else if (el.hasClass(INDETERMINATE_CLASS)) {
         el.addClass(ACTIVE_CLASS);
         el.removeClass(INDETERMINATE_CLASS);
@@ -564,7 +576,7 @@
           field.remove();
         }
         if (!field.length && fieldName) {
-          this.addInput(fieldName, value);
+          this.addInput(fieldName, value, selectedObject);
         }
         return selectedObject;
       } else {
@@ -583,7 +595,7 @@
         }
         if (value != null) {
           if (!field.length && fieldName) {
-            this.addInput(fieldName, value);
+            this.addInput(fieldName, value, selectedObject);
           } else {
             field.val(value).trigger('change');
           }
@@ -592,26 +604,37 @@
       }
     };
 
-    GitLabDropdown.prototype.addInput = function(fieldName, value) {
+    GitLabDropdown.prototype.addInput = function(fieldName, value, selectedObject) {
       var $input;
       $input = $('<input>').attr('type', 'hidden').attr('name', fieldName).val(value);
       if (this.options.inputId != null) {
         $input.attr('id', this.options.inputId);
       }
+      if (selectedObject && selectedObject.type) {
+        $input.attr('data-type', selectedObject.type);
+      }
       return this.dropdown.before($input);
     };
 
-    GitLabDropdown.prototype.selectRowAtIndex = function(e, index) {
+    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;
       }
       $el = $(selector, this.dropdown);
       if ($el.length) {
-        e.preventDefault();
-        e.stopImmediatePropagation();
-        return $el.first().trigger('click');
+        var href = $el.attr('href');
+        if (href && href !== '#') {
+          Turbolinks.visit(href);
+        } else {
+          $el.first().trigger('click');
+        }
       }
     };
 
@@ -619,7 +642,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)';
+      selector = SELECTABLE_CLASSES;
       if (this.dropdown.find(".dropdown-toggle-page").length) {
         selector = ".dropdown-page-one " + selector;
       }
@@ -647,7 +670,7 @@
             return false;
           }
           if (currentKeyCode === 13 && currentIndex !== -1) {
-            return _this.selectRowAtIndex(e, currentIndex);
+            _this.selectRowAtIndex();
           }
         };
       })(this));
@@ -657,6 +680,11 @@
       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;
       $('.is-focused', this.dropdown).removeClass('is-focused');
@@ -670,10 +698,14 @@
       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) {
+        $dropdownContent.scrollTop(0)
+      } else if (index === ($listItems.length - 1)) {
+        $dropdownContent.scrollTop($dropdownContent.prop('scrollHeight'));
+      } else if (listItemBottom > (dropdownContentBottom + dropdownScrollTop)) {
+        $dropdownContent.scrollTop(listItemBottom - dropdownContentBottom + CURSOR_SELECT_SCROLL_PADDING);
+      } else if (listItemTop < (dropdownContentTop + dropdownScrollTop)) {
+        return $dropdownContent.scrollTop(listItemTop - dropdownContentTop - CURSOR_SELECT_SCROLL_PADDING);
       }
     };
 
diff --git a/app/assets/javascripts/gl_form.js b/app/assets/javascripts/gl_form.js
index 6ac7564a848c3cfc9a0ac5c104cefd6688e94b90..528a673eb15c07a341806fd35d1ca9452a953b52 100644
--- a/app/assets/javascripts/gl_form.js
+++ b/app/assets/javascripts/gl_form.js
@@ -21,7 +21,7 @@
         this.form.find('.div-dropzone').remove();
         this.form.addClass('gfm-form');
         disableButtonIfEmptyField(this.form.find('.js-note-text'), this.form.find('.js-comment-button'));
-        GitLab.GfmAutoComplete.setup();
+        GitLab.GfmAutoComplete.setup(this.form.find('.js-gfm-input'));
         new DropzoneInput(this.form);
         autosize(this.textarea);
         this.addEventListeners();
diff --git a/app/assets/javascripts/issuable.js b/app/assets/javascripts/issuable.js
index f27f1bad1f7c2a35ed7c7b79261b024c9349af2d..d0305c6c6a1194e60452e393967c890c9825a0b2 100644
--- a/app/assets/javascripts/issuable.js
+++ b/app/assets/javascripts/issuable.js
@@ -5,13 +5,10 @@
 
   this.Issuable = {
     init: function() {
-      if (!issuable_created) {
-        issuable_created = true;
-        Issuable.initTemplates();
-        Issuable.initSearch();
-        Issuable.initChecks();
-        return Issuable.initLabelFilterRemove();
-      }
+      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> <% }); %>');
diff --git a/app/assets/javascripts/issuable_form.js b/app/assets/javascripts/issuable_form.js
index 297d4f029f0e50c541b8474f850c74f94ef271fb..b7f92ae98832a23684ab6bd61e938c11b8a811f3 100644
--- a/app/assets/javascripts/issuable_form.js
+++ b/app/assets/javascripts/issuable_form.js
@@ -102,20 +102,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/labels_select.js b/app/assets/javascripts/labels_select.js
index 675dd5b7ceaa45ff080e420152bbc1ef6200e6df..565dbeacdb3627bf8b5d98edba3bdf7374367f85 100644
--- a/app/assets/javascripts/labels_select.js
+++ b/app/assets/javascripts/labels_select.js
@@ -4,7 +4,7 @@
       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, projectId, saveLabelData, selectedLabel, showAny, showNo, $sidebarLabelTooltip;
         $dropdown = $(dropdown);
         projectId = $dropdown.data('project-id');
         labelUrl = $dropdown.data('labels');
@@ -13,8 +13,6 @@
         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');
         defaultLabel = $dropdown.data('default-label');
@@ -23,11 +21,8 @@
         $block = $selectbox.closest('.block');
         $form = $dropdown.closest('form');
         $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();
         if (issueUpdateURL != null) {
           issueURLSplit = issueUpdateURL.split('/');
@@ -36,60 +31,13 @@
           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) {
-              var errors;
-              $newLabelCreateButton.enable();
-              if (label.message != null) {
-                errors = _.map(label.message, function(value, key) {
-                  return key + " " + value[0];
-                });
-                return $newLabelError.html(errors.join("<br/>")).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'), projectId);
         }
+
         saveLabelData = function() {
           var data, selected;
           selected = $dropdown.closest('.selectbox').find("input[name='" + ($dropdown.data('field-name')) + "']").map(function() {
@@ -109,7 +57,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();
@@ -123,6 +71,27 @@
             }
             $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'
             });
@@ -270,6 +239,9 @@
             isMRIndex = page === 'projects:merge_requests:index';
             $selectbox.hide();
             $value.removeAttr('style');
+            if (page === 'projects:boards:show') {
+              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')) + "']");
@@ -289,7 +261,7 @@
             }
           },
           multiSelect: $dropdown.hasClass('js-multiselect'),
-          clicked: function(label) {
+          clicked: function(label, $el, e) {
             var isIssueIndex, isMRIndex, page;
             _this.enableBulkLabelDropdown();
             if ($dropdown.hasClass('js-filter-bulk-update')) {
@@ -298,7 +270,23 @@
             page = $('body').data('page');
             isIssueIndex = page === 'projects:issues:index';
             isMRIndex = page === 'projects:merge_requests:index';
-            if ($dropdown.hasClass('js-filter-submit') && (isIssueIndex || isMRIndex)) {
+            if (page === 'projects:boards:show') {
+              if (label.isAny) {
+                gl.issueBoards.BoardsStore.state.filters['label_name'] = [];
+              } else if (label.title) {
+                gl.issueBoards.BoardsStore.state.filters['label_name'].push(label.title);
+              } else {
+                var filters = gl.issueBoards.BoardsStore.state.filters['label_name'];
+                filters = filters.filter(function (label) {
+                  return label !== $el.text().trim();
+                });
+                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'));
diff --git a/app/assets/javascripts/lib/ace.js b/app/assets/javascripts/lib/ace.js
new file mode 100644
index 0000000000000000000000000000000000000000..4cdf99cae72578e74b28e564563a5db555039fa8
--- /dev/null
+++ b/app/assets/javascripts/lib/ace.js
@@ -0,0 +1,2 @@
+/*= require ace-rails-ap */
+/*= require ace/ext-searchbox */
diff --git a/app/assets/javascripts/lib/utils/datetime_utility.js b/app/assets/javascripts/lib/utils/datetime_utility.js
index e817261f2103ae6b07ed650ac972e5d3a8aaefe8..10afa7e432985da0f7eeb3d721e77161ec98b3bd 100644
--- a/app/assets/javascripts/lib/utils/datetime_utility.js
+++ b/app/assets/javascripts/lib/utils/datetime_utility.js
@@ -8,13 +8,16 @@
       base.utils = {};
     }
     w.gl.utils.days = ['Sunday', 'Monday', 'Tuesday', 'Wednesday', 'Thursday', 'Friday', 'Saturday'];
+
     w.gl.utils.formatDate = function(datetime) {
       return dateFormat(datetime, 'mmm d, yyyy h:MMtt Z');
     };
+
     w.gl.utils.getDayName = function(date) {
       return this.days[date.getDay()];
     };
-    return w.gl.utils.localTimeAgo = function($timeagoEls, setTimeago) {
+
+    w.gl.utils.localTimeAgo = function($timeagoEls, setTimeago) {
       if (setTimeago == null) {
         setTimeago = true;
       }
@@ -31,6 +34,39 @@
         });
       }
     };
+
+    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: []
+      };
+      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;
+    };
+
   })(window);
 
 }).call(this);
diff --git a/app/assets/javascripts/lib/utils/md5.js b/app/assets/javascripts/lib/utils/md5.js
deleted file mode 100644
index b63716eaad2f8f90678b99ccac2eb7f226214847..0000000000000000000000000000000000000000
--- a/app/assets/javascripts/lib/utils/md5.js
+++ /dev/null
@@ -1,211 +0,0 @@
-function md5 (str) {
-  // http://kevin.vanzonneveld.net
-  // +   original by: Webtoolkit.info (http://www.webtoolkit.info/)
-  // + namespaced by: Michael White (http://getsprink.com)
-  // +    tweaked by: Jack
-  // +   improved by: Kevin van Zonneveld (http://kevin.vanzonneveld.net)
-  // +      input by: Brett Zamir (http://brett-zamir.me)
-  // +   bugfixed by: Kevin van Zonneveld (http://kevin.vanzonneveld.net)
-  // -    depends on: utf8_encode
-  // *     example 1: md5('Kevin van Zonneveld');
-  // *     returns 1: '6e658d4bfcb59cc13f96c14450ac40b9'
-  var xl;
-
-  var rotateLeft = function (lValue, iShiftBits) {
-    return (lValue << iShiftBits) | (lValue >>> (32 - iShiftBits));
-  };
-
-  var addUnsigned = function (lX, lY) {
-    var lX4, lY4, lX8, lY8, lResult;
-    lX8 = (lX & 0x80000000);
-    lY8 = (lY & 0x80000000);
-    lX4 = (lX & 0x40000000);
-    lY4 = (lY & 0x40000000);
-    lResult = (lX & 0x3FFFFFFF) + (lY & 0x3FFFFFFF);
-    if (lX4 & lY4) {
-      return (lResult ^ 0x80000000 ^ lX8 ^ lY8);
-    }
-    if (lX4 | lY4) {
-      if (lResult & 0x40000000) {
-        return (lResult ^ 0xC0000000 ^ lX8 ^ lY8);
-      } else {
-        return (lResult ^ 0x40000000 ^ lX8 ^ lY8);
-      }
-    } else {
-      return (lResult ^ lX8 ^ lY8);
-    }
-  };
-
-  var _F = function (x, y, z) {
-    return (x & y) | ((~x) & z);
-  };
-  var _G = function (x, y, z) {
-    return (x & z) | (y & (~z));
-  };
-  var _H = function (x, y, z) {
-    return (x ^ y ^ z);
-  };
-  var _I = function (x, y, z) {
-    return (y ^ (x | (~z)));
-  };
-
-  var _FF = function (a, b, c, d, x, s, ac) {
-    a = addUnsigned(a, addUnsigned(addUnsigned(_F(b, c, d), x), ac));
-    return addUnsigned(rotateLeft(a, s), b);
-  };
-
-  var _GG = function (a, b, c, d, x, s, ac) {
-    a = addUnsigned(a, addUnsigned(addUnsigned(_G(b, c, d), x), ac));
-    return addUnsigned(rotateLeft(a, s), b);
-  };
-
-  var _HH = function (a, b, c, d, x, s, ac) {
-    a = addUnsigned(a, addUnsigned(addUnsigned(_H(b, c, d), x), ac));
-    return addUnsigned(rotateLeft(a, s), b);
-  };
-
-  var _II = function (a, b, c, d, x, s, ac) {
-    a = addUnsigned(a, addUnsigned(addUnsigned(_I(b, c, d), x), ac));
-    return addUnsigned(rotateLeft(a, s), b);
-  };
-
-  var convertToWordArray = function (str) {
-    var lWordCount;
-    var lMessageLength = str.length;
-    var lNumberOfWords_temp1 = lMessageLength + 8;
-    var lNumberOfWords_temp2 = (lNumberOfWords_temp1 - (lNumberOfWords_temp1 % 64)) / 64;
-    var lNumberOfWords = (lNumberOfWords_temp2 + 1) * 16;
-    var lWordArray = new Array(lNumberOfWords - 1);
-    var lBytePosition = 0;
-    var lByteCount = 0;
-    while (lByteCount < lMessageLength) {
-      lWordCount = (lByteCount - (lByteCount % 4)) / 4;
-      lBytePosition = (lByteCount % 4) * 8;
-      lWordArray[lWordCount] = (lWordArray[lWordCount] | (str.charCodeAt(lByteCount) << lBytePosition));
-      lByteCount++;
-    }
-    lWordCount = (lByteCount - (lByteCount % 4)) / 4;
-    lBytePosition = (lByteCount % 4) * 8;
-    lWordArray[lWordCount] = lWordArray[lWordCount] | (0x80 << lBytePosition);
-    lWordArray[lNumberOfWords - 2] = lMessageLength << 3;
-    lWordArray[lNumberOfWords - 1] = lMessageLength >>> 29;
-    return lWordArray;
-  };
-
-  var wordToHex = function (lValue) {
-    var wordToHexValue = "",
-      wordToHexValue_temp = "",
-      lByte, lCount;
-    for (lCount = 0; lCount <= 3; lCount++) {
-      lByte = (lValue >>> (lCount * 8)) & 255;
-      wordToHexValue_temp = "0" + lByte.toString(16);
-      wordToHexValue = wordToHexValue + wordToHexValue_temp.substr(wordToHexValue_temp.length - 2, 2);
-    }
-    return wordToHexValue;
-  };
-
-  var x = [],
-    k, AA, BB, CC, DD, a, b, c, d, S11 = 7,
-    S12 = 12,
-    S13 = 17,
-    S14 = 22,
-    S21 = 5,
-    S22 = 9,
-    S23 = 14,
-    S24 = 20,
-    S31 = 4,
-    S32 = 11,
-    S33 = 16,
-    S34 = 23,
-    S41 = 6,
-    S42 = 10,
-    S43 = 15,
-    S44 = 21;
-
-  str = this.utf8_encode(str);
-  x = convertToWordArray(str);
-  a = 0x67452301;
-  b = 0xEFCDAB89;
-  c = 0x98BADCFE;
-  d = 0x10325476;
-
-  xl = x.length;
-  for (k = 0; k < xl; k += 16) {
-    AA = a;
-    BB = b;
-    CC = c;
-    DD = d;
-    a = _FF(a, b, c, d, x[k + 0], S11, 0xD76AA478);
-    d = _FF(d, a, b, c, x[k + 1], S12, 0xE8C7B756);
-    c = _FF(c, d, a, b, x[k + 2], S13, 0x242070DB);
-    b = _FF(b, c, d, a, x[k + 3], S14, 0xC1BDCEEE);
-    a = _FF(a, b, c, d, x[k + 4], S11, 0xF57C0FAF);
-    d = _FF(d, a, b, c, x[k + 5], S12, 0x4787C62A);
-    c = _FF(c, d, a, b, x[k + 6], S13, 0xA8304613);
-    b = _FF(b, c, d, a, x[k + 7], S14, 0xFD469501);
-    a = _FF(a, b, c, d, x[k + 8], S11, 0x698098D8);
-    d = _FF(d, a, b, c, x[k + 9], S12, 0x8B44F7AF);
-    c = _FF(c, d, a, b, x[k + 10], S13, 0xFFFF5BB1);
-    b = _FF(b, c, d, a, x[k + 11], S14, 0x895CD7BE);
-    a = _FF(a, b, c, d, x[k + 12], S11, 0x6B901122);
-    d = _FF(d, a, b, c, x[k + 13], S12, 0xFD987193);
-    c = _FF(c, d, a, b, x[k + 14], S13, 0xA679438E);
-    b = _FF(b, c, d, a, x[k + 15], S14, 0x49B40821);
-    a = _GG(a, b, c, d, x[k + 1], S21, 0xF61E2562);
-    d = _GG(d, a, b, c, x[k + 6], S22, 0xC040B340);
-    c = _GG(c, d, a, b, x[k + 11], S23, 0x265E5A51);
-    b = _GG(b, c, d, a, x[k + 0], S24, 0xE9B6C7AA);
-    a = _GG(a, b, c, d, x[k + 5], S21, 0xD62F105D);
-    d = _GG(d, a, b, c, x[k + 10], S22, 0x2441453);
-    c = _GG(c, d, a, b, x[k + 15], S23, 0xD8A1E681);
-    b = _GG(b, c, d, a, x[k + 4], S24, 0xE7D3FBC8);
-    a = _GG(a, b, c, d, x[k + 9], S21, 0x21E1CDE6);
-    d = _GG(d, a, b, c, x[k + 14], S22, 0xC33707D6);
-    c = _GG(c, d, a, b, x[k + 3], S23, 0xF4D50D87);
-    b = _GG(b, c, d, a, x[k + 8], S24, 0x455A14ED);
-    a = _GG(a, b, c, d, x[k + 13], S21, 0xA9E3E905);
-    d = _GG(d, a, b, c, x[k + 2], S22, 0xFCEFA3F8);
-    c = _GG(c, d, a, b, x[k + 7], S23, 0x676F02D9);
-    b = _GG(b, c, d, a, x[k + 12], S24, 0x8D2A4C8A);
-    a = _HH(a, b, c, d, x[k + 5], S31, 0xFFFA3942);
-    d = _HH(d, a, b, c, x[k + 8], S32, 0x8771F681);
-    c = _HH(c, d, a, b, x[k + 11], S33, 0x6D9D6122);
-    b = _HH(b, c, d, a, x[k + 14], S34, 0xFDE5380C);
-    a = _HH(a, b, c, d, x[k + 1], S31, 0xA4BEEA44);
-    d = _HH(d, a, b, c, x[k + 4], S32, 0x4BDECFA9);
-    c = _HH(c, d, a, b, x[k + 7], S33, 0xF6BB4B60);
-    b = _HH(b, c, d, a, x[k + 10], S34, 0xBEBFBC70);
-    a = _HH(a, b, c, d, x[k + 13], S31, 0x289B7EC6);
-    d = _HH(d, a, b, c, x[k + 0], S32, 0xEAA127FA);
-    c = _HH(c, d, a, b, x[k + 3], S33, 0xD4EF3085);
-    b = _HH(b, c, d, a, x[k + 6], S34, 0x4881D05);
-    a = _HH(a, b, c, d, x[k + 9], S31, 0xD9D4D039);
-    d = _HH(d, a, b, c, x[k + 12], S32, 0xE6DB99E5);
-    c = _HH(c, d, a, b, x[k + 15], S33, 0x1FA27CF8);
-    b = _HH(b, c, d, a, x[k + 2], S34, 0xC4AC5665);
-    a = _II(a, b, c, d, x[k + 0], S41, 0xF4292244);
-    d = _II(d, a, b, c, x[k + 7], S42, 0x432AFF97);
-    c = _II(c, d, a, b, x[k + 14], S43, 0xAB9423A7);
-    b = _II(b, c, d, a, x[k + 5], S44, 0xFC93A039);
-    a = _II(a, b, c, d, x[k + 12], S41, 0x655B59C3);
-    d = _II(d, a, b, c, x[k + 3], S42, 0x8F0CCC92);
-    c = _II(c, d, a, b, x[k + 10], S43, 0xFFEFF47D);
-    b = _II(b, c, d, a, x[k + 1], S44, 0x85845DD1);
-    a = _II(a, b, c, d, x[k + 8], S41, 0x6FA87E4F);
-    d = _II(d, a, b, c, x[k + 15], S42, 0xFE2CE6E0);
-    c = _II(c, d, a, b, x[k + 6], S43, 0xA3014314);
-    b = _II(b, c, d, a, x[k + 13], S44, 0x4E0811A1);
-    a = _II(a, b, c, d, x[k + 4], S41, 0xF7537E82);
-    d = _II(d, a, b, c, x[k + 11], S42, 0xBD3AF235);
-    c = _II(c, d, a, b, x[k + 2], S43, 0x2AD7D2BB);
-    b = _II(b, c, d, a, x[k + 9], S44, 0xEB86D391);
-    a = addUnsigned(a, AA);
-    b = addUnsigned(b, BB);
-    c = addUnsigned(c, CC);
-    d = addUnsigned(d, DD);
-  }
-
-  var temp = wordToHex(a) + wordToHex(b) + wordToHex(c) + wordToHex(d);
-
-  return temp.toLowerCase();
-}
diff --git a/app/assets/javascripts/lib/utils/text_utility.js b/app/assets/javascripts/lib/utils/text_utility.js
index 130479642f32deb27ed79e278bce1df8465cbfcb..b6636de57670329772e56601c6ef9d73a916e18d 100644
--- a/app/assets/javascripts/lib/utils/text_utility.js
+++ b/app/assets/javascripts/lib/utils/text_utility.js
@@ -104,9 +104,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/utf8_encode.js b/app/assets/javascripts/lib/utils/utf8_encode.js
deleted file mode 100644
index 39ffe44dae0ef78c6e22dbe5f0d60bb3265c94af..0000000000000000000000000000000000000000
--- a/app/assets/javascripts/lib/utils/utf8_encode.js
+++ /dev/null
@@ -1,70 +0,0 @@
-function utf8_encode (argString) {
-  // http://kevin.vanzonneveld.net
-  // +   original by: Webtoolkit.info (http://www.webtoolkit.info/)
-  // +   improved by: Kevin van Zonneveld (http://kevin.vanzonneveld.net)
-  // +   improved by: sowberry
-  // +    tweaked by: Jack
-  // +   bugfixed by: Onno Marsman
-  // +   improved by: Yves Sucaet
-  // +   bugfixed by: Onno Marsman
-  // +   bugfixed by: Ulrich
-  // +   bugfixed by: Rafal Kukawski
-  // +   improved by: kirilloid
-  // +   bugfixed by: kirilloid
-  // *     example 1: utf8_encode('Kevin van Zonneveld');
-  // *     returns 1: 'Kevin van Zonneveld'
-
-  if (argString === null || typeof argString === "undefined") {
-    return "";
-  }
-
-  var string = (argString + ''); // .replace(/\r\n/g, "\n").replace(/\r/g, "\n");
-  var utftext = '',
-    start, end, stringl = 0;
-
-  start = end = 0;
-  stringl = string.length;
-  for (var n = 0; n < stringl; n++) {
-    var c1 = string.charCodeAt(n);
-    var enc = null;
-
-    if (c1 < 128) {
-      end++;
-    } else if (c1 > 127 && c1 < 2048) {
-      enc = String.fromCharCode(
-         (c1 >> 6)        | 192,
-        ( c1        & 63) | 128
-      );
-    } else if (c1 & 0xF800 != 0xD800) {
-      enc = String.fromCharCode(
-         (c1 >> 12)       | 224,
-        ((c1 >> 6)  & 63) | 128,
-        ( c1        & 63) | 128
-      );
-    } else { // surrogate pairs
-      if (c1 & 0xFC00 != 0xD800) { throw new RangeError("Unmatched trail surrogate at " + n); }
-      var c2 = string.charCodeAt(++n);
-      if (c2 & 0xFC00 != 0xDC00) { throw new RangeError("Unmatched lead surrogate at " + (n-1)); }
-      c1 = ((c1 & 0x3FF) << 10) + (c2 & 0x3FF) + 0x10000;
-      enc = String.fromCharCode(
-         (c1 >> 18)       | 240,
-        ((c1 >> 12) & 63) | 128,
-        ((c1 >> 6)  & 63) | 128,
-        ( c1        & 63) | 128
-      );
-    }
-    if (enc !== null) {
-      if (end > start) {
-        utftext += string.slice(start, end);
-      }
-      utftext += enc;
-      start = end = n + 1;
-    }
-  }
-
-  if (end > start) {
-    utftext += string.slice(start, stringl);
-  }
-
-  return utftext;
-}
diff --git a/app/assets/javascripts/member_expiration_date.js b/app/assets/javascripts/member_expiration_date.js
new file mode 100644
index 0000000000000000000000000000000000000000..1935af491f713d3c3d613471285b4068eb11cd8d
--- /dev/null
+++ b/app/assets/javascripts/member_expiration_date.js
@@ -0,0 +1,32 @@
+(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: toggleClearInput
+    });
+
+    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);
+      toggleClearInput.call(input);
+    });
+
+    inputs.on('blur', toggleClearInput);
+
+    inputs.each(toggleClearInput);
+  };
+}).call(this);
diff --git a/app/assets/javascripts/merge_conflict_data_provider.js.es6 b/app/assets/javascripts/merge_conflict_data_provider.js.es6
new file mode 100644
index 0000000000000000000000000000000000000000..cd92df8ddc5de268e043ea4d9f7c9410696a5ebd
--- /dev/null
+++ b/app/assets/javascripts/merge_conflict_data_provider.js.es6
@@ -0,0 +1,341 @@
+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';
+
+
+class MergeConflictDataProvider {
+
+  getInitialData() {
+    const diffViewType = $.cookie('diff_view');
+
+    return {
+      isLoading      : true,
+      hasError       : false,
+      isParallel     : diffViewType === 'parallel',
+      diffViewType   : diffViewType,
+      isSubmitting   : false,
+      conflictsData  : {},
+      resolutionData : {}
+    }
+  }
+
+
+  decorateData(vueInstance, data) {
+    this.vueInstance = vueInstance;
+
+    if (data.type === 'error') {
+      vueInstance.hasError = true;
+      data.errorMessage = data.message;
+    }
+    else {
+      data.shortCommitSha = data.commit_sha.slice(0, 7);
+      data.commitMessage  = data.commit_message;
+
+      this.setParallelLines(data);
+      this.setInlineLines(data);
+      this.updateResolutionsData(data);
+    }
+
+    vueInstance.conflictsData = data;
+    vueInstance.isSubmitting = false;
+
+    const conflictsText = this.getConflictsCount() > 1 ? 'conflicts' : 'conflict';
+    vueInstance.conflictsData.conflictsText = conflictsText;
+  }
+
+
+  updateResolutionsData(data) {
+    const vi = this.vueInstance;
+
+    data.files.forEach( (file) => {
+      file.sections.forEach( (section) => {
+        if (section.conflict) {
+          vi.$set(`resolutionData['${section.id}']`, false);
+        }
+      });
+    });
+  }
+
+
+  setParallelLines(data) {
+    data.files.forEach( (file) => {
+      file.filePath  = this.getFilePath(file);
+      file.iconClass = `fa-${file.blob_icon}`;
+      file.blobPath  = file.blob_path;
+      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]
+        ]);
+      }
+
+    });
+  }
+
+
+  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: '' });
+        }
+      }
+    }
+  }
+
+
+  setInlineLines(data) {
+    data.files.forEach( (file) => {
+      file.iconClass   = `fa-${file.blob_icon}`;
+      file.blobPath    = file.blob_path;
+      file.filePath    = this.getFilePath(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));
+        }
+      });
+    });
+  }
+
+
+  handleSelected(sectionId, selection) {
+    const vi = this.vueInstance;
+
+    vi.resolutionData[sectionId] = selection;
+    vi.conflictsData.files.forEach( (file) => {
+      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);
+        }
+      })
+    });
+  }
+
+
+  updateViewType(newType) {
+    const vi = this.vueInstance;
+
+    if (newType === vi.diffView || !(newType === 'parallel' || newType === 'inline')) {
+      return;
+    }
+
+    vi.diffView   = newType;
+    vi.isParallel = newType === 'parallel';
+    $.cookie('diff_view', newType); // TODO: Make sure that cookie path added.
+    $('.content-wrapper .container-fluid').toggleClass('container-limited');
+  }
+
+
+  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;
+    }
+  }
+
+
+  getConflictsCount() {
+    return Object.keys(this.vueInstance.resolutionData).length;
+  }
+
+
+  getResolvedCount() {
+    let  count = 0;
+    const data = this.vueInstance.resolutionData;
+
+    for (const id in data) {
+      const resolution = data[id];
+      if (resolution) {
+        count++;
+      }
+    }
+
+    return count;
+  }
+
+
+  isReadyToCommit() {
+    const { conflictsData, isSubmitting } = this.vueInstance
+    const allResolved = this.getConflictsCount() === this.getResolvedCount();
+    const hasCommitMessage = $.trim(conflictsData.commitMessage).length;
+
+    return !isSubmitting && hasCommitMessage && allResolved;
+  }
+
+
+  getCommitButtonText() {
+    const initial = 'Commit conflict resolution';
+    const inProgress = 'Committing...';
+    const vue = this.vueInstance;
+
+    return vue ? vue.isSubmitting ? inProgress : initial : initial;
+  }
+
+
+  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
+    }
+  }
+
+
+  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
+    }
+  }
+
+
+  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
+    }
+  }
+
+
+  handleFailedRequest(vueInstance, data) {
+    vueInstance.hasError = true;
+    vueInstance.conflictsData.errorMessage = 'Something went wrong!';
+  }
+
+
+  getCommitData() {
+    return {
+      commit_message: this.vueInstance.conflictsData.commitMessage,
+      sections: this.vueInstance.resolutionData
+    }
+  }
+
+
+  getFilePath(file) {
+    const { old_path, new_path } = file;
+    return old_path === new_path ? new_path : `${old_path} → ${new_path}`;
+  }
+
+}
diff --git a/app/assets/javascripts/merge_conflict_resolver.js.es6 b/app/assets/javascripts/merge_conflict_resolver.js.es6
new file mode 100644
index 0000000000000000000000000000000000000000..77bffbcb403be6e4835ad33121ae1b81241f3084
--- /dev/null
+++ b/app/assets/javascripts/merge_conflict_resolver.js.es6
@@ -0,0 +1,85 @@
+//= require vue
+
+class MergeConflictResolver {
+
+  constructor() {
+    this.dataProvider = new MergeConflictDataProvider()
+    this.initVue()
+  }
+
+
+  initVue() {
+    const that = this;
+    this.vue   = new Vue({
+      el       : '#conflicts',
+      name     : 'MergeConflictResolver',
+      data     : this.dataProvider.getInitialData(),
+      created  : this.fetchData(),
+      computed : this.setComputedProperties(),
+      methods  : {
+        handleSelected(sectionId, selection) {
+          that.dataProvider.handleSelected(sectionId, selection);
+        },
+        handleViewTypeChange(newType) {
+          that.dataProvider.updateViewType(newType);
+        },
+        commit() {
+          that.commit();
+        }
+      }
+    })
+  }
+
+
+  setComputedProperties() {
+    const dp = this.dataProvider;
+
+    return {
+      conflictsCount() { return dp.getConflictsCount() },
+      resolvedCount() { return dp.getResolvedCount() },
+      readyToCommit() { return dp.isReadyToCommit() },
+      commitButtonText() { return dp.getCommitButtonText() }
+    }
+  }
+
+
+  fetchData() {
+    const dp = this.dataProvider;
+
+    $.get($('#conflicts').data('conflictsPath'))
+      .done((data) => {
+        dp.decorateData(this.vue, data);
+      })
+      .error((data) => {
+        dp.handleFailedRequest(this.vue, data);
+      })
+      .always(() => {
+        this.vue.isLoading = false;
+
+        this.vue.$nextTick(() => {
+          $('#conflicts .js-syntax-highlight').syntaxHighlight();
+        });
+
+        if (this.vue.diffViewType === 'parallel') {
+          $('.content-wrapper .container-fluid').removeClass('container-limited');
+        }
+      })
+  }
+
+
+  commit() {
+    this.vue.isSubmitting = true;
+
+    $.post($('#conflicts').data('resolveConflictsPath'), this.dataProvider.getCommitData())
+      .done((data) => {
+        window.location.href = data.redirect_to;
+      })
+      .error(() => {
+        new Flash('Something went wrong!');
+      })
+      .always(() => {
+        this.vue.isSubmitting = false;
+      });
+  }
+
+}
diff --git a/app/assets/javascripts/merge_request.js b/app/assets/javascripts/merge_request.js
index 47e6dd1084d2a6a6527d49e0dedc0f46ee58c9c8..56ebf84c4f6d0484fd808a93ab090716dabc4e90 100644
--- a/app/assets/javascripts/merge_request.js
+++ b/app/assets/javascripts/merge_request.js
@@ -34,7 +34,7 @@
 
     MergeRequest.prototype.initTabs = function() {
       if (this.opts.action !== 'new') {
-        return new MergeRequestTabs(this.opts);
+        window.mrTabs = new MergeRequestTabs(this.opts);
       } else {
         return $('.merge-request-tabs a[data-toggle="tab"]:first').tab('show');
       }
diff --git a/app/assets/javascripts/merge_request_tabs.js b/app/assets/javascripts/merge_request_tabs.js
index 52c2ed6101259ca038913ee0cb46f02a8e1218a2..ad08209d61e55f598b2019c4fbc6b11da707848a 100644
--- a/app/assets/javascripts/merge_request_tabs.js
+++ b/app/assets/javascripts/merge_request_tabs.js
@@ -9,10 +9,13 @@
 
     MergeRequestTabs.prototype.buildsLoaded = false;
 
+    MergeRequestTabs.prototype.pipelinesLoaded = false;
+
     MergeRequestTabs.prototype.commitsLoaded = false;
 
     function MergeRequestTabs(opts) {
       this.opts = opts != null ? opts : {};
+      this.opts.setUrl = this.opts.setUrl !== undefined ? this.opts.setUrl : true;
       this.setCurrentAction = bind(this.setCurrentAction, this);
       this.tabShown = bind(this.tabShown, this);
       this.showTab = bind(this.showTab, this);
@@ -50,10 +53,15 @@
       } else if (action === 'builds') {
         this.loadBuilds($target.attr('href'));
         this.expandView();
+      } else if (action === 'pipelines') {
+        this.loadPipelines($target.attr('href'));
+        this.expandView();
       } else {
         this.expandView();
       }
-      return this.setCurrentAction(action);
+      if (this.opts.setUrl) {
+        this.setCurrentAction(action);
+      }
     };
 
     MergeRequestTabs.prototype.scrollToElement = function(container) {
@@ -81,7 +89,8 @@
       if (action === 'show') {
         action = 'notes';
       }
-      new_state = this._location.pathname.replace(/\/(commits|diffs|builds)(\.html)?\/?$/, '');
+      this.currentAction = action;
+      new_state = this._location.pathname.replace(/\/(commits|diffs|builds|pipelines)(\.html)?\/?$/, '');
       if (action !== 'notes') {
         new_state += "/" + action;
       }
@@ -119,6 +128,11 @@
         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();
@@ -177,6 +191,21 @@
       });
     };
 
+    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)
+      });
+    };
+
     MergeRequestTabs.prototype.toggleLoading = function(status) {
       return $('.mr-loading-status .loading').toggle(status);
     };
diff --git a/app/assets/javascripts/merge_request_widget.js b/app/assets/javascripts/merge_request_widget.js
index 362aaa906d033ce110e6c838cc7cbc14fd9d0dfd..bd35b6f679d18db622510b9a3c354b261e6b1c6d 100644
--- a/app/assets/javascripts/merge_request_widget.js
+++ b/app/assets/javascripts/merge_request_widget.js
@@ -28,7 +28,7 @@
 
     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;
@@ -53,7 +53,7 @@
           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>");
diff --git a/app/assets/javascripts/milestone_select.js b/app/assets/javascripts/milestone_select.js
index a0b65d20c03425c71954e1b6d3e968f485dd740c..e897ebdf6304bcdc337fae6d2a60c4d6f3ff779d 100644
--- a/app/assets/javascripts/milestone_select.js
+++ b/app/assets/javascripts/milestone_select.js
@@ -94,7 +94,7 @@
             $selectbox.hide();
             return $value.css('display', '');
           },
-          clicked: function(selected) {
+          clicked: function(selected, $el, e) {
             var data, isIssueIndex, isMRIndex, page;
             page = $('body').data('page');
             isIssueIndex = page === 'projects:issues:index';
@@ -102,7 +102,11 @@
             if ($dropdown.hasClass('js-filter-bulk-update')) {
               return;
             }
-            if ($dropdown.hasClass('js-filter-submit') && (isIssueIndex || isMRIndex)) {
+            if (page === 'projects:boards:show') {
+              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 {
diff --git a/app/assets/javascripts/notes.js b/app/assets/javascripts/notes.js
index 9ece474d9941ea4c8f64bcfe67d98c2da3993022..d0d5cad813a24eb9064560c019e2d20964b9e14b 100644
--- a/app/assets/javascripts/notes.js
+++ b/app/assets/javascripts/notes.js
@@ -68,6 +68,7 @@
       $(document).on("click", ".note-edit-cancel", this.cancelEdit);
       $(document).on("click", ".js-comment-button", this.updateCloseButton);
       $(document).on("keyup input", ".js-note-text", this.updateTargetButtons);
+      $(document).on('click', '.js-comment-resolve-button', this.resolveDiscussion);
       $(document).on("click", ".js-note-delete", this.removeNote);
       $(document).on("click", ".js-note-attachment-delete", this.removeAttachment);
       $(document).on("ajax:complete", ".js-main-target-form", this.reenableTargetFormSubmitButton);
@@ -100,6 +101,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');
     };
@@ -201,7 +203,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 +225,7 @@
 
     /*
     Render note in main comments area.
-    
+
     Note: for rendering inline notes use renderDiscussionNote
      */
 
@@ -231,7 +233,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;
       }
@@ -245,6 +253,7 @@
         $notesList.append(note.html).syntaxHighlight();
         gl.utils.localTimeAgo($notesList.find("#note_" + note.id + " .js-timeago"), false);
         this.initTaskList();
+        this.refresh();
         return this.updateNotesCount(1);
       }
     };
@@ -265,7 +274,7 @@
 
     /*
     Render note in discussion area.
-    
+
     Note: for rendering inline notes use renderDiscussionNote
      */
 
@@ -297,6 +306,11 @@
       } else {
         discussionContainer.append(note_html);
       }
+
+      if (typeof DiffNotesApp !== 'undefined') {
+        DiffNotesApp.compileComponents();
+      }
+
       gl.utils.localTimeAgo($('.js-timeago', note_html), false);
       return this.updateNotesCount(1);
     };
@@ -304,7 +318,7 @@
 
     /*
     Called in response the main target form has been successfully submitted.
-    
+
     Removes any errors.
     Resets text and preview.
     Resets buttons.
@@ -329,7 +343,7 @@
 
     /*
     Shows the main form and does some setup on it.
-    
+
     Sets some hidden fields in the form.
      */
 
@@ -343,13 +357,14 @@
       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 +381,7 @@
 
     /*
     Called in response to the new note form being submitted
-    
+
     Adds new note to list.
      */
 
@@ -381,19 +396,33 @@
 
     /*
     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 namespacePath = $form.attr('data-namespace-path'),
+            projectPath = $form.attr('data-project-path')
+            discussionId = $form.attr('data-discussion-id'),
+            mergeRequestId = $form.attr('data-noteable-iid'),
+            namespace = namespacePath + '/' + projectPath;
+
+        if (ResolveService != null) {
+          ResolveService.toggleResolveForDiscussion(namespace, mergeRequestId, discussionId);
+        }
+      }
+
       this.renderDiscussionNote(note);
-      return this.removeDiscussionNoteForm($(xhr.target));
+      this.removeDiscussionNoteForm($form);
     };
 
 
     /*
     Called in response to the edit note form being submitted
-    
+
     Updates the current note field.
      */
 
@@ -404,13 +433,18 @@
       $html.syntaxHighlight();
       $html.find('.js-task-list-container').taskList('enable');
       $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
      */
@@ -450,7 +484,7 @@
 
     /*
     Called in response to clicking the edit note link
-    
+
     Hides edit form and restores the original note text to the editor textarea.
      */
 
@@ -472,7 +506,7 @@
 
     /*
     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.
      */
@@ -485,6 +519,15 @@
           var note, notes;
           note = $(el);
           notes = note.closest(".notes");
+
+          if (typeof DiffNotesApp !== "undefined" && DiffNotesApp !== null) {
+            ref = DiffNotesApp.$refs[noteId];
+
+            if (ref) {
+              ref.$destroy(true);
+            }
+          }
+
           if (notes.find(".note").length === 1) {
             notes.closest(".timeline-entry").remove();
             notes.closest("tr").remove();
@@ -498,7 +541,7 @@
 
     /*
     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 +558,7 @@
 
     /*
     Called when clicking on the "reply" button for a diff line.
-    
+
     Shows the note form below the notes.
      */
 
@@ -523,17 +566,19 @@
       var form, replyLink;
       form = this.formClone.clone();
       replyLink = $(e.target).closest(".js-discussion-reply-button");
-      replyLink.hide();
-      replyLink.after(form);
+      replyLink
+        .closest('.discussion-reply-holder')
+        .hide()
+        .after(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.
      */
@@ -549,15 +594,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,16 +629,19 @@
       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>";
       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];
@@ -593,11 +655,13 @@
         }
       } else {
         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);
         return this.setupDiscussionNoteForm($link, newForm);
       }
     };
@@ -605,7 +669,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,7 +680,9 @@
       glForm = form.data('gl-form');
       glForm.destroy();
       form.find(".js-note-text").data("autosave").reset();
-      form.prev(".js-discussion-reply-button").show();
+      form
+        .prev('.discussion-reply-holder')
+        .show();
       if (row.is(".js-temp-notes-holder")) {
         return row.remove();
       } else {
@@ -634,7 +700,7 @@
 
     /*
     Called after an attachment file has been selected.
-    
+
     Updates the file name for the selected attachment.
      */
 
@@ -725,6 +791,18 @@
       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-namespace-path', $this.attr('data-namespace-path'))
+        .attr('data-project-path', $this.attr('data-project-path'));
+    };
+
     return Notes;
 
   })();
diff --git a/app/assets/javascripts/pipeline.js.es6 b/app/assets/javascripts/pipeline.js.es6
new file mode 100644
index 0000000000000000000000000000000000000000..bf33eb1010053e8d09aee6120d8629b9b13add11
--- /dev/null
+++ b/app/assets/javascripts/pipeline.js.es6
@@ -0,0 +1,15 @@
+(function() {
+  function toggleGraph() {
+    const $pipelineBtn = $(this).closest('.toggle-pipeline-btn');
+    const $pipelineGraph = $(this).closest('.row-content-block').next('.pipeline-graph');
+    const $btnText = $(this).find('.toggle-btn-text');
+
+    $($pipelineBtn).add($pipelineGraph).toggleClass('graph-collapsed');
+
+    const graphCollapsed = $pipelineGraph.hasClass('graph-collapsed');
+
+    graphCollapsed ? $btnText.text('Expand') : $btnText.text('Hide')
+  }
+
+  $(document).on('click', '.toggle-pipeline-btn', toggleGraph);
+})();
diff --git a/app/assets/javascripts/markdown_preview.js b/app/assets/javascripts/preview_markdown.js
similarity index 98%
rename from app/assets/javascripts/markdown_preview.js
rename to app/assets/javascripts/preview_markdown.js
index 18fc7bae09a6e5f65a3fbbce792b11097bde90fa..5fd7579964024c842915af9f237863d7335394f0 100644
--- a/app/assets/javascripts/markdown_preview.js
+++ b/app/assets/javascripts/preview_markdown.js
@@ -28,7 +28,7 @@
     };
 
     MarkdownPreview.prototype.renderMarkdown = function(text, success) {
-      if (!window.markdown_preview_path) {
+      if (!window.preview_markdown_path) {
         return;
       }
       if (text === this.ajaxCache.text) {
@@ -36,7 +36,7 @@
       }
       return $.ajax({
         type: 'POST',
-        url: window.markdown_preview_path,
+        url: window.preview_markdown_path,
         data: {
           text: text
         },
diff --git a/app/assets/javascripts/project.js b/app/assets/javascripts/project.js
index e666317716109500a180d47504c50583cf9e3096..4e1de4dfb7279864edd04818abcf1ab188242ba6 100644
--- a/app/assets/javascripts/project.js
+++ b/app/assets/javascripts/project.js
@@ -65,7 +65,8 @@
               url: $dropdown.data('refs-url'),
               data: {
                 ref: $dropdown.data('ref')
-              }
+              },
+              dataType: "json"
             }).done(function(refs) {
               return callback(refs);
             });
@@ -73,7 +74,7 @@
           selectable: true,
           filterable: true,
           filterByText: true,
-          fieldName: 'ref',
+          fieldName: $dropdown.data('field-name'),
           renderRow: function(ref) {
             var link;
             if (ref.header != null) {
@@ -89,8 +90,14 @@
           toggleLabel: function(obj, $el) {
             return $el.text().trim();
           },
-          clicked: function(e) {
-            return $dropdown.closest('form').submit();
+          clicked: function(selected, $el, e) {
+            e.preventDefault()
+            if ($('input[name="ref"]').length) {
+              var $form = $dropdown.closest('form'),
+                  action = $form.attr('action'),
+                  divider = action.indexOf('?') < 0 ? '?' : '&';
+              Turbolinks.visit(action + '' + divider + '' + $form.serialize());
+            }
           }
         });
       });
diff --git a/app/assets/javascripts/project_members.js b/app/assets/javascripts/project_members.js
index f6a796b325aba297ab0312047e7c6cfce5459c3d..78f7b48bc7d726d043ea51a09c27bd04c79b17ba 100644
--- a/app/assets/javascripts/project_members.js
+++ b/app/assets/javascripts/project_members.js
@@ -5,9 +5,6 @@
         return $(this).fadeOut();
       });
     }
-
     return ProjectMembers;
-
   })();
-
 }).call(this);
diff --git a/app/assets/javascripts/protected_branch_access_dropdown.js.es6 b/app/assets/javascripts/protected_branch_access_dropdown.js.es6
new file mode 100644
index 0000000000000000000000000000000000000000..7aeb5f9251402b5936355c442a590c1125c0c8fb
--- /dev/null
+++ b/app/assets/javascripts/protected_branch_access_dropdown.js.es6
@@ -0,0 +1,28 @@
+(global => {
+  global.gl = global.gl || {};
+
+  gl.ProtectedBranchAccessDropdown = class {
+    constructor(options) {
+      const { $dropdown, data, onSelect } = options;
+
+      $dropdown.glDropdown({
+        data: data,
+        selectable: true,
+        inputId: $dropdown.data('input-id'),
+        fieldName: $dropdown.data('field-name'),
+        toggleLabel(item, el) {
+          if (el.is('.is-active')) {
+            return item.text;
+          } else {
+            return 'Select';
+          }
+        },
+        clicked(item, $el, e) {
+          e.preventDefault();
+          onSelect();
+        }
+      });
+    }
+  }
+
+})(window);
diff --git a/app/assets/javascripts/protected_branch_create.js.es6 b/app/assets/javascripts/protected_branch_create.js.es6
new file mode 100644
index 0000000000000000000000000000000000000000..46beca469b99a0da6c9823a2970036e46ddaff94
--- /dev/null
+++ b/app/assets/javascripts/protected_branch_create.js.es6
@@ -0,0 +1,54 @@
+(global => {
+  global.gl = global.gl || {};
+
+  gl.ProtectedBranchCreate = class {
+    constructor() {
+      this.$wrap = this.$form = $('#new_protected_branch');
+      this.buildDropdowns();
+    }
+
+    buildDropdowns() {
+      const $allowedToMergeDropdown = this.$wrap.find('.js-allowed-to-merge');
+      const $allowedToPushDropdown = this.$wrap.find('.js-allowed-to-push');
+
+      // Cache callback
+      this.onSelectCallback = this.onSelect.bind(this);
+
+      // Allowed to Merge dropdown
+      new gl.ProtectedBranchAccessDropdown({
+        $dropdown: $allowedToMergeDropdown,
+        data: gon.merge_access_levels,
+        onSelect: this.onSelectCallback
+      });
+
+      // Allowed to Push dropdown
+      new gl.ProtectedBranchAccessDropdown({
+        $dropdown: $allowedToPushDropdown,
+        data: gon.push_access_levels,
+        onSelect: this.onSelectCallback
+      });
+
+      // Select default
+      $allowedToPushDropdown.data('glDropdown').selectRowAtIndex(0);
+      $allowedToMergeDropdown.data('glDropdown').selectRowAtIndex(0);
+
+      // Protected branch dropdown
+      new ProtectedBranchDropdown({
+        $dropdown: this.$wrap.find('.js-protected-branch-select'),
+        onSelect: this.onSelectCallback
+      });
+    }
+
+    // This will run after clicked callback
+    onSelect() {
+
+      // Enable submit button
+      const $branchInput = this.$wrap.find('input[name="protected_branch[name]"]');
+      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]"]');
+
+      this.$form.find('input[type="submit"]').attr('disabled', !($branchInput.val() && $allowedToMergeInput.length && $allowedToPushInput.length));
+    }
+  }
+
+})(window);
diff --git a/app/assets/javascripts/protected_branch_dropdown.js.es6 b/app/assets/javascripts/protected_branch_dropdown.js.es6
new file mode 100644
index 0000000000000000000000000000000000000000..6738dc8862dff3e10683a7ea926337d16a088524
--- /dev/null
+++ b/app/assets/javascripts/protected_branch_dropdown.js.es6
@@ -0,0 +1,75 @@
+class ProtectedBranchDropdown {
+  constructor(options) {
+    this.onSelect = options.onSelect;
+    this.$dropdown = options.$dropdown;
+    this.$dropdownContainer = this.$dropdown.parent();
+    this.$dropdownFooter = this.$dropdownContainer.find('.dropdown-footer');
+    this.$protectedBranch = this.$dropdownContainer.find('.create-new-protected-branch');
+
+    this.buildDropdown();
+    this.bindEvents();
+
+    // Hide footer
+    this.$dropdownFooter.addClass('hidden');
+  }
+
+  buildDropdown() {
+    this.$dropdown.glDropdown({
+      data: this.getProtectedBranches.bind(this),
+      filterable: true,
+      remote: false,
+      search: {
+        fields: ['title']
+      },
+      selectable: true,
+      toggleLabel(selected) {
+        return (selected && 'id' in selected) ? selected.title : 'Protected Branch';
+      },
+      fieldName: 'protected_branch[name]',
+      text(protectedBranch) {
+        return _.escape(protectedBranch.title);
+      },
+      id(protectedBranch) {
+        return _.escape(protectedBranch.id);
+      },
+      onFilter: this.toggleCreateNewButton.bind(this),
+      clicked: (item, $el, e) => {
+        e.preventDefault();
+        this.onSelect();
+      }
+    });
+  }
+
+  bindEvents() {
+    this.$protectedBranch.on('click', this.onClickCreateWildcard.bind(this));
+  }
+
+  onClickCreateWildcard() {
+    this.$dropdown.data('glDropdown').remote.execute();
+    this.$dropdown.data('glDropdown').selectRowAtIndex(0);
+  }
+
+  getProtectedBranches(term, callback) {
+    if (this.selectedBranch) {
+      callback(gon.open_branches.concat(this.selectedBranch));
+    } else {
+      callback(gon.open_branches);
+    }
+  }
+
+  toggleCreateNewButton(branchName) {
+    this.selectedBranch = {
+      title: branchName,
+      id: branchName,
+      text: branchName
+    };
+
+    if (branchName) {
+      this.$dropdownContainer
+        .find('.create-new-protected-branch code')
+        .text(branchName);
+    }
+
+    this.$dropdownFooter.toggleClass('hidden', !branchName);
+  }
+}
diff --git a/app/assets/javascripts/protected_branch_edit.js.es6 b/app/assets/javascripts/protected_branch_edit.js.es6
new file mode 100644
index 0000000000000000000000000000000000000000..40bc4adb71bf86a230f4bbb5879cbd7e7d49cb98
--- /dev/null
+++ b/app/assets/javascripts/protected_branch_edit.js.es6
@@ -0,0 +1,66 @@
+(global => {
+  global.gl = global.gl || {};
+
+  gl.ProtectedBranchEdit = class {
+    constructor(options) {
+      this.$wrap = options.$wrap;
+      this.$allowedToMergeDropdown = this.$wrap.find('.js-allowed-to-merge');
+      this.$allowedToPushDropdown = this.$wrap.find('.js-allowed-to-push');
+
+      this.buildDropdowns();
+    }
+
+    buildDropdowns() {
+
+      // Allowed to merge dropdown
+      new gl.ProtectedBranchAccessDropdown({
+        $dropdown: this.$allowedToMergeDropdown,
+        data: gon.merge_access_levels,
+        onSelect: this.onSelect.bind(this)
+      });
+
+      // Allowed to push dropdown
+      new gl.ProtectedBranchAccessDropdown({
+        $dropdown: this.$allowedToPushDropdown,
+        data: gon.push_access_levels,
+        onSelect: this.onSelect.bind(this)
+      });
+    }
+
+    onSelect() {
+      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'),
+              access_level: $allowedToMergeInput.val()
+            }],
+            push_access_levels_attributes: [{
+              id: this.$allowedToPushDropdown.data('access-level-id'),
+              access_level: $allowedToPushInput.val()
+            }]
+          }
+        },
+        success: () => {
+          this.$wrap.effect('highlight');
+        },
+        error() {
+          $.scrollTo(0);
+          new Flash('Failed to update branch!');
+        }
+      });
+    }
+  }
+
+})(window);
diff --git a/app/assets/javascripts/protected_branch_edit_list.js.es6 b/app/assets/javascripts/protected_branch_edit_list.js.es6
new file mode 100644
index 0000000000000000000000000000000000000000..9ff0fd12c76202031395f05ecd05f1ce55fa6c72
--- /dev/null
+++ b/app/assets/javascripts/protected_branch_edit_list.js.es6
@@ -0,0 +1,17 @@
+(global => {
+  global.gl = global.gl || {};
+
+  gl.ProtectedBranchEditList = class {
+    constructor() {
+      this.$wrap = $('.protected-branches-list');
+
+      // Build edit forms
+      this.$wrap.find('.js-protected-branch-edit-form').each((i, el) => {
+        new gl.ProtectedBranchEdit({
+          $wrap: $(el)
+        });
+      });
+    }
+  }
+
+})(window);
diff --git a/app/assets/javascripts/protected_branch_select.js b/app/assets/javascripts/protected_branch_select.js
deleted file mode 100644
index 3a47fc972dc94d9c10a0f4b805373b0e492091cf..0000000000000000000000000000000000000000
--- a/app/assets/javascripts/protected_branch_select.js
+++ /dev/null
@@ -1,72 +0,0 @@
-(function() {
-  var bind = function(fn, me){ return function(){ return fn.apply(me, arguments); }; };
-
-  this.ProtectedBranchSelect = (function() {
-    function ProtectedBranchSelect(currentProject) {
-      this.toggleCreateNewButton = bind(this.toggleCreateNewButton, this);
-      this.getProtectedBranches = bind(this.getProtectedBranches, this);
-      $('.dropdown-footer').hide();
-      this.dropdown = $('.js-protected-branch-select').glDropdown({
-        data: this.getProtectedBranches,
-        filterable: true,
-        remote: false,
-        search: {
-          fields: ['title']
-        },
-        selectable: true,
-        toggleLabel: function(selected) {
-          if (selected && 'id' in selected) {
-            return selected.title;
-          } else {
-            return 'Protected Branch';
-          }
-        },
-        fieldName: 'protected_branch[name]',
-        text: function(protected_branch) {
-          return _.escape(protected_branch.title);
-        },
-        id: function(protected_branch) {
-          return _.escape(protected_branch.id);
-        },
-        onFilter: this.toggleCreateNewButton,
-        clicked: function() {
-          return $('.protect-branch-btn').attr('disabled', false);
-        }
-      });
-      $('.create-new-protected-branch').on('click', (function(_this) {
-        return function(event) {
-          _this.dropdown.data('glDropdown').remote.execute();
-          return _this.dropdown.data('glDropdown').selectRowAtIndex(event, 0);
-        };
-      })(this));
-    }
-
-    ProtectedBranchSelect.prototype.getProtectedBranches = function(term, callback) {
-      if (this.selectedBranch) {
-        return callback(gon.open_branches.concat(this.selectedBranch));
-      } else {
-        return callback(gon.open_branches);
-      }
-    };
-
-    ProtectedBranchSelect.prototype.toggleCreateNewButton = function(branchName) {
-      this.selectedBranch = {
-        title: branchName,
-        id: branchName,
-        text: branchName
-      };
-      if (branchName === '') {
-        $('.protected-branch-select-footer-list').addClass('hidden');
-        return $('.dropdown-footer').hide();
-      } else {
-        $('.create-new-protected-branch').text("Create Protected Branch: " + branchName);
-        $('.protected-branch-select-footer-list').removeClass('hidden');
-        return $('.dropdown-footer').show();
-      }
-    };
-
-    return ProtectedBranchSelect;
-
-  })();
-
-}).call(this);
diff --git a/app/assets/javascripts/protected_branches_access_select.js.es6 b/app/assets/javascripts/protected_branches_access_select.js.es6
deleted file mode 100644
index e98312bbf37d2241917c28228aa3b944075bafdd..0000000000000000000000000000000000000000
--- a/app/assets/javascripts/protected_branches_access_select.js.es6
+++ /dev/null
@@ -1,63 +0,0 @@
-class ProtectedBranchesAccessSelect {
-  constructor(container, saveOnSelect, selectDefault) {
-    this.container = container;
-    this.saveOnSelect = saveOnSelect;
-
-    this.container.find(".allowed-to-merge").each((i, element) => {
-      var fieldName = $(element).data('field-name');
-      var dropdown = $(element).glDropdown({
-        data: gon.merge_access_levels,
-        selectable: true,
-        fieldName: fieldName,
-        clicked: _.chain(this.onSelect).partial(element).bind(this).value()
-      });
-
-      if (selectDefault) {
-        dropdown.data('glDropdown').selectRowAtIndex(document.createEvent("Event"), 0);
-      }
-    });
-
-
-    this.container.find(".allowed-to-push").each((i, element) => {
-      var fieldName = $(element).data('field-name');
-      var dropdown = $(element).glDropdown({
-        data: gon.push_access_levels,
-        selectable: true,
-        fieldName: fieldName,
-        clicked: _.chain(this.onSelect).partial(element).bind(this).value()
-      });
-
-      if (selectDefault) {
-        dropdown.data('glDropdown').selectRowAtIndex(document.createEvent("Event"), 0);
-      }
-    });
-  }
-
-  onSelect(dropdown, selected, element, e) {
-    $(dropdown).find('.dropdown-toggle-text').text(selected.text);
-    if (this.saveOnSelect) {
-      return $.ajax({
-        type: "POST",
-        url: $(dropdown).data('url'),
-        dataType: "json",
-        data: {
-          _method: 'PATCH',
-          id: $(dropdown).data('id'),
-          protected_branch: {
-            ["" + ($(dropdown).data('type')) + "_attributes"]: {
-              "access_level": selected.id
-            }
-          }
-        },
-        success: function() {
-          var row;
-          row = $(e.target);
-          return row.closest('tr').effect('highlight');
-        },
-        error: function() {
-          return new Flash("Failed to update branch!", "alert");
-        }
-      });
-    }
-  }
-}
diff --git a/app/assets/javascripts/search_autocomplete.js b/app/assets/javascripts/search_autocomplete.js
index 990f6536eb2a69a4ca8b2d9425e4a8cb2a0d0066..227e8c696b4ffce37f8fd682c3da4e2871c1ebaa 100644
--- a/app/assets/javascripts/search_autocomplete.js
+++ b/app/assets/javascripts/search_autocomplete.js
@@ -7,7 +7,9 @@
     KEYCODE = {
       ESCAPE: 27,
       BACKSPACE: 8,
-      ENTER: 13
+      ENTER: 13,
+      UP: 38,
+      DOWN: 40
     };
 
     function SearchAutocomplete(opts) {
@@ -223,6 +225,12 @@
         case KEYCODE.ESCAPE:
           this.restoreOriginalState();
           break;
+        case KEYCODE.ENTER:
+          this.disableAutocomplete();
+          break;
+        case KEYCODE.UP:
+        case KEYCODE.DOWN:
+          return;
         default:
           if (this.searchInput.val() === '') {
             this.disableAutocomplete();
@@ -319,9 +327,11 @@
     };
 
     SearchAutocomplete.prototype.disableAutocomplete = function() {
-      this.searchInput.addClass('disabled');
-      this.dropdown.removeClass('open');
-      return this.restoreMenu();
+      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() {
diff --git a/app/assets/javascripts/single_file_diff.js b/app/assets/javascripts/single_file_diff.js
index b9ae497b0e598e132c098dce19188d9cc4005ec0..156b9b8abec32199ca3fbce26e5d3758670fb02b 100644
--- a/app/assets/javascripts/single_file_diff.js
+++ b/app/assets/javascripts/single_file_diff.js
@@ -35,10 +35,16 @@
       this.isOpen = !this.isOpen;
       if (!this.isOpen && !this.hasError) {
         this.content.hide();
-        return this.collapsedContent.show();
+        this.collapsedContent.show();
+        if (typeof DiffNotesApp !== 'undefined') {
+          DiffNotesApp.compileComponents();
+        }
       } else if (this.content) {
         this.collapsedContent.hide();
-        return this.content.show();
+        this.content.show();
+        if (typeof DiffNotesApp !== 'undefined') {
+          DiffNotesApp.compileComponents();
+        }
       } else {
         return this.getContentHTML();
       }
@@ -57,7 +63,11 @@
             _this.hasError = true;
             _this.content = $(ERROR_HTML);
           }
-          return _this.collapsedContent.after(_this.content);
+          _this.collapsedContent.after(_this.content);
+
+          if (typeof DiffNotesApp !== 'undefined') {
+            DiffNotesApp.compileComponents();
+          }
         };
       })(this));
     };
diff --git a/app/assets/javascripts/snippet/snippet_bundle.js b/app/assets/javascripts/snippet/snippet_bundle.js
new file mode 100644
index 0000000000000000000000000000000000000000..855e97eb301ac56fde11741f46335eec74b321ff
--- /dev/null
+++ b/app/assets/javascripts/snippet/snippet_bundle.js
@@ -0,0 +1,12 @@
+/*= 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/templates/issuable_template_selector.js.es6 b/app/assets/javascripts/templates/issuable_template_selector.js.es6
new file mode 100644
index 0000000000000000000000000000000000000000..c32ddf802199e1483123269cbcb72f30ed000145
--- /dev/null
+++ b/app/assets/javascripts/templates/issuable_template_selector.js.es6
@@ -0,0 +1,51 @@
+/*= require ../blob/template_selector */
+
+((global) => {
+  class IssuableTemplateSelector extends TemplateSelector {
+    constructor(...args) {
+      super(...args);
+      this.projectPath = this.dropdown.data('project-path');
+      this.namespacePath = this.dropdown.data('namespace-path');
+      this.issuableType = this.wrapper.data('issuable-type');
+      this.titleInput = $(`#${this.issuableType}_title`);
+
+      let initialQuery = {
+        name: this.dropdown.data('selected')
+      };
+
+      if (initialQuery.name) this.requestFile(initialQuery);
+
+      $('.reset-template', this.dropdown.parent()).on('click', () => {
+        if (this.currentTemplate) this.setInputValueToTemplateContent();
+      });
+    }
+
+    requestFile(query) {
+      this.startLoadingSpinner();
+      Api.issueTemplate(this.namespacePath, this.projectPath, query.name, this.issuableType, (err, currentTemplate) => {
+        this.currentTemplate = currentTemplate;
+        if (err) return; // Error handled by global AJAX error handler
+        this.stopLoadingSpinner();
+        this.setInputValueToTemplateContent();
+      });
+      return;
+    }
+
+    setInputValueToTemplateContent() {
+      // `this.requestFileSuccess` sets the value of the description input field
+      // 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);
+        this.titleInput.focus();
+      } else {
+        this.requestFileSuccess(this.currentTemplate);
+      }
+      return;
+    }
+  }
+
+  global.IssuableTemplateSelector = IssuableTemplateSelector;
+})(window);
diff --git a/app/assets/javascripts/templates/issuable_template_selectors.js.es6 b/app/assets/javascripts/templates/issuable_template_selectors.js.es6
new file mode 100644
index 0000000000000000000000000000000000000000..bd8cdde033ef98ac77a9d8254b565ace9ea520c7
--- /dev/null
+++ b/app/assets/javascripts/templates/issuable_template_selectors.js.es6
@@ -0,0 +1,29 @@
+((global) => {
+  class IssuableTemplateSelectors {
+    constructor(opts = {}) {
+      this.$dropdowns = opts.$dropdowns || $('.js-issuable-selector');
+      this.editor = opts.editor || this.initEditor();
+
+      this.$dropdowns.each((i, dropdown) => {
+        let $dropdown = $(dropdown);
+        new IssuableTemplateSelector({
+          pattern: /(\.md)/,
+          data: $dropdown.data('data'),
+          wrapper: $dropdown.closest('.js-issuable-selector-wrap'),
+          dropdown: $dropdown,
+          editor: this.editor
+        });
+      });
+    }
+
+    initEditor() {
+      let editor = $('.markdown-area');
+      // Proxy ace-editor's .setValue to jQuery's .val
+      editor.setValue = editor.val;
+      editor.getValue = editor.val;
+      return editor;
+    }
+  }
+
+  global.IssuableTemplateSelectors = IssuableTemplateSelectors;
+})(window);
diff --git a/app/assets/javascripts/users_select.js b/app/assets/javascripts/users_select.js
index 4af2a214e12bc48f9cdf58168a523f34dd2f09f4..bad82868ab045dbdcd18e83ee6610336757c917a 100644
--- a/app/assets/javascripts/users_select.js
+++ b/app/assets/javascripts/users_select.js
@@ -13,14 +13,15 @@
       }
       $('.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;
           $dropdown = $(dropdown);
-          _this.projectId = $dropdown.data('project-id');
-          _this.showCurrentUser = $dropdown.data('current-user');
+          options.projectId = $dropdown.data('project-id');
+          options.showCurrentUser = $dropdown.data('current-user');
           showNullUser = $dropdown.data('null-user');
           showAnyUser = $dropdown.data('any-user');
           firstUser = $dropdown.data('first-user');
-          _this.authorId = $dropdown.data('author-id');
+          options.authorId = $dropdown.data('author-id');
           selectedId = $dropdown.data('selected');
           defaultLabel = $dropdown.data('default-label');
           issueURL = $dropdown.data('issueUpdate');
@@ -75,7 +76,7 @@
             data: function(term, callback) {
               var isAuthorFilter;
               isAuthorFilter = $('.js-author-search');
-              return _this.users(term, function(users) {
+              return _this.users(term, options, function(users) {
                 var anyUser, index, j, len, name, obj, showDivider;
                 if (term.length === 0) {
                   showDivider = 0;
@@ -140,7 +141,7 @@
               $selectbox.hide();
               return $value.css('display', '');
             },
-            clicked: function(user) {
+            clicked: function(user, $el, e) {
               var isIssueIndex, isMRIndex, page, selected;
               page = $('body').data('page');
               isIssueIndex = page === 'projects:issues:index';
@@ -148,7 +149,12 @@
               if ($dropdown.hasClass('js-filter-bulk-update')) {
                 return;
               }
-              if ($dropdown.hasClass('js-filter-submit') && (isIssueIndex || isMRIndex)) {
+              if (page === 'projects:boards:show') {
+                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')) {
@@ -185,11 +191,14 @@
       $('.ajax-users-select').each((function(_this) {
         return function(i, select) {
           var firstUser, showAnyUser, showEmailUser, showNullUser;
-          _this.projectId = $(select).data('project-id');
-          _this.groupId = $(select).data('group-id');
-          _this.showCurrentUser = $(select).data('current-user');
-          _this.authorId = $(select).data('author-id');
-          _this.skipUsers = $(select).data('skip-users');
+          var options = {};
+          options.skipLdap = $(select).hasClass('skip_ldap');
+          options.projectId = $(select).data('project-id');
+          options.groupId = $(select).data('group-id');
+          options.showCurrentUser = $(select).data('current-user');
+          options.pushCodeToProtectedBranches = $(select).data('push-code-to-protected-branches');
+          options.authorId = $(select).data('author-id');
+          options.skipUsers = $(select).data('skip-users');
           showNullUser = $(select).data('null-user');
           showAnyUser = $(select).data('any-user');
           showEmailUser = $(select).data('email-user');
@@ -199,7 +208,7 @@
             multiple: $(select).hasClass('multiselect'),
             minimumInputLength: 0,
             query: function(query) {
-              return _this.users(query.term, function(users) {
+              return _this.users(query.term, options, function(users) {
                 var anyUser, data, emailUser, index, j, len, name, nullUser, obj, ref;
                 data = {
                   results: users
@@ -309,7 +318,7 @@
       });
     };
 
-    UsersSelect.prototype.users = function(query, callback) {
+    UsersSelect.prototype.users = function(query, options, callback) {
       var url;
       url = this.buildUrl(this.usersPath);
       return $.ajax({
@@ -318,11 +327,13 @@
           search: query,
           per_page: 20,
           active: true,
-          project_id: this.projectId,
-          group_id: this.groupId,
-          current_user: this.showCurrentUser,
-          author_id: this.authorId,
-          skip_users: this.skipUsers
+          project_id: options.projectId || null,
+          group_id: options.groupId || null,
+          skip_ldap: options.skipLdap || null,
+          current_user: options.showCurrentUser || null,
+          push_code_to_protected_branches: options.pushCodeToProtectedBranches || null,
+          author_id: options.authorId || null,
+          skip_users: options.skipUsers || null
         },
         dataType: "json"
       }).done(function(users) {
diff --git a/app/assets/stylesheets/behaviors.scss b/app/assets/stylesheets/behaviors.scss
index 542a53f0377f924f06dbc104f99251f0d56797e9..897bc49e7df0e098b98128d4479d0bd446351dec 100644
--- a/app/assets/stylesheets/behaviors.scss
+++ b/app/assets/stylesheets/behaviors.scss
@@ -20,3 +20,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/buttons.scss b/app/assets/stylesheets/framework/buttons.scss
index 473530cf09435059b18da95a578463a97bed37ea..6c3786b49bb9469d5e34ec1d2cf1ccc854c20103 100644
--- a/app/assets/stylesheets/framework/buttons.scss
+++ b/app/assets/stylesheets/framework/buttons.scss
@@ -164,6 +164,10 @@
     @include btn-outline($white-light, $orange-normal, $orange-normal, $orange-light, $white-light, $orange-light);
   }
 
+  &.btn-spam {
+    @include btn-outline($white-light, $red-normal, $red-normal, $red-light, $white-light, $red-light);
+  }
+
   &.btn-danger,
   &.btn-remove,
   &.btn-red {
@@ -200,6 +204,10 @@
     position: relative;
     top: 2px;
   }
+
+  svg, .fa {
+    margin-right: 3px;
+  }
 }
 
 .btn-lg {
diff --git a/app/assets/stylesheets/framework/dropdowns.scss b/app/assets/stylesheets/framework/dropdowns.scss
index c54eb0d64790824cced0fd1df5f3718dd7ca0231..7d3a063d6c2c25cdaccce9334b8ce5ad5c15850b 100644
--- a/app/assets/stylesheets/framework/dropdowns.scss
+++ b/app/assets/stylesheets/framework/dropdowns.scss
@@ -56,9 +56,13 @@
     position: absolute;
     top: 50%;
     right: 6px;
-    margin-top: -4px;
+    margin-top: -6px;
     color: $dropdown-toggle-icon-color;
     font-size: 10px;
+    &.fa-spinner {
+      font-size: 16px;
+      margin-top: -8px;
+    }
   }
 
   &:hover, {
@@ -72,6 +76,23 @@
   &.large {
     width: 200px;
   }
+
+  &.wide {
+    width: 100%;
+
+    + .dropdown-select {
+      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,
@@ -398,6 +419,7 @@
   font-size: 14px;
 
   a {
+    cursor: pointer;
     padding-left: 10px;
   }
 }
diff --git a/app/assets/stylesheets/framework/files.scss b/app/assets/stylesheets/framework/files.scss
index 407f1873431f823ead2ab6e2fa339223106235f7..d3e3fc50736ecc4a6f1c137173a116edd7192103 100644
--- a/app/assets/stylesheets/framework/files.scss
+++ b/app/assets/stylesheets/framework/files.scss
@@ -63,9 +63,10 @@
     &.image_file {
       background: #eee;
       text-align: center;
+      
       img {
-        padding: 100px;
-        max-width: 50%;
+        padding: 20px;
+        max-width: 80%;
       }
     }
 
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/highlight.scss b/app/assets/stylesheets/framework/highlight.scss
index 7cf4d4fba421fc51c087c69f09e9a1771e14b861..07c8874bf038e47ef8d0e6a061913619351978a1 100644
--- a/app/assets/stylesheets/framework/highlight.scss
+++ b/app/assets/stylesheets/framework/highlight.scss
@@ -6,11 +6,11 @@
   table-layout: fixed;
 
   pre {
-    padding: 10px;
+    padding: 10px 0;
     border: none;
     border-radius: 0;
     font-family: $monospace_font;
-    font-size: $code_font_size !important;
+    font-size: $code_font_size;
     line-height: $code_line_height !important;
     margin: 0;
     overflow: auto;
@@ -20,13 +20,20 @@
     border-left: 1px solid;
 
     code {
+      display: inline-block;
+      min-width: 100%;
       font-family: $monospace_font;
-      white-space: pre;
+      white-space: normal;
       word-wrap: normal;
       padding: 0;
 
       .line {
-        display: inline-block;
+        display: block;
+        width: 100%;
+        min-height: 19px;
+        padding-left: 10px;
+        padding-right: 10px;
+        white-space: pre;
       }
     }
   }
diff --git a/app/assets/stylesheets/framework/lists.scss b/app/assets/stylesheets/framework/lists.scss
index 2c40ec430ca43f8794ccce4ded6b4186ef15f7c5..965fcc06518a350e0ee4a6836911757e5ce16274 100644
--- a/app/assets/stylesheets/framework/lists.scss
+++ b/app/assets/stylesheets/framework/lists.scss
@@ -114,6 +114,12 @@ ul.content-list {
     font-size: $list-font-size;
     color: $list-text-color;
 
+    &.no-description {
+      .title {
+        line-height: $list-text-height;
+      }
+    }
+
     .title {
       font-weight: 600;
     }
@@ -134,12 +140,11 @@ ul.content-list {
     }
 
     .controls {
-      padding-top: 1px;
       float: right;
 
       > .control-text {
         margin-right: $gl-padding-top;
-        line-height: 40px;
+        line-height: $list-text-height;
 
         &:last-child {
           margin-right: 0;
@@ -150,7 +155,7 @@ ul.content-list {
       > .btn-group {
         margin-right: $gl-padding-top;
         display: inline-block;
-        margin-top: 4px;
+        margin-top: 3px;
         margin-bottom: 4px;
 
         &:last-child {
diff --git a/app/assets/stylesheets/framework/markdown_area.scss b/app/assets/stylesheets/framework/markdown_area.scss
index 96565da1bc9ae788741cdaa94f4bf339e5cd015e..edea4ad00eb3085e6fa052cf2877e68634c82f12 100644
--- a/app/assets/stylesheets/framework/markdown_area.scss
+++ b/app/assets/stylesheets/framework/markdown_area.scss
@@ -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..d2d60ed71967e11d667c72f61c76518c052bf8ce 100644
--- a/app/assets/stylesheets/framework/mixins.scss
+++ b/app/assets/stylesheets/framework/mixins.scss
@@ -123,4 +123,9 @@
       }
     }
   }
-}
\ 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);
+}
diff --git a/app/assets/stylesheets/framework/modal.scss b/app/assets/stylesheets/framework/modal.scss
index 26ad2870aa00834bf8cec81b70514f954b5cc3e6..8374f30d0b2592fffccdd75a0ffdfcdf06708320 100644
--- a/app/assets/stylesheets/framework/modal.scss
+++ b/app/assets/stylesheets/framework/modal.scss
@@ -1,6 +1,5 @@
 .modal-body {
   position: relative;
-  overflow-y: auto;
   padding: 15px;
 
   .form-actions {
diff --git a/app/assets/stylesheets/framework/nav.scss b/app/assets/stylesheets/framework/nav.scss
index 364952d3b4ae76faa2b7d8ec78955ded5c881f47..9e924f99e9c67fb32f25fa973d06320a4b856db2 100644
--- a/app/assets/stylesheets/framework/nav.scss
+++ b/app/assets/stylesheets/framework/nav.scss
@@ -72,6 +72,7 @@
       font-weight: normal;
       background-color: #eee;
       color: #78a;
+      vertical-align: baseline;
     }
   }
 
@@ -182,7 +183,6 @@
 
     > form {
       display: inline-block;
-      margin-top: -1px;
     }
 
     .icon-label {
@@ -193,7 +193,6 @@
       height: 35px;
       display: inline-block;
       position: relative;
-      top: 2px;
       margin-right: $gl-padding-top;
 
       /* Medium devices (desktops, 992px and up) */
diff --git a/app/assets/stylesheets/framework/panels.scss b/app/assets/stylesheets/framework/panels.scss
index 874416e10074f779020fa1dcc1651c3dcbec6551..c6f30e144fdcedfe82350df44c72cda2b6404b86 100644
--- a/app/assets/stylesheets/framework/panels.scss
+++ b/app/assets/stylesheets/framework/panels.scss
@@ -23,4 +23,9 @@
       margin-top: $gl-padding;
     }
   }
+
+  .panel-title {
+    font-size: inherit;
+    line-height: inherit;
+  }
 }
diff --git a/app/assets/stylesheets/framework/selects.scss b/app/assets/stylesheets/framework/selects.scss
index 21d87cc9d341d6775b00be8e3299c811b50fd728..b2e22b60440e4130185db46e4c141f7755d73f69 100644
--- a/app/assets/stylesheets/framework/selects.scss
+++ b/app/assets/stylesheets/framework/selects.scss
@@ -45,7 +45,8 @@
   min-width: 175px;
 }
 
-.select2-results .select2-result-label {
+.select2-results .select2-result-label,
+.select2-more-results {
   padding: 10px 15px;
 }
 
diff --git a/app/assets/stylesheets/framework/sidebar.scss b/app/assets/stylesheets/framework/sidebar.scss
index 3fa4a22258dfa3eb25dda78b70c46802a2cc1134..015fe3debf9b0faf6ec0dac10c1b9196ed14c83e 100644
--- a/app/assets/stylesheets/framework/sidebar.scss
+++ b/app/assets/stylesheets/framework/sidebar.scss
@@ -222,3 +222,7 @@ header.header-pinned-nav {
     padding-right: $sidebar_collapsed_width;
   }
 }
+
+.right-sidebar {
+  border-left: 1px solid $border-color;
+}
diff --git a/app/assets/stylesheets/framework/typography.scss b/app/assets/stylesheets/framework/typography.scss
index 8659604cb8bf11c42f35102549fdb418308ce225..06874a993faff390e9bc1628af4605e2b6a9b2cc 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;
diff --git a/app/assets/stylesheets/framework/variables.scss b/app/assets/stylesheets/framework/variables.scss
index 1882d4e888db70bd8e700269b9d84995b68df8e2..5da390118c66250cf123858de8501e88426e6d78 100644
--- a/app/assets/stylesheets/framework/variables.scss
+++ b/app/assets/stylesheets/framework/variables.scss
@@ -43,6 +43,7 @@ $gl-header-color:      $gl-title-color;
 $list-font-size:   $gl-font-size;
 $list-title-color: $gl-title-color;
 $list-text-color:  $gl-text-color;
+$list-text-height: 42px;
 
 /*
  * Markdown
@@ -275,3 +276,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..16ffbe57a99fbe93f6587700468f0e9ab983ea33 100644
--- a/app/assets/stylesheets/highlight/dark.scss
+++ b/app/assets/stylesheets/highlight/dark.scss
@@ -21,6 +21,10 @@
 
   // 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;
@@ -36,8 +40,7 @@
     }
 
     .line_content.match {
-      color: rgba(255, 255, 255, 0.3);
-      background: rgba(255, 255, 255, 0.1);
+      @include dark-diff-match-line;
     }
   }
 
diff --git a/app/assets/stylesheets/highlight/monokai.scss b/app/assets/stylesheets/highlight/monokai.scss
index 80a509a7c1ac9fa3b949bf8269228d7aa4bc7b9a..7de920e074b37a01aeaa519ae3a3e3701167ec37 100644
--- a/app/assets/stylesheets/highlight/monokai.scss
+++ b/app/assets/stylesheets/highlight/monokai.scss
@@ -21,6 +21,10 @@
 
   // 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;
@@ -36,8 +40,7 @@
     }
 
     .line_content.match {
-      color: rgba(255, 255, 255, 0.3);
-      background: rgba(255, 255, 255, 0.1);
+      @include dark-diff-match-line;
     }
   }
 
diff --git a/app/assets/stylesheets/highlight/solarized_dark.scss b/app/assets/stylesheets/highlight/solarized_dark.scss
index c62bd021aefda15de6fe2baea4d02b965b97e294..b11499c71eec8e065e899011e0143143c704cfbf 100644
--- a/app/assets/stylesheets/highlight/solarized_dark.scss
+++ b/app/assets/stylesheets/highlight/solarized_dark.scss
@@ -21,6 +21,10 @@
 
   // 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;
@@ -36,8 +40,7 @@
     }
 
     .line_content.match {
-      color: rgba(255, 255, 255, 0.3);
-      background: rgba(255, 255, 255, 0.1);
+      @include dark-diff-match-line;
     }
   }
 
diff --git a/app/assets/stylesheets/highlight/solarized_light.scss b/app/assets/stylesheets/highlight/solarized_light.scss
index 524cfaf90c309c43d0aac388acbaf80fc87984b3..657bb5e3cd964109a49e58ccdb12b59ef355bf06 100644
--- a/app/assets/stylesheets/highlight/solarized_light.scss
+++ b/app/assets/stylesheets/highlight/solarized_light.scss
@@ -1,4 +1,10 @@
 /* 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 {
@@ -21,6 +27,10 @@
 
   // 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;
@@ -36,8 +46,7 @@
     }
 
     .line_content.match {
-      color: $black-transparent;
-      background: rgba(255, 255, 255, 0.4);
+      @include matchLine;
     }
   }
 
diff --git a/app/assets/stylesheets/highlight/white.scss b/app/assets/stylesheets/highlight/white.scss
index 31a4e3deaac866c8446a9ef2b55c72c547a03418..36a80a916b2df218d3a9420b20528712fa4db5ce 100644
--- a/app/assets/stylesheets/highlight/white.scss
+++ b/app/assets/stylesheets/highlight/white.scss
@@ -1,4 +1,10 @@
 /* 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 {
@@ -22,6 +28,10 @@
   // Diff line
   .line_holder {
 
+    &.match .line_content {
+      @include matchLine;
+    }
+
     .diff-line-num {
       &.old {
         background-color: $line-number-old;
@@ -57,8 +67,7 @@
       }
 
       &.match {
-        color: $black-transparent;
-        background-color: $match-line;
+        @include matchLine;
       }
 
       &.hll:not(.empty-cell) {
diff --git a/app/assets/stylesheets/mailers/repository_push_email.scss b/app/assets/stylesheets/mailers/repository_push_email.scss
index 33aedf1f7c101c002fbf04d661ca35a75f285582..5bfe9bcb443dd9a9aff5d13dd505f5751165fa04 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,6 +70,10 @@
   }
 }
 
+pre {
+  margin: 0;
+}
+
 span.highlight_word {
   background-color: #fafe3d !important;
 }
diff --git a/app/assets/stylesheets/pages/admin.scss b/app/assets/stylesheets/pages/admin.scss
index 5607239d92df1805797b42672cee1bf203ece5ad..c9cdfdcd29c92b10c33824687933b7c80fbb8b5f 100644
--- a/app/assets/stylesheets/pages/admin.scss
+++ b/app/assets/stylesheets/pages/admin.scss
@@ -72,7 +72,6 @@
   margin-bottom: 20px;
 }
 
-
 // Users List
 
 .users-list {
@@ -98,3 +97,44 @@
     }
   }
 }
+
+.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/boards.scss b/app/assets/stylesheets/pages/boards.scss
new file mode 100644
index 0000000000000000000000000000000000000000..9ac4d801ac426fbe8a745eaeff31ab6b913e0cba
--- /dev/null
+++ b/app/assets/stylesheets/pages/boards.scss
@@ -0,0 +1,306 @@
+[v-cloak] {
+  display: none;
+}
+
+.user-can-drag {
+  cursor: -webkit-grab;
+  cursor: grab;
+}
+
+.is-dragging {
+  // Important because plugin sets inline CSS
+  opacity: 1!important;
+  
+  * {
+    // !important to make sure no style can override this when dragging
+    cursor: -webkit-grabbing!important;
+    cursor: grabbing!important;
+  }
+}
+
+.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;
+    color: #9c9c9c;
+  }
+}
+
+.issue-boards-page {
+  .content-wrapper {
+    display: -webkit-flex;
+    display: flex;
+    -webkit-flex-direction: column;
+    flex-direction: column;
+  }
+
+  .sub-nav,
+  .issues-filters {
+    -webkit-flex: none;
+    flex: none;
+  }
+
+  .page-with-sidebar {
+    display: -webkit-flex;
+    display: flex;
+    min-height: 100vh;
+    max-height: 100vh;
+    padding-bottom: 0;
+  }
+
+  .issue-boards-content {
+    display: -webkit-flex;
+    display: flex;
+    -webkit-flex: 1;
+    flex: 1;
+    width: 100%;
+
+    .content {
+      display: -webkit-flex;
+      display: flex;
+      -webkit-flex-direction: column;
+      flex-direction: column;
+      width: 100%;
+    }
+  }
+}
+
+.boards-app-loading {
+  width: 100%;
+  font-size: 34px;
+}
+
+.boards-list {
+  display: -webkit-flex;
+  display: flex;
+  -webkit-flex: 1;
+  flex: 1;
+  -webkit-flex-basis: 0;
+  flex-basis: 0;
+  min-height: calc(100vh - 152px);
+  max-height: calc(100vh - 152px);
+  padding-top: 25px;
+  padding-right: ($gl-padding / 2);
+  padding-left: ($gl-padding / 2);
+  overflow-x: scroll;
+
+  @media (min-width: $screen-sm-min) {
+    min-height: 475px;
+    max-height: none;
+  }
+}
+
+.board {
+  display: -webkit-flex;
+  display: flex;
+  min-width: calc(85vw - 15px);
+  max-width: calc(85vw - 15px);
+  margin-bottom: 25px;
+  padding-right: ($gl-padding / 2);
+  padding-left: ($gl-padding / 2);
+
+  @media (min-width: $screen-sm-min) {
+    min-width: 400px;
+    max-width: 400px;
+  }
+}
+
+.board-inner {
+  display: -webkit-flex;
+  display: flex;
+  -webkit-flex-direction: column;
+  flex-direction: column;
+  width: 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-header-loading-spinner {
+  margin-right: 10px;
+  color: $gray-darkest;
+}
+
+.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-search-container {
+  position: relative;
+  background-color: #fff;
+
+  .form-control {
+    padding-right: 30px;
+  }
+}
+
+.board-search-icon,
+.board-search-clear-btn {
+  position: absolute;
+  right: $gl-padding + 10px;
+  top: 50%;
+  margin-top: -7px;
+  font-size: 14px;
+}
+
+.board-search-icon {
+  color: $gl-placeholder-color;
+}
+
+.board-search-clear-btn {
+  padding: 0;
+  line-height: 1;
+  background: transparent;
+  border: 0;
+  outline: 0;
+
+  &:hover {
+    color: $gl-link-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: 100%;
+  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 {
+  -webkit-flex: 1;
+  flex: 1;
+  height: 400px;
+  margin-bottom: 0;
+  padding: 5px;
+  overflow-y: scroll;
+  overflow-x: hidden;
+}
+
+.board-list-loading {
+  margin-top: 10px;
+  font-size: 26px;
+}
+
+.is-ghost {
+  opacity: 0.3;
+}
+
+.card {
+  position: relative;
+  width: 100%;
+  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;
+
+  &.user-can-drag {
+    padding-left: $gl-padding;
+  }
+
+  &:not(:last-child) {
+    margin-bottom: 5px;
+  }
+
+  a {
+    cursor: pointer;
+  }
+
+  .label {
+    border: 0;
+    outline: 0;
+  }
+
+  .confidential-icon {
+    margin-right: 5px;
+  }
+}
+
+.card-title {
+  margin: 0;
+  font-size: 1em;
+
+  a {
+    color: inherit;
+  }
+}
+
+.card-footer {
+  margin-top: 5px;
+  line-height: 25px;
+
+  .label {
+    margin-right: 4px;
+    font-size: (14px / $issue-boards-font-size) * 1em;
+  }
+}
+
+.card-number {
+  margin-right: 8px;
+  font-weight: 500;
+}
diff --git a/app/assets/stylesheets/pages/builds.scss b/app/assets/stylesheets/pages/builds.scss
index e26f8f7080d7ae94f7abf91ccac6c63dbb9f783a..c1bb250b42d35b151a58729906fd9efe4ff7f96a 100644
--- a/app/assets/stylesheets/pages/builds.scss
+++ b/app/assets/stylesheets/pages/builds.scss
@@ -53,14 +53,6 @@
       left: 70px;
     }
   }
-
-  .nav-links {
-    svg {
-      position: relative;
-      top: 2px;
-      margin-right: 3px;
-    }
-  }
 }
 
 .build-header {
@@ -108,24 +100,101 @@
 }
 
 .right-sidebar.build-sidebar {
-  padding-top: $gl-padding;
-  padding-bottom: $gl-padding;
+  padding: $gl-padding 0;
 
   &.right-sidebar-collapsed {
     display: none;
   }
 
+  .blocks-container {
+    padding: $gl-padding;
+  }
+
   .block {
     width: 100%;
   }
 
   .build-sidebar-header {
-    padding-top: 0;
+    padding: 0 $gl-padding $gl-padding;
 
     .gutter-toggle {
       margin-top: 0;
     }
   }
+
+  .stage-item {
+    cursor: pointer;
+
+    &:hover {
+      color: $gl-text-color;
+    }
+  }
+
+  .build-dropdown {
+    padding: 0 $gl-padding;
+
+    .dropdown-menu-toggle {
+      margin-top: 8px;
+    }
+
+    .dropdown-menu {
+      right: $gl-padding;
+      left: $gl-padding;
+      width: auto;
+    }
+  }
+
+  .builds-container {
+    margin-top: $gl-padding;
+    background-color: $white-light;
+    border-top: 1px solid $border-color;
+    border-bottom: 1px solid $border-color;
+    max-height: 300px;
+    overflow: scroll;
+
+    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 {
+        position: absolute;
+        left: 15px;
+        top: 20px;
+        display: none;
+      }
+
+      &.active {
+        font-weight: bold;
+
+        .fa {
+          display: block;
+        }
+      }
+
+      &:hover {
+        background-color: $row-hover;
+      }
+    }
+  }
 }
 
 .build-detail-row {
diff --git a/app/assets/stylesheets/pages/commit.scss b/app/assets/stylesheets/pages/commit.scss
index bbe0c6c5f1fa1842bcf0528c402ecb3701c80a90..53ec0002afed6a60016c499b556e2854bb3c21a9 100644
--- a/app/assets/stylesheets/pages/commit.scss
+++ b/app/assets/stylesheets/pages/commit.scss
@@ -66,6 +66,15 @@
       margin-left: 8px;
     }
   }
+
+  .ci-status-link {
+
+    svg {
+      position: relative;
+      top: 2px;
+      margin: 0 2px 0 3px;
+    }
+  }
 }
 
 .ci-status-link {
diff --git a/app/assets/stylesheets/pages/commits.scss b/app/assets/stylesheets/pages/commits.scss
index cbc980f52ff1570e668f85c2590e9ce1d53810dc..6a58b445afaf580328f8c2b85a7b7b82d136ad56 100644
--- a/app/assets/stylesheets/pages/commits.scss
+++ b/app/assets/stylesheets/pages/commits.scss
@@ -1,8 +1,6 @@
 .commits-compare-switch {
   @include btn-default;
   @include btn-white;
-  background: image-url("switch_icon.png") no-repeat center center;
-  text-indent: -9999px;
   float: left;
   margin-right: 9px;
 }
diff --git a/app/assets/stylesheets/pages/detail_page.scss b/app/assets/stylesheets/pages/detail_page.scss
index 1b389d83525da3cd5e2bfcad042bd1cda135218d..4d9c73c6840a02456836801acc3d3a70c03d0692 100644
--- a/app/assets/stylesheets/pages/detail_page.scss
+++ b/app/assets/stylesheets/pages/detail_page.scss
@@ -34,11 +34,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 21b1c223c88b8432d2fa35f2de1f716385bb92ec..21cee2e3a70cced31cea9364f02e9ac5278b8f24 100644
--- a/app/assets/stylesheets/pages/diff.scss
+++ b/app/assets/stylesheets/pages/diff.scss
@@ -164,7 +164,10 @@
       line-height: 0;
       img {
         border: 1px solid #fff;
-        background: image-url('trans_bg.gif');
+        background-image: linear-gradient(45deg, #e5e5e5 25%, transparent 25%, transparent 75%, #e5e5e5 75%, #e5e5e5 100%),
+        linear-gradient(45deg, #e5e5e5 25%, transparent 25%, transparent 75%, #e5e5e5 75%, #e5e5e5 100%);
+        background-size: 10px 10px;
+        background-position: 0 0, 5px 5px;
         max-width: 100%;
       }
       &.deleted {
diff --git a/app/assets/stylesheets/pages/environments.scss b/app/assets/stylesheets/pages/environments.scss
index e160d676e35c3dffbce4d19e3cbca2c3779b6fac..55f9d4a001123f9549b31aace1063f1e85e2bdf3 100644
--- a/app/assets/stylesheets/pages/environments.scss
+++ b/app/assets/stylesheets/pages/environments.scss
@@ -1,5 +1,35 @@
 .environments {
+
   .commit-title {
     margin: 0;
   }
+
+  .fa-play {
+    font-size: 14px;
+  }
+
+  .dropdown-new {
+    color: $table-text-gray;
+  }
+
+  .dropdown-menu {
+
+    .fa {
+      margin-right: 6px;
+      color: $table-text-gray;
+    }
+  }
+
+  .branch-name {
+    color: $gl-dark-link-color;
+  }
+}
+
+.table.builds.environments {
+  min-width: 500px;
+
+  .icon-container {
+    width: 20px;
+    text-align: center;
+  }
 }
diff --git a/app/assets/stylesheets/pages/groups.scss b/app/assets/stylesheets/pages/groups.scss
index 2a3acc3eb4c6028fd37d14ff57f702f7467e47c8..b657ca47d38637753b44b19f31b97eea54a3c1cf 100644
--- a/app/assets/stylesheets/pages/groups.scss
+++ b/app/assets/stylesheets/pages/groups.scss
@@ -23,15 +23,9 @@
 }
 
 .group-row {
-  &.no-description {
-    .group-name {
-      line-height: 44px;
-    }
-  }
-
   .stats {
     float: right;
-    line-height: 44px;
+    line-height: $list-text-height;
     color: $gl-gray;
 
     span {
diff --git a/app/assets/stylesheets/pages/issuable.scss b/app/assets/stylesheets/pages/issuable.scss
index 7a50bc9c8320f369f47da2728a1ff4d8f515e601..46c4a11aa2eb07ad642e3d68bdedd413bfe170e0 100644
--- a/app/assets/stylesheets/pages/issuable.scss
+++ b/app/assets/stylesheets/pages/issuable.scss
@@ -395,3 +395,12 @@
   display: inline-block;
   line-height: 18px;
 }
+
+.js-issuable-selector-wrap {
+  .js-issuable-selector {
+    width: 100%;
+  }
+  @media (max-width: $screen-sm-max) {
+    margin-bottom: $gl-padding;
+  }
+}
diff --git a/app/assets/stylesheets/pages/labels.scss b/app/assets/stylesheets/pages/labels.scss
index 3b1e38fc07ddd44a9d6b346759a181aa14089d1f..606459f82cd03c89102b723c5db1bacd9081ba49 100644
--- a/app/assets/stylesheets/pages/labels.scss
+++ b/app/assets/stylesheets/pages/labels.scss
@@ -182,6 +182,17 @@
   .btn {
     color: inherit;
   }
+
+  a.btn {
+    padding: 0;
+
+    .has-tooltip {
+      top: 0;
+      border-top-right-radius: 0;
+      border-bottom-right-radius: 0;
+      line-height: 1.1;
+    }
+  }
 }
 
 .label-options-toggle {
diff --git a/app/assets/stylesheets/pages/merge_conflicts.scss b/app/assets/stylesheets/pages/merge_conflicts.scss
new file mode 100644
index 0000000000000000000000000000000000000000..1f499897c165bee80aeb1af77ea45f3d23c9e742
--- /dev/null
+++ b/app/assets/stylesheets/pages/merge_conflicts.scss
@@ -0,0 +1,238 @@
+$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       : #f9f9f9,
+
+
+  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;
+      outline: none;
+      color: #fff;
+      width: 75px; // static width to make 2 buttons have same width
+      height: 19px;
+    }
+  }
+
+  .btn-success .fa-spinner {
+    color: #fff;
+  }
+}
diff --git a/app/assets/stylesheets/pages/merge_requests.scss b/app/assets/stylesheets/pages/merge_requests.scss
index 0a661e529f0c96e364b64a0fcccbc364c9adda1e..fcdaf671538a66e1fdafd5bd5c3e0a484b5c8e43 100644
--- a/app/assets/stylesheets/pages/merge_requests.scss
+++ b/app/assets/stylesheets/pages/merge_requests.scss
@@ -69,6 +69,10 @@
 
     &.ci-success {
       color: $gl-success;
+
+      a.environment {
+        color: inherit;
+      }
     }
 
     &.ci-success_with_warnings {
@@ -126,7 +130,6 @@
       &.has-conflicts .fa-exclamation-triangle {
         color: $gl-warning;
       }
-
     }
 
     p:last-child {
@@ -371,3 +374,10 @@
     }
   }
 }
+
+.merge-request-details {
+
+  .title {
+    margin-bottom: 20px;
+  }
+}
diff --git a/app/assets/stylesheets/pages/note_form.scss b/app/assets/stylesheets/pages/note_form.scss
index 3784010348a0dcdbfa0876081f16283d08c7197d..bd875b9823ffed1efebbc513110648bc5289f34f 100644
--- a/app/assets/stylesheets/pages/note_form.scss
+++ b/app/assets/stylesheets/pages/note_form.scss
@@ -159,6 +159,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;
 }
diff --git a/app/assets/stylesheets/pages/notes.scss b/app/assets/stylesheets/pages/notes.scss
index a2b5437e5031080ed00835470d1967fd7d17bf70..08d1692c888c5110268116aff95c6cf890140fb0 100644
--- a/app/assets/stylesheets/pages/notes.scss
+++ b/app/assets/stylesheets/pages/notes.scss
@@ -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..6fa097e3bf165a1b877fe7978a4fa97c2e489abf 100644
--- a/app/assets/stylesheets/pages/pipelines.scss
+++ b/app/assets/stylesheets/pages/pipelines.scss
@@ -229,3 +229,203 @@
     box-shadow: none;
   }
 }
+
+// Pipeline visualization
+
+.toggle-pipeline-btn {
+  background-color: $gray-dark;
+
+  .caret {
+    border-top: none;
+    border-bottom: 4px solid;
+  }
+
+  &.graph-collapsed {
+    background-color: $white-light;
+
+    .caret {
+      border-bottom: none;
+      border-top: 4px solid;
+    }
+  }
+}
+
+.pipeline-graph {
+  width: 100%;
+  overflow: auto;
+  white-space: nowrap;
+  max-height: 500px;
+  transition: max-height 0.3s, padding 0.3s;
+
+  &.graph-collapsed {
+    max-height: 0;
+    padding: 0 16px;
+  }
+}
+
+.pipeline-visualization {
+  position: relative;
+  min-width: 1220px;
+
+  ul {
+    padding: 0;
+  }
+}
+
+.stage-column {
+  display: inline-block;
+  vertical-align: top;
+  margin-right: 50px;
+
+  li {
+    list-style: none;
+  }
+
+  .stage-name {
+    margin-bottom: 15px;
+    font-weight: bold;
+    width: 150px;
+    white-space: nowrap;
+    overflow: hidden;
+    text-overflow: ellipsis;
+  }
+
+  .build {
+    border: 1px solid $border-color;
+    position: relative;
+    padding: 6px 10px;
+    border-radius: 30px;
+    width: 150px;
+    margin-bottom: 10px;
+
+    &.playable {
+      background-color: $gray-light;
+
+      svg {
+        height: 12px;
+        width: 12px;
+        position: relative;
+        top: 1px;
+
+        path {
+          fill: $layout-link-gray;
+        }
+      }
+    }
+
+    .build-content {
+      width: 130px;
+      white-space: nowrap;
+      overflow: hidden;
+      text-overflow: ellipsis;
+
+      a {
+        color: $layout-link-gray;
+      }
+    }
+
+    svg {
+      position: relative;
+      top: 2px;
+      margin-right: 5px;
+    }
+
+    // Connect first build in each stage with right horizontal line
+    &:first-child {
+      &::after {
+        content: '';
+        position: absolute;
+        top: 50%;
+        right: -54px;
+        border-top: 2px solid $border-color;
+        width: 54px;
+        height: 1px;
+      }
+    }
+
+    // Connect each build (except for first) with curved lines
+    &:not(:first-child) {
+      &::after, &::before {
+        content: '';
+        top: -47px;
+        position: absolute;
+        border-bottom: 2px solid $border-color;
+        width: 20px;
+        height: 65px;
+      }
+
+      // Right connecting curves
+      &::after {
+        right: -20px;
+        border-right: 2px solid $border-color;
+        border-radius: 0 0 50px;
+      }
+
+      // Left connecting curves
+      &::before {
+        left: -20px;
+        border-left: 2px solid $border-color;
+        border-radius: 0 0 0 50px;
+      }
+    }
+
+    // Connect second build to first build with smaller curved line
+    &:nth-child(2) {
+      &::after, &::before {
+        height: 45px;
+        top: -26px;
+      }
+    }
+  }
+
+  &:last-child {
+    .build {
+      // Remove right connecting horizontal line from first build in last stage
+      &:first-child {
+        &::after, &::before {
+          border: none;
+        }
+      }
+      // Remove right curved connectors from all builds in last stage
+      &:not(:first-child) {
+        &::after {
+          border: none;
+        }
+      }
+    }
+  }
+
+  &:first-child {
+    .build {
+      // Remove left curved connectors from all builds in first stage
+      &:not(:first-child) {
+        &::before {
+          border: none;
+        }
+      }
+    }
+  }
+}
+
+.pipeline-actions {
+  border-bottom: none;
+}
+
+.toggle-pipeline-btn {
+
+  .fa {
+    color: $dropdown-header-color;
+  }
+}
+
+.pipelines.tab-pane {
+
+  .content-list.pipelines {
+    overflow: scroll;
+  }
+
+  .stage {
+    max-width: 60px;
+    width: 60px;
+  }
+}
diff --git a/app/assets/stylesheets/pages/profile.scss b/app/assets/stylesheets/pages/profile.scss
index 46371ec6871fa7569c69b5feed3d6b9c698b1356..6f58203f49c2632ae6c63da90fff008c8b8b95d8 100644
--- a/app/assets/stylesheets/pages/profile.scss
+++ b/app/assets/stylesheets/pages/profile.scss
@@ -228,3 +228,9 @@
     }
   }
 }
+
+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/projects.scss b/app/assets/stylesheets/pages/projects.scss
index 4409477916f42799e9d827aeefa914224661e994..eaf2d3270b30379eab2a803c1899fde3aaef2124 100644
--- a/app/assets/stylesheets/pages/projects.scss
+++ b/app/assets/stylesheets/pages/projects.scss
@@ -99,7 +99,7 @@
     margin-left: auto;
     margin-right: auto;
     margin-bottom: 15px;
-    max-width: 480px;
+    max-width: 700px;
 
     > p {
       margin-bottom: 0;
@@ -512,18 +512,12 @@ pre.light-well {
   .project-row {
     border-color: $table-border-color;
 
-    &.no-description {
-      .project {
-        line-height: 40px;
-      }
-    }
-
     .project-full-name {
       @include str-truncated;
     }
 
     .controls {
-      line-height: 40px;
+      line-height: $list-text-height;
 
       a:hover {
         text-decoration: none;
@@ -662,13 +656,9 @@ pre.light-well {
 }
 
 .new_protected_branch {
-  .dropdown {
-    display: inline;
-    margin-left: 15px;
-  }
-
   label {
-    min-width: 120px;
+    margin-top: 6px;
+    font-weight: normal;
   }
 }
 
@@ -684,6 +674,21 @@ pre.light-well {
       font-weight: 600;
     }
   }
+
+  .settings-message {
+    margin: 0;
+    border-radius: 0 0 1px 1px;
+    padding: 20px 0;
+    border: none;
+  }
+
+  .table-bordered {
+    border-radius: 1px;
+
+    th:not(:last-child), td:not(:last-child) {
+      border-right: solid 1px transparent;
+    }
+  }
 }
 
 .custom-notifications-form {
@@ -714,3 +719,29 @@ 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;
+    }
+  }
+}
diff --git a/app/assets/stylesheets/pages/todos.scss b/app/assets/stylesheets/pages/todos.scss
index cf16d070cfe6eaba477a9219e8b20ca5c7421ba3..0340526a53aa811859f3d9b696d2c0d862550bf1 100644
--- a/app/assets/stylesheets/pages/todos.scss
+++ b/app/assets/stylesheets/pages/todos.scss
@@ -20,10 +20,43 @@
   }
 }
 
-.todo {
+.todos-list > .todo {
+  // workaround because we cannot use border-colapse
+  border-top: 1px solid transparent;
+  display: -webkit-flex;
+  display: flex;
+  -webkit-flex-direction: row;
+  flex-direction: row;
+
   &:hover {
+    background-color: $row-hover;
+    border-color: $row-hover-border;
     cursor: pointer;
   }
+
+  // overwrite border style of .content-list
+  &:last-child {
+    border-bottom: 1px solid transparent;
+
+    &:hover {
+      border-color: $row-hover-border;
+    }
+  }
+
+  .todo-actions {
+    display: -webkit-flex;
+    display: flex;
+    -webkit-justify-content: center;
+    justify-content: center;
+    -webkit-flex-direction: column;
+    flex-direction: column;
+    margin-left: 10px;
+  }
+
+  .todo-item {
+    -webkit-flex: auto;
+    flex: auto;
+  }
 }
 
 .todo-item {
@@ -43,8 +76,6 @@
   }
 
   .todo-body {
-    margin-right: 174px;
-
     .todo-note {
       word-wrap: break-word;
 
@@ -90,6 +121,12 @@
 }
 
 @media (max-width: $screen-xs-max) {
+  .todo {
+    .avatar {
+      display: none;
+    }
+  }
+
   .todo-item {
     .todo-title {
       white-space: normal;
@@ -98,10 +135,6 @@
       margin-bottom: 10px;
     }
 
-    .avatar {
-      display: none;
-    }
-
     .todo-body {
       margin: 0;
       border-left: 2px solid #ddd;
diff --git a/app/assets/stylesheets/pages/tree.scss b/app/assets/stylesheets/pages/tree.scss
index 390977297fb4c34bc3f4783775e5d003c0fd74ba..9da40fe2b09d5a6fac7b167defc56af175ee18ea 100644
--- a/app/assets/stylesheets/pages/tree.scss
+++ b/app/assets/stylesheets/pages/tree.scss
@@ -58,6 +58,10 @@
 
     .tree_commit {
       max-width: 320px;
+
+      .str-truncated {
+        max-width: 100%;
+      }
     }
 
     .tree_time_ago {
diff --git a/app/controllers/admin/application_settings_controller.rb b/app/controllers/admin/application_settings_controller.rb
index 9e1dc15de849c53075870720aab4668a9a5c1d56..6ef7cf0bae66bf9b072e5448f0f9c4c5f0327fe4 100644
--- a/app/controllers/admin/application_settings_controller.rb
+++ b/app/controllers/admin/application_settings_controller.rb
@@ -109,6 +109,8 @@ 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,
diff --git a/app/controllers/admin/groups_controller.rb b/app/controllers/admin/groups_controller.rb
index f3a88a8e6c8cd47e8b1143c2fbf14140b9996764..cdfa8d91a2880d3a65d8a88a8e15a7b19a50f4d2 100644
--- a/app/controllers/admin/groups_controller.rb
+++ b/app/controllers/admin/groups_controller.rb
@@ -42,15 +42,15 @@ 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
 
   def destroy
-    DestroyGroupService.new(@group, current_user).execute
+    DestroyGroupService.new(@group, current_user).async_execute
 
-    redirect_to admin_groups_path, notice: 'Group was successfully deleted.'
+    redirect_to admin_groups_path, alert: "Group '#{@group.name}' was scheduled for deletion."
   end
 
   private
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/spam_logs_controller.rb b/app/controllers/admin/spam_logs_controller.rb
index 3a2f0185315d4cbc9d2725e4d5f224a92b91e6b1..2abfa22712d6d7dd67876eb15d4f3e5d9d86326b 100644
--- a/app/controllers/admin/spam_logs_controller.rb
+++ b/app/controllers/admin/spam_logs_controller.rb
@@ -14,4 +14,14 @@ class Admin::SpamLogsController < Admin::ApplicationController
       head :ok
     end
   end
+
+  def mark_as_ham
+    spam_log = SpamLog.find(params[:id])
+
+    if HamService.new(spam_log).mark_as_ham!
+      redirect_to admin_spam_logs_path, notice: 'Spam log successfully submitted as ham.'
+    else
+      redirect_to admin_spam_logs_path, alert: 'Error with Akismet. Please check the logs for more info.'
+    end
+  end
 end
diff --git a/app/controllers/autocomplete_controller.rb b/app/controllers/autocomplete_controller.rb
index d828d163c280e1e40d7257a12594ab96b2300828..b48668eea87295385631f753ef973086e207b2ea 100644
--- a/app/controllers/autocomplete_controller.rb
+++ b/app/controllers/autocomplete_controller.rb
@@ -1,5 +1,6 @@
 class AutocompleteController < ApplicationController
   skip_before_action :authenticate_user!, only: [:users]
+  before_action :load_project, only: [:users]
   before_action :find_users, only: [:users]
 
   def users
@@ -34,19 +35,13 @@ class AutocompleteController < ApplicationController
 
   def projects
     project = Project.find_by_id(params[:project_id])
-
-    projects = current_user.authorized_projects
-    projects = projects.search(params[:search]) if params[:search].present?
-    projects = projects.select do |project|
-      current_user.can?(:admin_issue, project)
-    end
+    projects = projects_finder.execute(project, search: params[:search], offset_id: params[:offset_id])
 
     no_project = {
       id: 0,
       name_with_namespace: 'No project',
     }
-    projects.unshift(no_project)
-    projects.delete(project)
+    projects.unshift(no_project) unless params[:offset_id].present?
 
     render json: projects.to_json(only: [:id, :name_with_namespace], methods: :name_with_namespace)
   end
@@ -55,11 +50,8 @@ class AutocompleteController < ApplicationController
 
   def find_users
     @users =
-      if params[:project_id].present?
-        project = Project.find(params[:project_id])
-        return render_404 unless can?(current_user, :read_project, project)
-
-        project.team.users
+      if @project
+        @project.team.users
       elsif params[:group_id].present?
         group = Group.find(params[:group_id])
         return render_404 unless can?(current_user, :read_group, group)
@@ -71,4 +63,18 @@ class AutocompleteController < ApplicationController
         User.none
       end
   end
+
+  def load_project
+    @project ||= begin
+      if params[:project_id].present?
+        project = Project.find(params[:project_id])
+        return render_404 unless can?(current_user, :read_project, project)
+        project
+      end
+    end
+  end
+
+  def projects_finder
+    MoveToProjectFinder.new(current_user)
+  end
 end
diff --git a/app/controllers/concerns/diff_for_path.rb b/app/controllers/concerns/diff_for_path.rb
index 026d8b2e1e0f691e21cdce694f535a8b8a603071..aeec3009f15538b3317114380c5e92304638b8ab 100644
--- a/app/controllers/concerns/diff_for_path.rb
+++ b/app/controllers/concerns/diff_for_path.rb
@@ -1,8 +1,8 @@
 module DiffForPath
   extend ActiveSupport::Concern
 
-  def render_diff_for_path(diffs, diff_refs, project)
-    diff_file = safe_diff_files(diffs, diff_refs: diff_refs, repository: project.repository).find do |diff|
+  def render_diff_for_path(diffs)
+    diff_file = diffs.diff_files.find do |diff|
       diff.old_path == params[:old_path] && diff.new_path == params[:new_path]
     end
 
@@ -14,7 +14,7 @@ module DiffForPath
     locals = {
       diff_file: diff_file,
       diff_commit: diff_commit,
-      diff_refs: diff_refs,
+      diff_refs: diffs.diff_refs,
       blob: blob,
       project: project
     }
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/service_params.rb b/app/controllers/concerns/service_params.rb
index 58877c5ad5d5cfccc701e39139e4ca356d6e4f72..a69877edfd40b786925787593137e4a22f065921 100644
--- a/app/controllers/concerns/service_params.rb
+++ b/app/controllers/concerns/service_params.rb
@@ -7,8 +7,12 @@ module ServiceParams
                     :build_key, :server, :teamcity_url, :drone_url, :build_type,
                     :description, :issues_url, :new_issue_url, :restrict_to_branch, :channel,
                     :colorize_messages, :channels,
-                    # See app/helpers/services_helper.rb
-                    # for why we need issues_events and merge_requests_events.
+                    # We're using `issues_events` and `merge_requests_events`
+                    # in the view so we still need to explicitly state them
+                    # here. `Service#event_names` would only give
+                    # `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,
                     :notify_only_broken_builds, :notify_only_broken_pipelines,
                     :add_pusher, :send_from_committer_email, :disable_diffs,
diff --git a/app/controllers/concerns/spammable_actions.rb b/app/controllers/concerns/spammable_actions.rb
new file mode 100644
index 0000000000000000000000000000000000000000..29e243c66a33d134115c1c9f754985ca969aa720
--- /dev/null
+++ b/app/controllers/concerns/spammable_actions.rb
@@ -0,0 +1,25 @@
+module SpammableActions
+  extend ActiveSupport::Concern
+
+  included do
+    before_action :authorize_submit_spammable!, only: :mark_as_spam
+  end
+
+  def mark_as_spam
+    if SpamService.new(spammable).mark_as_spam!
+      redirect_to spammable, notice: "#{spammable.class.to_s} was submitted to Akismet successfully."
+    else
+      redirect_to spammable, alert: 'Error with Akismet. Please check the logs for more info.'
+    end
+  end
+
+  private
+
+  def spammable
+    raise NotImplementedError, "#{self.class} does not implement #{__method__}"
+  end
+
+  def authorize_submit_spammable!
+    access_denied! unless current_user.admin?
+  end
+end
diff --git a/app/controllers/dashboard/todos_controller.rb b/app/controllers/dashboard/todos_controller.rb
index 19a76a5b5d8d11a04d8ad179271810d56f6a75c6..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,18 +28,14 @@ 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
 
   def todos_counts
     {
-      count: TodosFinder.new(current_user, state: :pending).execute.count,
-      done_count: TodosFinder.new(current_user, state: :done).execute.count
+      count: current_user.todos_pending_count,
+      done_count: current_user.todos_done_count
     }
   end
 end
diff --git a/app/controllers/groups/group_members_controller.rb b/app/controllers/groups/group_members_controller.rb
index 9fc41a125364f74a8b0f7651fcb4cb995d01d02c..272164cd0ccc8a6cd32222789cb63afeff1222cf 100644
--- a/app/controllers/groups/group_members_controller.rb
+++ b/app/controllers/groups/group_members_controller.rb
@@ -21,7 +21,12 @@ class Groups::GroupMembersController < Groups::ApplicationController
   end
 
   def create
-    @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,
+      expires_at: params[:expires_at]
+    )
 
     redirect_to group_group_members_path(@group), notice: 'Users were successfully added.'
   end
@@ -63,7 +68,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_controller.rb b/app/controllers/groups_controller.rb
index 6780a6d4d8731b23cdae74365aea3493a245a866..cb82d62616c863835f5b77eac7851abef4054bf7 100644
--- a/app/controllers/groups_controller.rb
+++ b/app/controllers/groups_controller.rb
@@ -87,9 +87,9 @@ class GroupsController < Groups::ApplicationController
   end
 
   def destroy
-    DestroyGroupService.new(@group, current_user).execute
+    DestroyGroupService.new(@group, current_user).async_execute
 
-    redirect_to root_path, alert: "Group '#{@group.name}' was successfully deleted."
+    redirect_to root_path, alert: "Group '#{@group.name}' was scheduled for deletion."
   end
 
   protected
diff --git a/app/controllers/import/gitlab_projects_controller.rb b/app/controllers/import/gitlab_projects_controller.rb
index 30df1fb2fecfd071a0f4b833c535ac9a012585eb..7d0eff376354d84de066f7c9804e11a7ed8015ac 100644
--- a/app/controllers/import/gitlab_projects_controller.rb
+++ b/app/controllers/import/gitlab_projects_controller.rb
@@ -1,5 +1,6 @@
 class Import::GitlabProjectsController < Import::BaseController
   before_action :verify_gitlab_project_import_enabled
+  before_action :authenticate_admin!
 
   def new
     @namespace_id = project_params[:namespace_id]
@@ -12,13 +13,14 @@ class Import::GitlabProjectsController < Import::BaseController
       return redirect_back_or_default(options: { alert: "You need to upload a GitLab project export archive." })
     end
 
-    imported_file = project_params[:file].path + "-import"
+    import_upload_path = Gitlab::ImportExport.import_upload_path(filename: project_params[:file].original_filename)
 
-    FileUtils.copy_entry(project_params[:file].path, imported_file)
+    FileUtils.mkdir_p(File.dirname(import_upload_path))
+    FileUtils.copy_entry(project_params[:file].path, import_upload_path)
 
     @project = Gitlab::ImportExport::ProjectCreator.new(project_params[:namespace_id],
                                                         current_user,
-                                                        File.expand_path(imported_file),
+                                                        import_upload_path,
                                                         project_params[:path]).execute
 
     if @project.saved?
@@ -46,4 +48,8 @@ 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/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/profiles/passwords_controller.rb b/app/controllers/profiles/passwords_controller.rb
index c780e0983f93ae07443138cb2a42ef95828a767b..6217ec5ecef196dcb4306250d6d9543663c38b74 100644
--- a/app/controllers/profiles/passwords_controller.rb
+++ b/app/controllers/profiles/passwords_controller.rb
@@ -50,6 +50,7 @@ class Profiles::PasswordsController < Profiles::ApplicationController
       flash[:notice] = "Password was successfully updated. Please login with it"
       redirect_to new_user_session_path
     else
+      @user.reload
       render 'edit'
     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/projects/application_controller.rb b/app/controllers/projects/application_controller.rb
index 996909a28c6805156e19f26705843fad413b39a1..91315a07debcff03bba0db8f7e0578847fac0dd0 100644
--- a/app/controllers/projects/application_controller.rb
+++ b/app/controllers/projects/application_controller.rb
@@ -83,6 +83,7 @@ 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
 
diff --git a/app/controllers/projects/badges_controller.rb b/app/controllers/projects/badges_controller.rb
index a9f482c8787b0edad09127fcaa472fd8133a86b7..6c25cd83a24353c6c5321248e997691136c721f9 100644
--- a/app/controllers/projects/badges_controller.rb
+++ b/app/controllers/projects/badges_controller.rb
@@ -4,12 +4,26 @@ class Projects::BadgesController < Projects::ApplicationController
   before_action :no_cache_headers, except: [:index]
 
   def build
-    badge = Gitlab::Badge::Build.new(project, params[:ref])
+    build_status = Gitlab::Badge::Build::Status
+      .new(project, params[:ref])
 
+    render_badge build_status
+  end
+
+  def coverage
+    coverage_report = Gitlab::Badge::Coverage::Report
+      .new(project, params[:ref], params[:job])
+
+    render_badge coverage_report
+  end
+
+  private
+
+  def render_badge(badge)
     respond_to do |format|
       format.html { render_404 }
       format.svg do
-        send_data(badge.data, type: badge.type, disposition: 'inline')
+        render 'badge', locals: { badge: badge.template }
       end
     end
   end
diff --git a/app/controllers/projects/blob_controller.rb b/app/controllers/projects/blob_controller.rb
index eda3727a28de2ebc788e41fbc12f86dd46831b79..cdf9a04bacfcfb80d50e15bf590508db4e1c2988 100644
--- a/app/controllers/projects/blob_controller.rb
+++ b/app/controllers/projects/blob_controller.rb
@@ -17,6 +17,7 @@ class Projects::BlobController < Projects::ApplicationController
   before_action :require_branch_head, only: [:edit, :update]
   before_action :editor_variables, except: [:show, :preview, :diff]
   before_action :validate_diff_params, only: :diff
+  before_action :set_last_commit_sha, only: [:edit, :update]
 
   def new
     commit unless @repository.empty?
@@ -33,7 +34,6 @@ class Projects::BlobController < Projects::ApplicationController
   end
 
   def edit
-    @last_commit = Gitlab::Git::Commit.last_for_path(@repository, @ref, @path).sha
     blob.load_all_data!(@repository)
   end
 
@@ -55,6 +55,10 @@ class Projects::BlobController < Projects::ApplicationController
     create_commit(Files::UpdateService, success_path: after_edit_path,
                                         failure_view: :edit,
                                         failure_path: namespace_project_blob_path(@project.namespace, @project, @id))
+
+  rescue Files::UpdateService::FileChangedError
+    @conflict = true
+    render :edit
   end
 
   def preview
@@ -76,6 +80,8 @@ class Projects::BlobController < Projects::ApplicationController
   end
 
   def diff
+    apply_diff_view_cookie!
+
     @form  = UnfoldForm.new(params)
     @lines = Gitlab::Highlight.highlight_lines(repository, @ref, @path)
     @lines = @lines[@form.since - 1..@form.to - 1]
@@ -150,7 +156,8 @@ class Projects::BlobController < Projects::ApplicationController
       file_path: @file_path,
       commit_message: params[:commit_message],
       file_content: params[:content],
-      file_content_encoding: params[:encoding]
+      file_content_encoding: params[:encoding],
+      last_commit_sha: params[:last_commit_sha]
     }
   end
 
@@ -159,4 +166,9 @@ class Projects::BlobController < Projects::ApplicationController
       render nothing: true
     end
   end
+
+  def set_last_commit_sha
+    @last_commit_sha = Gitlab::Git::Commit.
+      last_for_path(@repository, @ref, @path).sha
+  end
 end
diff --git a/app/controllers/projects/board_lists_controller.rb b/app/controllers/projects/board_lists_controller.rb
new file mode 100644
index 0000000000000000000000000000000000000000..3cfb08d5822ee44eb5089ee7a39c52e1d1246955
--- /dev/null
+++ b/app/controllers/projects/board_lists_controller.rb
@@ -0,0 +1,65 @@
+class Projects::BoardListsController < Projects::ApplicationController
+  respond_to :json
+
+  before_action :authorize_admin_list!
+
+  rescue_from ActiveRecord::RecordNotFound, with: :record_not_found
+
+  def create
+    list = Boards::Lists::CreateService.new(project, current_user, list_params).execute
+
+    if list.valid?
+      render json: list.as_json(only: [:id, :list_type, :position], methods: [:title], include: { label: { only: [:id, :title, :description, :color, :priority] } })
+    else
+      render json: list.errors, status: :unprocessable_entity
+    end
+  end
+
+  def update
+    service = Boards::Lists::MoveService.new(project, current_user, move_params)
+
+    if service.execute
+      head :ok
+    else
+      head :unprocessable_entity
+    end
+  end
+
+  def destroy
+    service = Boards::Lists::DestroyService.new(project, current_user, params)
+
+    if service.execute
+      head :ok
+    else
+      head :unprocessable_entity
+    end
+  end
+
+  def generate
+    service = Boards::Lists::GenerateService.new(project, current_user)
+
+    if service.execute
+      render json: project.board.lists.label.as_json(only: [:id, :list_type, :position], methods: [:title], include: { label: { only: [:id, :title, :description, :color, :priority] } })
+    else
+      head :unprocessable_entity
+    end
+  end
+
+  private
+
+  def authorize_admin_list!
+    return render_403 unless can?(current_user, :admin_list, project)
+  end
+
+  def list_params
+    params.require(:list).permit(:label_id)
+  end
+
+  def move_params
+    params.require(:list).permit(:position).merge(id: params[:id])
+  end
+
+  def record_not_found(exception)
+    render json: { error: exception.message }, status: :not_found
+  end
+end
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..1a4f6b50e8f4b2fe72f2354d65c47be8711acb0a
--- /dev/null
+++ b/app/controllers/projects/boards/issues_controller.rb
@@ -0,0 +1,56 @@
+module Projects
+  module Boards
+    class IssuesController < Boards::ApplicationController
+      before_action :authorize_read_issue!, only: [:index]
+      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.as_json(
+          only: [:iid, :title, :confidential],
+          include: {
+            assignee: { only: [:id, :name, :username], methods: [:avatar_url] },
+            labels:   { only: [:id, :title, :description, :color, :priority], methods: [:text_color] }
+          })
+      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, state: 'all')
+                      .execute
+                      .where(iid: params[:id])
+                      .first!
+      end
+
+      def authorize_read_issue!
+        return render_403 unless can?(current_user, :read_issue, project)
+      end
+
+      def authorize_update_issue!
+        return render_403 unless can?(current_user, :update_issue, issue)
+      end
+
+      def filter_params
+        params.merge(id: params[:list_id])
+      end
+
+      def move_params
+        params.permit(:id, :from_list_id, :to_list_id)
+      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..b995f58673710cbd6ca8ae86a806df93d9ba623a
--- /dev/null
+++ b/app/controllers/projects/boards/lists_controller.rb
@@ -0,0 +1,81 @@
+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(project.board.lists)
+      end
+
+      def create
+        list = ::Boards::Lists::CreateService.new(project, current_user, list_params).execute
+
+        if list.valid?
+          render json: serialize_as_json(list)
+        else
+          render json: list.errors, status: :unprocessable_entity
+        end
+      end
+
+      def update
+        list = project.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 = project.board.lists.destroyable.find(params[:id])
+        service = ::Boards::Lists::DestroyService.new(project, current_user, params)
+
+        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
+          render json: serialize_as_json(project.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 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],
+          include: {
+            label: { only: [:id, :title, :description, :color, :priority] }
+          })
+      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..3320671708983bc8c6d11c64abf96444c355836f
--- /dev/null
+++ b/app/controllers/projects/boards_controller.rb
@@ -0,0 +1,15 @@
+class Projects::BoardsController < Projects::ApplicationController
+  respond_to :html
+
+  before_action :authorize_read_board!, only: [:show]
+
+  def show
+    ::Boards::CreateService.new(project, current_user).execute
+  end
+
+  private
+
+  def authorize_read_board!
+    return access_denied! unless can?(current_user, :read_board, project)
+  end
+end
diff --git a/app/controllers/projects/branches_controller.rb b/app/controllers/projects/branches_controller.rb
index e926043f3ebadbcf21721efa2430daa852f23409..2de8ada3e29604cf787c3d879697ffeae2cd66d0 100644
--- a/app/controllers/projects/branches_controller.rb
+++ b/app/controllers/projects/branches_controller.rb
@@ -1,12 +1,13 @@
 class Projects::BranchesController < Projects::ApplicationController
   include ActionView::Helpers::SanitizeHelper
+  include SortingHelper
   # Authorize
   before_action :require_non_empty_project
   before_action :authorize_download_code!
   before_action :authorize_push_code!, only: [:new, :create, :destroy]
 
   def index
-    @sort = params[:sort].presence || 'name'
+    @sort = params[:sort].presence || sort_value_name
     @branches = BranchesFinder.new(@repository, params).execute
     @branches = Kaminari.paginate_array(@branches).page(params[:page])
 
@@ -14,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 553b62741a5de091ad536aa467686d2ac1794014..12195c3cbb82704fd93d8648909551a1a75e5f19 100644
--- a/app/controllers/projects/builds_controller.rb
+++ b/app/controllers/projects/builds_controller.rb
@@ -6,7 +6,7 @@ class Projects::BuildsController < Projects::ApplicationController
 
   def index
     @scope = params[:scope]
-    @all_builds = project.builds
+    @all_builds = project.builds.relevant
     @builds = @all_builds.order('created_at DESC')
     @builds =
       case @scope
diff --git a/app/controllers/projects/commit_controller.rb b/app/controllers/projects/commit_controller.rb
index 7ae034f9398ce360f86fbe801031809189898f70..02fb3f568905626aa5f97bc5ae8ca335cfcd21db 100644
--- a/app/controllers/projects/commit_controller.rb
+++ b/app/controllers/projects/commit_controller.rb
@@ -28,7 +28,7 @@ class Projects::CommitController < Projects::ApplicationController
   end
 
   def diff_for_path
-    render_diff_for_path(@diffs, @commit.diff_refs, @project)
+    render_diff_for_path(@commit.diffs(diff_options))
   end
 
   def builds
@@ -93,7 +93,7 @@ class Projects::CommitController < Projects::ApplicationController
   end
 
   def commit
-    @commit ||= @project.commit(params[:id])
+    @noteable = @commit ||= @project.commit(params[:id])
   end
 
   def pipelines
@@ -134,8 +134,8 @@ class Projects::CommitController < Projects::ApplicationController
   end
 
   def define_status_vars
-    @statuses = CommitStatus.where(pipeline: pipelines)
-    @builds = Ci::Build.where(pipeline: pipelines)
+    @statuses = CommitStatus.where(pipeline: pipelines).relevant
+    @builds = Ci::Build.where(pipeline: pipelines).relevant
   end
 
   def assign_change_commit_vars(mr_source_branch)
diff --git a/app/controllers/projects/compare_controller.rb b/app/controllers/projects/compare_controller.rb
index 8c004724f02561ed9260b2da53acfc17c3182dcd..bee3d56076c6c2232849261239fad018bcb0335e 100644
--- a/app/controllers/projects/compare_controller.rb
+++ b/app/controllers/projects/compare_controller.rb
@@ -21,7 +21,7 @@ class Projects::CompareController < Projects::ApplicationController
   def diff_for_path
     return render_404 unless @compare
 
-    render_diff_for_path(@diffs, @diff_refs, @project)
+    render_diff_for_path(@compare.diffs(diff_options))
   end
 
   def create
@@ -40,18 +40,12 @@ class Projects::CompareController < Projects::ApplicationController
     @compare = CompareService.new.execute(@project, @head_ref, @project, @start_ref)
 
     if @compare
-      @commits = Commit.decorate(@compare.commits, @project)
-
-      @start_commit = @project.commit(@start_ref)
-      @commit = @project.commit(@head_ref)
-      @base_commit = @project.merge_base_commit(@start_ref, @head_ref)
+      @commits = @compare.commits
+      @start_commit = @compare.start_commit
+      @commit = @compare.commit
+      @base_commit = @compare.base_commit
 
       @diffs = @compare.diffs(diff_options)
-      @diff_refs = Gitlab::Diff::DiffRefs.new(
-        base_sha: @base_commit.try(:sha),
-        start_sha: @start_commit.try(:sha),
-        head_sha: @commit.try(:sha)
-      )
 
       @diff_notes_disabled = true
       @grouped_diff_discussions = {}
diff --git a/app/controllers/projects/deploy_keys_controller.rb b/app/controllers/projects/deploy_keys_controller.rb
index 83d5ced9be8b60add2c32c5a950a337cac3cdd3e..529e0aa2d33e13c96037fc8f6e2b05e40608ad6a 100644
--- a/app/controllers/projects/deploy_keys_controller.rb
+++ b/app/controllers/projects/deploy_keys_controller.rb
@@ -12,8 +12,7 @@ class Projects::DeployKeysController < Projects::ApplicationController
   end
 
   def new
-    redirect_to namespace_project_deploy_keys_path(@project.namespace,
-                                                   @project)
+    redirect_to namespace_project_deploy_keys_path(@project.namespace, @project)
   end
 
   def create
@@ -21,19 +20,16 @@ class Projects::DeployKeysController < Projects::ApplicationController
     set_index_vars
 
     if @key.valid? && @project.deploy_keys << @key
-      redirect_to namespace_project_deploy_keys_path(@project.namespace,
-                                                     @project)
+      redirect_to namespace_project_deploy_keys_path(@project.namespace, @project)
     else
       render "index"
     end
   end
 
   def enable
-    @key = accessible_keys.find(params[:id])
-    @project.deploy_keys << @key
+    Projects::EnableDeployKeyService.new(@project, current_user, params).execute
 
-    redirect_to namespace_project_deploy_keys_path(@project.namespace,
-                                                   @project)
+    redirect_to namespace_project_deploy_keys_path(@project.namespace, @project)
   end
 
   def disable
@@ -45,9 +41,9 @@ class Projects::DeployKeysController < Projects::ApplicationController
   protected
 
   def set_index_vars
-    @enabled_keys ||= @project.deploy_keys
+    @enabled_keys           ||= @project.deploy_keys
 
-    @available_keys         ||= accessible_keys - @enabled_keys
+    @available_keys         ||= current_user.accessible_deploy_keys - @enabled_keys
     @available_project_keys ||= current_user.project_deploy_keys - @enabled_keys
     @available_public_keys  ||= DeployKey.are_public - @enabled_keys
 
@@ -56,10 +52,6 @@ class Projects::DeployKeysController < Projects::ApplicationController
     @available_public_keys -= @available_project_keys
   end
 
-  def accessible_keys
-    @accessible_keys ||= current_user.accessible_deploy_keys
-  end
-
   def deploy_key_params
     params.require(:deploy_key).permit(:key, :title)
   end
diff --git a/app/controllers/projects/discussions_controller.rb b/app/controllers/projects/discussions_controller.rb
new file mode 100644
index 0000000000000000000000000000000000000000..b2e8733ccb741c1411d8a424238c06285b594993
--- /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.merge_requests_enabled
+  end
+end
diff --git a/app/controllers/projects/git_http_client_controller.rb b/app/controllers/projects/git_http_client_controller.rb
new file mode 100644
index 0000000000000000000000000000000000000000..a5b4031c30f8e2e09551570881cff71acd9d282a
--- /dev/null
+++ b/app/controllers/projects/git_http_client_controller.rb
@@ -0,0 +1,120 @@
+# This file should be identical in GitLab Community Edition and Enterprise Edition
+
+class Projects::GitHttpClientController < Projects::ApplicationController
+  include ActionController::HttpAuthentication::Basic
+  include KerberosSpnegoHelper
+
+  attr_reader :user
+
+  # Git clients will not know what authenticity token to send along
+  skip_before_action :verify_authenticity_token
+  skip_before_action :repository
+  before_action :authenticate_user
+  before_action :ensure_project_found!
+
+  private
+
+  def authenticate_user
+    if project && project.public? && download_request?
+      return # Allow access
+    end
+
+    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
+      elsif auth_result.type == :missing_personal_token
+        render_missing_personal_token
+        return # Render above denied access, nothing left to do
+      else
+        @user = auth_result.user
+      end
+
+      if ci? || user
+        return # Allow access
+      end
+    elsif allow_kerberos_spnego_auth? && spnego_provided?
+      @user = find_kerberos_user
+
+      if user
+        send_final_spnego_response
+        return # Allow access
+      end
+    end
+
+    send_challenges
+    render plain: "HTTP Basic: Access denied\n", status: 401
+  end
+
+  def basic_auth_provided?
+    has_basic_credentials?(request)
+  end
+
+  def send_challenges
+    challenges = []
+    challenges << 'Basic realm="GitLab"' if allow_basic_auth?
+    challenges << spnego_challenge if allow_kerberos_spnego_auth?
+    headers['Www-Authenticate'] = challenges.join("\n") if challenges.any?
+  end
+
+  def ensure_project_found!
+    render_not_found if project.blank?
+  end
+
+  def project
+    return @project if defined?(@project)
+
+    project_id, _ = project_id_with_suffix
+    if project_id.blank?
+      @project = nil
+    else
+      @project = Project.find_with_namespace("#{params[:namespace_id]}/#{project_id}")
+    end
+  end
+
+  # This method returns two values so that we can parse
+  # params[:project_id] (untrusted input!) in exactly one place.
+  def project_id_with_suffix
+    id = params[:project_id] || ''
+
+    %w[.wiki.git .git].each do |suffix|
+      if id.end_with?(suffix)
+        # Be careful to only remove the suffix from the end of 'id'.
+        # Accidentally removing it from the middle is how security
+        # vulnerabilities happen!
+        return [id.slice(0, id.length - suffix.length), suffix]
+      end
+    end
+
+    # Something is wrong with params[:project_id]; do not pass it on.
+    [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'
+      project.wiki.repository
+    else
+      project.repository
+    end
+  end
+
+  def render_not_found
+    render plain: 'Not Found', status: :not_found
+  end
+
+  def ci?
+    @ci.present?
+  end
+end
diff --git a/app/controllers/projects/git_http_controller.rb b/app/controllers/projects/git_http_controller.rb
index 40a8b7940d9eb1a1e7d1b1c80cb1b26a50918886..b4373ef89efa6575ddadb01311c627d3762346e8 100644
--- a/app/controllers/projects/git_http_controller.rb
+++ b/app/controllers/projects/git_http_controller.rb
@@ -1,17 +1,6 @@
 # This file should be identical in GitLab Community Edition and Enterprise Edition
 
-class Projects::GitHttpController < Projects::ApplicationController
-  include ActionController::HttpAuthentication::Basic
-  include KerberosSpnegoHelper
-
-  attr_reader :user
-
-  # Git clients will not know what authenticity token to send along
-  skip_before_action :verify_authenticity_token
-  skip_before_action :repository
-  before_action :authenticate_user
-  before_action :ensure_project_found!
-
+class Projects::GitHttpController < Projects::GitHttpClientController
   # 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
@@ -20,9 +9,9 @@ class Projects::GitHttpController < Projects::ApplicationController
     elsif receive_pack? && receive_pack_allowed?
       render_ok
     elsif http_blocked?
-      render_not_allowed
+      render_http_not_allowed
     else
-      render_not_found
+      render_denied
     end
   end
 
@@ -31,7 +20,7 @@ class Projects::GitHttpController < Projects::ApplicationController
     if upload_pack? && upload_pack_allowed?
       render_ok
     else
-      render_not_found
+      render_denied
     end
   end
 
@@ -40,87 +29,14 @@ class Projects::GitHttpController < Projects::ApplicationController
     if receive_pack? && receive_pack_allowed?
       render_ok
     else
-      render_not_found
+      render_denied
     end
   end
 
   private
 
-  def authenticate_user
-    if project && project.public? && upload_pack?
-      return # Allow access
-    end
-
-    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 && upload_pack?
-        @ci = true
-      elsif auth_result.type == :oauth && !upload_pack?
-        # Not allowed
-      else
-        @user = auth_result.user
-      end
-
-      if ci? || user
-        return # Allow access
-      end
-    elsif allow_kerberos_spnego_auth? && spnego_provided?
-      @user = find_kerberos_user
-
-      if user
-        send_final_spnego_response
-        return # Allow access
-      end
-    end
-
-    send_challenges
-    render plain: "HTTP Basic: Access denied\n", status: 401
-  end
-
-  def basic_auth_provided?
-    has_basic_credentials?(request)
-  end
-
-  def send_challenges
-    challenges = []
-    challenges << 'Basic realm="GitLab"' if allow_basic_auth?
-    challenges << spnego_challenge if allow_kerberos_spnego_auth?
-    headers['Www-Authenticate'] = challenges.join("\n") if challenges.any?
-  end
-
-  def ensure_project_found!
-    render_not_found if project.blank?
-  end
-
-  def project
-    return @project if defined?(@project)
-
-    project_id, _ = project_id_with_suffix
-    if project_id.blank?
-      @project = nil
-    else
-      @project = Project.find_with_namespace("#{params[:namespace_id]}/#{project_id}")
-    end
-  end
-
-  # This method returns two values so that we can parse
-  # params[:project_id] (untrusted input!) in exactly one place.
-  def project_id_with_suffix
-    id = params[:project_id] || ''
-
-    %w[.wiki.git .git].each do |suffix|
-      if id.end_with?(suffix)
-        # Be careful to only remove the suffix from the end of 'id'.
-        # Accidentally removing it from the middle is how security
-        # vulnerabilities happen!
-        return [id.slice(0, id.length - suffix.length), suffix]
-      end
-    end
-
-    # Something is wrong with params[:project_id]; do not pass it on.
-    [nil, nil]
+  def download_request?
+    upload_pack?
   end
 
   def upload_pack?
@@ -143,47 +59,37 @@ class Projects::GitHttpController < Projects::ApplicationController
     render json: Gitlab::Workhorse.git_http_ok(repository, user)
   end
 
-  def repository
-    _, suffix = project_id_with_suffix
-    if suffix == '.wiki.git'
-      project.wiki.repository
-    else
-      project.repository
-    end
-  end
-
-  def render_not_found
-    render plain: 'Not Found', status: :not_found
+  def render_http_not_allowed
+    render plain: access_check.message, status: :forbidden
   end
 
-  def render_not_allowed
-    render plain: download_access.message, status: :forbidden
-  end
-
-  def ci?
-    @ci.present?
+  def render_denied
+    if user && user.can?(:read_project, project)
+      render plain: 'Access denied', status: :forbidden
+    else
+      # Do not leak information about project existence
+      render_not_found
+    end
   end
 
   def upload_pack_allowed?
     return false unless Gitlab.config.gitlab_shell.upload_pack
 
     if user
-      download_access.allowed?
+      access_check.allowed?
     else
       ci? || project.public?
     end
   end
 
   def access
-    return @access if defined?(@access)
-
-    @access = Gitlab::GitAccess.new(user, project, 'http')
+    @access ||= Gitlab::GitAccess.new(user, project, 'http')
   end
 
-  def download_access
-    return @download_access if defined?(@download_access)
-
-    @download_access = access.check('git-upload-pack')
+  def access_check
+    # Use the magic string '_any' to indicate we do not know what the
+    # changes are. This is also what gitlab-shell does.
+    @access_check ||= access.check(git_command, '_any')
   end
 
   def http_blocked?
@@ -193,8 +99,6 @@ class Projects::GitHttpController < Projects::ApplicationController
   def receive_pack_allowed?
     return false unless Gitlab.config.gitlab_shell.receive_pack
 
-    # Skip user authorization on upload request.
-    # It will be done by the pre-receive hook in the repository.
-    user.present?
+    access_check.allowed?
   end
 end
diff --git a/app/controllers/projects/group_links_controller.rb b/app/controllers/projects/group_links_controller.rb
index 606552fa85322b5486c8aa930fdf34c459125c3c..d0c4550733c6ddae4629ab0804e40e0f98897809 100644
--- a/app/controllers/projects/group_links_controller.rb
+++ b/app/controllers/projects/group_links_controller.rb
@@ -11,7 +11,9 @@ class Projects::GroupLinksController < Projects::ApplicationController
     return render_404 unless can?(current_user, :read_group, group)
 
     project.project_group_links.create(
-      group: group, group_access: params[:link_group_access]
+      group: group,
+      group_access: params[:link_group_access],
+      expires_at: params[:expires_at]
     )
 
     redirect_to namespace_project_group_links_path(project.namespace, project)
diff --git a/app/controllers/projects/issues_controller.rb b/app/controllers/projects/issues_controller.rb
index 7f5c3ff3d6a25dc2db917992150f399e54baea18..7b0189150f801c64d148a620a41f885202170af7 100644
--- a/app/controllers/projects/issues_controller.rb
+++ b/app/controllers/projects/issues_controller.rb
@@ -4,7 +4,9 @@ class Projects::IssuesController < Projects::ApplicationController
   include IssuableActions
   include ToggleAwardEmoji
   include IssuableCollections
+  include SpammableActions
 
+  before_action :redirect_to_external_issue_tracker, only: [:index, :new]
   before_action :module_enabled
   before_action :issue, only: [:edit, :update, :show, :referenced_merge_requests,
                                :related_branches, :can_create_branch]
@@ -123,6 +125,10 @@ class Projects::IssuesController < Projects::ApplicationController
         render json: @issue.to_json(include: { milestone: {}, assignee: { methods: :avatar_url }, labels: { methods: :text_color } })
       end
     end
+
+  rescue ActiveRecord::StaleObjectError
+    @conflict = true
+    render :edit
   end
 
   def referenced_merge_requests
@@ -175,15 +181,12 @@ class Projects::IssuesController < Projects::ApplicationController
   protected
 
   def issue
-    @issue ||= begin
-                 @project.issues.find_by!(iid: params[:id])
-               rescue ActiveRecord::RecordNotFound
-                 redirect_old
-               end
+    @noteable = @issue ||= @project.issues.find_by(iid: params[:id]) || redirect_old
   end
   alias_method :subscribable_resource, :issue
   alias_method :issuable, :issue
   alias_method :awardable, :issue
+  alias_method :spammable, :issue
 
   def authorize_read_issue!
     return render_404 unless can?(current_user, :read_issue, @issue)
@@ -201,6 +204,18 @@ class Projects::IssuesController < Projects::ApplicationController
     return render_404 unless @project.issues_enabled && @project.default_issues_tracker?
   end
 
+  def redirect_to_external_issue_tracker
+    external = @project.external_issue_tracker
+
+    return unless external
+
+    if action_name == 'new'
+      redirect_to external.new_issue_path
+    else
+      redirect_to external.issues_url
+    end
+  end
+
   # Since iids are implemented only in 6.1
   # user may navigate to issue page using old global ids.
   #
@@ -211,7 +226,6 @@ class Projects::IssuesController < Projects::ApplicationController
 
     if issue
       redirect_to issue_path(issue)
-      return
     else
       raise ActiveRecord::RecordNotFound.new
     end
@@ -220,7 +234,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: []
+      :milestone_id, :due_date, :state_event, :task_num, :lock_version, label_ids: []
     )
   end
 
diff --git a/app/controllers/projects/lfs_api_controller.rb b/app/controllers/projects/lfs_api_controller.rb
new file mode 100644
index 0000000000000000000000000000000000000000..ece49dcd92257209cda06b7775772a1898feff6b
--- /dev/null
+++ b/app/controllers/projects/lfs_api_controller.rb
@@ -0,0 +1,94 @@
+class Projects::LfsApiController < Projects::GitHttpClientController
+  include LfsHelper
+
+  before_action :require_lfs_enabled!
+  before_action :lfs_check_access!, except: [:deprecated]
+
+  def batch
+    unless objects.present?
+      render_lfs_not_found
+      return
+    end
+
+    if download_request?
+      render json: { objects: download_objects! }
+    elsif upload_request?
+      render json: { objects: upload_objects! }
+    else
+      raise "Never reached"
+    end
+  end
+
+  def deprecated
+    render(
+      json: {
+        message: 'Server supports batch API only, please update your Git LFS client to version 1.0.1 and up.',
+        documentation_url: "#{Gitlab.config.gitlab.url}/help",
+      },
+      status: 501
+    )
+  end
+
+  private
+
+  def objects
+    @objects ||= (params[:objects] || []).to_a
+  end
+
+  def existing_oids
+    @existing_oids ||= begin
+      storage_project.lfs_objects.where(oid: objects.map { |o| o['oid'].to_s }).pluck(:oid)
+    end
+  end
+
+  def download_objects!
+    objects.each do |object|
+      if existing_oids.include?(object[:oid])
+        object[:actions] = download_actions(object)
+      else
+        object[:error] = {
+          code: 404,
+          message: "Object does not exist on the server or you don't have permissions to access it",
+        }
+      end
+    end
+    objects
+  end
+
+  def upload_objects!
+    objects.each do |object|
+      object[:actions] = upload_actions(object) unless existing_oids.include?(object[:oid])
+    end
+    objects
+  end
+
+  def download_actions(object)
+    {
+      download: {
+        href: "#{project.http_url_to_repo}/gitlab-lfs/objects/#{object[:oid]}",
+        header: {
+          Authorization: request.headers['Authorization']
+        }.compact
+      }
+    }
+  end
+
+  def upload_actions(object)
+    {
+      upload: {
+        href: "#{project.http_url_to_repo}/gitlab-lfs/objects/#{object[:oid]}/#{object[:size]}",
+        header: {
+          Authorization: request.headers['Authorization']
+        }.compact
+      }
+    }
+  end
+
+  def download_request?
+    params[:operation] == 'download'
+  end
+
+  def upload_request?
+    params[:operation] == 'upload'
+  end
+end
diff --git a/app/controllers/projects/lfs_storage_controller.rb b/app/controllers/projects/lfs_storage_controller.rb
new file mode 100644
index 0000000000000000000000000000000000000000..69066cb40e671286810267fb4650699bf66aea58
--- /dev/null
+++ b/app/controllers/projects/lfs_storage_controller.rb
@@ -0,0 +1,92 @@
+class Projects::LfsStorageController < Projects::GitHttpClientController
+  include LfsHelper
+
+  before_action :require_lfs_enabled!
+  before_action :lfs_check_access!
+
+  def download
+    lfs_object = LfsObject.find_by_oid(oid)
+    unless lfs_object && lfs_object.file.exists?
+      render_lfs_not_found
+      return
+    end
+
+    send_file lfs_object.file.path, content_type: "application/octet-stream"
+  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'
+    )
+  end
+
+  def upload_finalize
+    unless tmp_filename
+      render_lfs_forbidden
+      return
+    end
+
+    if store_file(oid, size, tmp_filename)
+      head 200
+    else
+      render plain: 'Unprocessable entity', status: 422
+    end
+  end
+
+  private
+
+  def download_request?
+    action_name == 'download'
+  end
+
+  def upload_request?
+    %w[upload_authorize upload_finalize].include? action_name
+  end
+
+  def oid
+    params[:oid].to_s
+  end
+
+  def size
+    params[:size].to_i
+  end
+
+  def tmp_filename
+    name = request.headers['X-Gitlab-Lfs-Tmp']
+    return if name.include?('/')
+    return unless oid.present? && name.start_with?(oid)
+    name
+  end
+
+  def store_file(oid, size, tmp_file)
+    # Define tmp_file_path early because we use it in "ensure"
+    tmp_file_path = File.join("#{Gitlab.config.lfs.storage_path}/tmp/upload", tmp_file)
+
+    object = LfsObject.find_or_create_by(oid: oid, size: size)
+    file_exists = object.file.exists? || move_tmp_file_to_storage(object, tmp_file_path)
+    file_exists && link_to_project(object)
+  ensure
+    FileUtils.rm_f(tmp_file_path)
+  end
+
+  def move_tmp_file_to_storage(object, path)
+    File.open(path) do |f|
+      object.file = f
+    end
+
+    object.file.store!
+    object.save
+  end
+
+  def link_to_project(object)
+    if object && !object.projects.exists?(storage_project.id)
+      object.projects << storage_project
+      object.save
+    end
+  end
+end
diff --git a/app/controllers/projects/merge_requests_controller.rb b/app/controllers/projects/merge_requests_controller.rb
index 03166294ddd8d7a6b46649ddfe066a7dd04eac6a..6a8c7166b395ba724ca3b568e61ff4f7a71a911c 100644
--- a/app/controllers/projects/merge_requests_controller.rb
+++ b/app/controllers/projects/merge_requests_controller.rb
@@ -9,15 +9,15 @@ 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, :builds, :pipelines, :merge, :merge_check,
+    :ci_status, :toggle_subscription, :cancel_merge_when_build_succeeds, :remove_wip, :resolve_conflicts
   ]
-  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, :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, :pipelines]
 
   # Allow read any merge_request
   before_action :authorize_read_merge_request!
@@ -28,6 +28,8 @@ class Projects::MergeRequestsController < Projects::ApplicationController
   # Allow modify merge_request
   before_action :authorize_update_merge_request!, only: [:close, :edit, :update, :remove_wip, :sort]
 
+  before_action :authorize_can_resolve_conflicts!, only: [:conflicts, :resolve_conflicts]
+
   def index
     terms = params['issue_search']
     @merge_requests = merge_requests_collection
@@ -85,7 +87,11 @@ class Projects::MergeRequestsController < Projects::ApplicationController
 
     respond_to do |format|
       format.html { define_discussion_vars }
-      format.json { render json: { html: view_to_html_string("projects/merge_requests/show/_diffs") } }
+      format.json do
+        @diffs = @merge_request.diffs(diff_options)
+
+        render json: { html: view_to_html_string("projects/merge_requests/show/_diffs") }
+      end
     end
   end
 
@@ -103,9 +109,8 @@ class Projects::MergeRequestsController < Projects::ApplicationController
     end
 
     define_commit_vars
-    diffs = @merge_request.diffs(diff_options)
 
-    render_diff_for_path(diffs, @merge_request.diff_refs, @merge_request.project)
+    render_diff_for_path(@merge_request.diffs(diff_options))
   end
 
   def commits
@@ -127,6 +132,47 @@ 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 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::File::MissingResolution => e
+      render status: :bad_request, json: { message: e.message }
+    end
+  end
+
   def builds
     respond_to do |format|
       format.html do
@@ -138,7 +184,22 @@ class Projects::MergeRequestsController < Projects::ApplicationController
     end
   end
 
+  def pipelines
+    @pipelines = @merge_request.all_pipelines
+
+    respond_to do |format|
+      format.html do
+        define_discussion_vars
+
+        render 'show'
+      end
+      format.json { render json: { html: view_to_html_string('projects/merge_requests/show/_pipelines') } }
+    end
+  end
+
   def new
+    apply_diff_view_cookie!
+
     build_merge_request
     @noteable = @merge_request
 
@@ -153,11 +214,10 @@ class Projects::MergeRequestsController < Projects::ApplicationController
     @commits = @merge_request.compare_commits.reverse
     @commit = @merge_request.diff_head_commit
     @base_commit = @merge_request.diff_base_commit
-    @diffs = @merge_request.compare.diffs(diff_options) if @merge_request.compare
+    @diffs = @merge_request.diffs(diff_options) if @merge_request.compare
     @diff_notes_disabled = true
-
     @pipeline = @merge_request.pipeline
-    @statuses = @pipeline.statuses if @pipeline
+    @statuses = @pipeline.statuses.relevant if @pipeline
 
     @note_counts = Note.where(commit_id: @commits.map(&:id)).
       group(:commit_id).count
@@ -198,6 +258,9 @@ class Projects::MergeRequestsController < Projects::ApplicationController
     else
       render "edit"
     end
+  rescue ActiveRecord::StaleObjectError
+    @conflict = true
+    render :edit
   end
 
   def remove_wip
@@ -321,7 +384,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
@@ -335,6 +398,10 @@ 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
   end
@@ -359,7 +426,7 @@ class Projects::MergeRequestsController < Projects::ApplicationController
     @commits_count = @merge_request.commits.count
 
     @pipeline = @merge_request.pipeline
-    @statuses = @pipeline.statuses if @pipeline
+    @statuses = @pipeline.statuses.relevant if @pipeline
 
     if @merge_request.locked_long_ago?
       @merge_request.unlock_mr
@@ -371,12 +438,11 @@ class Projects::MergeRequestsController < Projects::ApplicationController
   # :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 = @merge_request.discussions
 
-    @discussions = @noteable.mr_and_commit_notes.
-      inc_author_project_award_emoji.
-      fresh.
-      discussions
+    preload_noteable_for_regular_notes(@discussions.flat_map(&:notes))
 
     # This is not executed lazily
     @notes = Banzai::NoteRenderer.render(
@@ -407,8 +473,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),
@@ -430,7 +496,7 @@ class Projects::MergeRequestsController < Projects::ApplicationController
       :title, :assignee_id, :source_project_id, :source_branch,
       :target_project_id, :target_branch, :milestone_id,
       :state_event, :description, :task_num, :force_remove_source_branch,
-      label_ids: []
+      :lock_version, label_ids: []
     )
   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 487963fdcd7b712721bd3640c9aae714bf6c7d56..b0c72cfe4b4fd6da6e4cb569eccb0b99ccecb359 100644
--- a/app/controllers/projects/pipelines_controller.rb
+++ b/app/controllers/projects/pipelines_controller.rb
@@ -19,7 +19,7 @@ class Projects::PipelinesController < Projects::ApplicationController
   end
 
   def create
-    @pipeline = Ci::CreatePipelineService.new(project, current_user, create_params).execute
+    @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/pipelines_settings_controller.rb b/app/controllers/projects/pipelines_settings_controller.rb
index 85ba706e5cd050eeb19e074a8141611d344e765a..9136633b87a149f6a31947733820145dba43eeeb 100644
--- a/app/controllers/projects/pipelines_settings_controller.rb
+++ b/app/controllers/projects/pipelines_settings_controller.rb
@@ -3,7 +3,13 @@ class Projects::PipelinesSettingsController < Projects::ApplicationController
 
   def show
     @ref = params[:ref] || @project.default_branch || 'master'
-    @build_badge = Gitlab::Badge::Build.new(@project, @ref)
+
+    @badges = [Gitlab::Badge::Build::Status,
+               Gitlab::Badge::Coverage::Report]
+
+    @badges.map! do |badge|
+      badge.new(@project, @ref).metadata
+    end
   end
 
   def update
diff --git a/app/controllers/projects/project_members_controller.rb b/app/controllers/projects/project_members_controller.rb
index 3435a1189647e71c6eb2b20890f3caa86e6fffeb..42a7e5a2c30d6fe3e0bfa9ef8a16738cf59f3ca2 100644
--- a/app/controllers/projects/project_members_controller.rb
+++ b/app/controllers/projects/project_members_controller.rb
@@ -36,7 +36,12 @@ class Projects::ProjectMembersController < Projects::ApplicationController
   end
 
   def create
-    @project.team.add_users(params[:user_ids].split(','), params[:access_level], current_user)
+    @project.team.add_users(
+      params[:user_ids].split(','),
+      params[:access_level],
+      expires_at: params[:expires_at],
+      current_user: current_user
+    )
 
     redirect_to namespace_project_project_members_path(@project.namespace, @project)
   end
@@ -94,7 +99,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/protected_branches_controller.rb b/app/controllers/projects/protected_branches_controller.rb
index d28ec6e2eacc06d20f3d73fecf2bc8734743f5ca..9a438d5512c7670c7c77202e6bd30143440c7c9e 100644
--- a/app/controllers/projects/protected_branches_controller.rb
+++ b/app/controllers/projects/protected_branches_controller.rb
@@ -9,16 +9,16 @@ class Projects::ProtectedBranchesController < Projects::ApplicationController
 
   def index
     @protected_branch = @project.protected_branches.new
-    load_protected_branches_gon_variables
+    load_gon_index
   end
 
   def create
-    @protected_branch = ProtectedBranches::CreateService.new(@project, current_user, protected_branch_params).execute
+    @protected_branch = ::ProtectedBranches::CreateService.new(@project, current_user, protected_branch_params).execute
     if @protected_branch.persisted?
       redirect_to namespace_project_protected_branches_path(@project.namespace, @project)
     else
       load_protected_branches
-      load_protected_branches_gon_variables
+      load_gon_index
       render :index
     end
   end
@@ -28,7 +28,7 @@ class Projects::ProtectedBranchesController < Projects::ApplicationController
   end
 
   def update
-    @protected_branch = ProtectedBranches::UpdateService.new(@project, current_user, protected_branch_params).execute(@protected_branch)
+    @protected_branch = ::ProtectedBranches::UpdateService.new(@project, current_user, protected_branch_params).execute(@protected_branch)
 
     if @protected_branch.valid?
       respond_to do |format|
@@ -58,17 +58,23 @@ class Projects::ProtectedBranchesController < Projects::ApplicationController
 
   def protected_branch_params
     params.require(:protected_branch).permit(:name,
-                                             merge_access_level_attributes: [:access_level],
-                                             push_access_level_attributes: [:access_level])
+                                             merge_access_levels_attributes: [:access_level, :id],
+                                             push_access_levels_attributes: [:access_level, :id])
   end
 
   def load_protected_branches
     @protected_branches = @project.protected_branches.order(:name).page(params[:page])
   end
 
-  def load_protected_branches_gon_variables
-    gon.push({ open_branches: @project.open_branches.map { |br| { text: br.name, id: br.name, title: br.name } },
-               push_access_levels: ProtectedBranch::PushAccessLevel.human_access_levels.map { |id, text| { id: id, text: text } },
-               merge_access_levels: ProtectedBranch::MergeAccessLevel.human_access_levels.map { |id, text| { id: id, text: text } } })
+  def access_levels_options
+    {
+      push_access_levels: ProtectedBranch::PushAccessLevel.human_access_levels.map { |id, text| { id: id, text: text, before_divider: true } },
+      merge_access_levels: ProtectedBranch::MergeAccessLevel.human_access_levels.map { |id, text| { id: id, text: text, before_divider: true } }
+    }
+  end
+
+  def load_gon_index
+    params = { open_branches: @project.open_branches.map { |br| { text: br.name, id: br.name, title: br.name } } }
+    gon.push(params.merge(access_levels_options))
   end
 end
diff --git a/app/controllers/projects/templates_controller.rb b/app/controllers/projects/templates_controller.rb
new file mode 100644
index 0000000000000000000000000000000000000000..694b468c8d37c775cc4cfcb19ba0b18966b25017
--- /dev/null
+++ b/app/controllers/projects/templates_controller.rb
@@ -0,0 +1,19 @@
+class Projects::TemplatesController < Projects::ApplicationController
+  before_action :authenticate_user!, :get_template_class
+
+  def show
+    template = @template_type.find(params[:key], project)
+
+    respond_to do |format|
+      format.json { render json: template.to_json }
+    end
+  end
+
+  private
+
+  def get_template_class
+    template_types = { issue: Gitlab::Template::IssueTemplate, merge_request: Gitlab::Template::MergeRequestTemplate }.with_indifferent_access
+    @template_type = template_types[params[:template_type]]
+    render json: [], status: 404 unless @template_type
+  end
+end
diff --git a/app/controllers/projects/wikis_controller.rb b/app/controllers/projects/wikis_controller.rb
index 607fe9c7fed02d8bccc9af787ccea1284c4ed544..177ccf5eec963113407f3d8f1d8872da3b505b8c 100644
--- a/app/controllers/projects/wikis_controller.rb
+++ b/app/controllers/projects/wikis_controller.rb
@@ -91,7 +91,7 @@ class Projects::WikisController < Projects::ApplicationController
     )
   end
 
-  def markdown_preview
+  def preview_markdown
     text = params[:text]
 
     ext = Gitlab::ReferenceExtractor.new(@project, current_user)
diff --git a/app/controllers/projects_controller.rb b/app/controllers/projects_controller.rb
index a6e1aa5ccc1f69102ad853b309912c08186ca1f5..2a6385c1029514ef981ec8cdbb5776b0ecec6da9 100644
--- a/app/controllers/projects_controller.rb
+++ b/app/controllers/projects_controller.rb
@@ -5,7 +5,7 @@ class ProjectsController < Projects::ApplicationController
   before_action :project, except: [:new, :create]
   before_action :repository, except: [:new, :create]
   before_action :assign_ref_vars, only: [:show], if: :repo_exists?
-  before_action :tree, only: [:show], if: [:repo_exists?, :project_view_files?]
+  before_action :assign_tree_vars, only: [:show], if: [:repo_exists?, :project_view_files?]
 
   # Authorize
   before_action :authorize_admin_project!, only: [:edit, :update, :housekeeping, :download_export, :export, :remove_export, :generate_new_export]
@@ -125,7 +125,7 @@ class ProjectsController < Projects::ApplicationController
   def destroy
     return access_denied! unless can?(current_user, :remove_project, @project)
 
-    ::Projects::DestroyService.new(@project, current_user, {}).pending_delete!
+    ::Projects::DestroyService.new(@project, current_user, {}).async_execute
     flash[:alert] = "Project '#{@project.name}' will be deleted."
 
     redirect_to dashboard_projects_path
@@ -134,10 +134,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, state: 'all').
+          execute.find_by(iid: params[:type_id])
+      when 'MergeRequest'
+        MergeRequestsFinder.new(current_user, project_id: @project.id, state: 'all').
+          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 +157,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|
@@ -238,7 +251,7 @@ class ProjectsController < Projects::ApplicationController
     }
   end
 
-  def markdown_preview
+  def preview_markdown
     text = params[:text]
 
     ext = Gitlab::ReferenceExtractor.new(@project, current_user)
@@ -319,4 +332,11 @@ class ProjectsController < Projects::ApplicationController
   def get_id
     project.repository.root_ref
   end
+
+  # ExtractsPath will set @id = project.path on the show route, but it has to be the
+  # branch name for the tree view to work correctly.
+  def assign_tree_vars
+    @id = get_id
+    tree
+  end
 end
diff --git a/app/controllers/registrations_controller.rb b/app/controllers/registrations_controller.rb
index 75b78a49eab7b1e532f181b537da8309e220e30f..3327f4f2b871b146b890d600e80d29fc551335be 100644
--- a/app/controllers/registrations_controller.rb
+++ b/app/controllers/registrations_controller.rb
@@ -33,7 +33,7 @@ class RegistrationsController < Devise::RegistrationsController
 
   protected
 
-  def build_resource(hash=nil)
+  def build_resource(hash = nil)
     super
   end
 
diff --git a/app/controllers/sessions_controller.rb b/app/controllers/sessions_controller.rb
index 17aed816cbd984f6c6e65193b8406e9f36c027cd..5d7ecfeacf4a8a030bcb32eef52249c9faabf9ef 100644
--- a/app/controllers/sessions_controller.rb
+++ b/app/controllers/sessions_controller.rb
@@ -101,7 +101,7 @@ class SessionsController < Devise::SessionsController
     # Prevent alert from popping up on the first page shown after authentication.
     flash[:alert] = nil
 
-    redirect_to user_omniauth_authorize_path(provider.to_sym)
+    redirect_to omniauth_authorize_path(:user, provider)
   end
 
   def valid_otp_attempt?(user)
diff --git a/app/finders/move_to_project_finder.rb b/app/finders/move_to_project_finder.rb
new file mode 100644
index 0000000000000000000000000000000000000000..79eb45568bebf013d99fe7f1d9b0bb5a85acb12c
--- /dev/null
+++ b/app/finders/move_to_project_finder.rb
@@ -0,0 +1,20 @@
+class MoveToProjectFinder
+  PAGE_SIZE = 50
+
+  def initialize(user)
+    @user = user
+  end
+
+  def execute(from_project, search: nil, offset_id: nil)
+    projects = @user.projects_where_can_admin_issues
+    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
+end
diff --git a/app/finders/projects_finder.rb b/app/finders/projects_finder.rb
index 2f0a9659d15b878a47c8555c6a0daca56d43bae2..c7911736812e1f94f7ba288e561b8bfcf3904cb8 100644
--- a/app/finders/projects_finder.rb
+++ b/app/finders/projects_finder.rb
@@ -1,6 +1,7 @@
 class ProjectsFinder < UnionFinder
-  def execute(current_user = nil, options = {})
+  def execute(current_user = nil, project_ids_relation = nil)
     segments = all_projects(current_user)
+    segments.map! { |s| s.where(id: project_ids_relation) } if project_ids_relation
 
     find_union(segments, Project)
   end
diff --git a/app/finders/todos_finder.rb b/app/finders/todos_finder.rb
index ff866c2faa502e219f8bb06603dfa72ddb997c60..06b3e8a9502372e44dfc1728c2b9c64cab65ed9e 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
@@ -27,11 +27,13 @@ class TodosFinder
     items = by_action_id(items)
     items = by_action(items)
     items = by_author(items)
-    items = by_project(items)
     items = by_state(items)
     items = by_type(items)
+    # Filtering by project HAS TO be the last because we use
+    # the project IDs yielded by the todos query thus far
+    items = by_project(items)
 
-    items.reorder(id: :desc)
+    sort(items)
   end
 
   private
@@ -91,14 +93,9 @@ class TodosFinder
     @project
   end
 
-  def projects
-    return @projects if defined?(@projects)
-
-    if project?
-      @projects = project
-    else
-      @projects = ProjectsFinder.new.execute(current_user)
-    end
+  def projects(items)
+    item_project_ids = items.reorder(nil).select(:project_id)
+    ProjectsFinder.new.execute(current_user, item_project_ids)
   end
 
   def type?
@@ -109,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)
@@ -136,8 +137,9 @@ class TodosFinder
   def by_project(items)
     if project?
       items = items.where(project: project)
-    elsif projects
-      items = items.merge(projects).joins(:project)
+    else
+      item_projects = projects(items)
+      items = items.merge(item_projects).joins(:project)
     end
 
     items
diff --git a/app/helpers/appearances_helper.rb b/app/helpers/appearances_helper.rb
index e12a10529881930a6921d9470fa76989e086b7b9..de13e7a1fc2d8c2847ddd3ae60c400448efc4db3 100644
--- a/app/helpers/appearances_helper.rb
+++ b/app/helpers/appearances_helper.rb
@@ -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 50de93d4bdf9f260af2845782ff7a014ee769ef4..f3733b0172145ab9e1e252bb8392d0f8a22186c9 100644
--- a/app/helpers/application_helper.rb
+++ b/app/helpers/application_helper.rb
@@ -163,9 +163,13 @@ module ApplicationHelper
   # `html_class` argument is provided.
   #
   # Returns an HTML-safe String
-  def time_ago_with_tooltip(time, placement: 'top', html_class: 'time_ago', skip_js: false)
+  def time_ago_with_tooltip(time, placement: 'top', html_class: '', skip_js: false, 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: "#{html_class} js-timeago #{"js-timeago-pending" unless skip_js}",
+      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' }
@@ -316,4 +320,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..6de25bea654a782072e710de71978140ed26a6fa 100644
--- a/app/helpers/application_settings_helper.rb
+++ b/app/helpers/application_settings_helper.rb
@@ -31,6 +31,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
diff --git a/app/helpers/avatars_helper.rb b/app/helpers/avatars_helper.rb
index 6ff40c6b461a10a99cf535f493947c40d3e5c1eb..aa8acbe7567585ae346b2d98ab509e91bc499ec8 100644
--- a/app/helpers/avatars_helper.rb
+++ b/app/helpers/avatars_helper.rb
@@ -1,5 +1,4 @@
 module AvatarsHelper
-
   def author_avatar(commit_or_event, options = {})
     user_avatar(options.merge({
       user: commit_or_event.author,
@@ -8,8 +7,6 @@ module AvatarsHelper
     }))
   end
 
-  private
-
   def user_avatar(options = {})
     avatar_size = options[:size] || 16
     user_name = options[:user].try(:name) || options[:user_name]
@@ -26,5 +23,4 @@ module AvatarsHelper
       mail_to(options[:user_email], avatar)
     end
   end
-
 end
diff --git a/app/helpers/blob_helper.rb b/app/helpers/blob_helper.rb
index 48c2782821905c3782534bfd75638f3332bccd50..e13b7cdd7077da04845d003a990e93d0ec9dc64d 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,17 +179,50 @@ 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
+
   def gitignore_names
-    @gitignore_names ||=
-      Gitlab::Template::Gitignore.categories.keys.map do |k|
-        [k, Gitlab::Template::Gitignore.by_category(k).map { |t| { name: t.name } }]
-      end.to_h
+    @gitignore_names ||= Gitlab::Template::GitignoreTemplate.dropdown_names
   end
 
   def gitlab_ci_ymls
-    @gitlab_ci_ymls ||=
-      Gitlab::Template::GitlabCiYml.categories.keys.map do |k|
-        [k, Gitlab::Template::GitlabCiYml.by_category(k).map { |t| { name: t.name } }]
-      end.to_h
+    @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/ci_status_helper.rb b/app/helpers/ci_status_helper.rb
index ea2f5f9281a84c4ae5be1faadb37e70645f5e6cf..bb285a17baffa033adeeb5c28443dd18327fd950 100644
--- a/app/helpers/ci_status_helper.rb
+++ b/app/helpers/ci_status_helper.rb
@@ -38,6 +38,10 @@ module CiStatusHelper
         'icon_status_pending'
       when 'running'
         'icon_status_running'
+      when 'play'
+        'icon_play'
+      when 'created'
+        'icon_status_pending'
       else
         'icon_status_cancel'
       end
@@ -48,13 +52,13 @@ module CiStatusHelper
   def render_commit_status(commit, 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)
+    render_status_with_link('commit', commit.status, 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 +66,17 @@ module CiStatusHelper
       Ci::Runner.shared.blank?
   end
 
-  private
+  def render_status_with_link(type, status, path = nil, tooltip_placement: 'auto left', cssclass: '')
+    klass = "ci-status-link ci-status-icon-#{status.dasherize} #{cssclass}"
+    title = "#{type.titleize}: #{ci_label_for_status(status)}"
+    data = { toggle: 'tooltip', placement: tooltip_placement }
 
-  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 f497626e21aa6425494c4723298ad7981b6ecbdb..33dcee49aee4c5d50cea03ae4d389428d46e1874 100644
--- a/app/helpers/commits_helper.rb
+++ b/app/helpers/commits_helper.rb
@@ -98,28 +98,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)
@@ -206,10 +209,10 @@ module CommitsHelper
     end
   end
 
-  def view_file_btn(commit_sha, diff, project)
+  def view_file_btn(commit_sha, diff_new_path, project)
     link_to(
       namespace_project_blob_path(project.namespace, project,
-                                  tree_join(commit_sha, diff.new_path)),
+                                  tree_join(commit_sha, diff_new_path)),
       class: 'btn view-file js-view-file btn-file-option'
     ) do
       raw('View file @') + content_tag(:span, commit_sha[0..6],
diff --git a/app/helpers/diff_helper.rb b/app/helpers/diff_helper.rb
index f35e2f6ddcdadff7f7d41219c06708a74d083a1d..0725c3f4c56c812256d6021e3cb9978972e11633 100644
--- a/app/helpers/diff_helper.rb
+++ b/app/helpers/diff_helper.rb
@@ -13,12 +13,11 @@ module DiffHelper
   end
 
   def diff_view
-    diff_views = %w(inline parallel)
-
-    if diff_views.include?(cookies[:diff_view])
-      cookies[:diff_view]
-    else
-      diff_views.first
+    @diff_view ||= begin
+      diff_views = %w(inline parallel)
+      diff_view = cookies[:diff_view]
+      diff_view = diff_views.first unless diff_views.include?(diff_view)
+      diff_view.to_sym
     end
   end
 
@@ -30,19 +29,26 @@ module DiffHelper
       options[:paths] = params.values_at(:old_path, :new_path)
     end
 
-    Commit.max_diff_options.merge(options)
+    options
   end
 
-  def safe_diff_files(diffs, diff_refs: nil, repository: nil)
-    diffs.decorate! { |diff| Gitlab::Diff::File.new(diff, diff_refs: diff_refs, repository: repository) }
-  end
+  def diff_match_line(old_pos, new_pos, text: '', view: :inline, bottom: false)
+    content = content_tag :td, text, class: "line_content match #{view == :inline ? '' : view}"
+    cls = ['diff-line-num', 'unfold', 'js-unfold']
+    cls << 'js-unfold-bottom' if bottom
 
-  def unfold_bottom_class(bottom)
-    bottom ? 'js-unfold js-unfold-bottom' : ''
-  end
+    html = ''
+    if old_pos
+      html << content_tag(:td, '...', class: cls + ['old_line'], data: { linenumber: old_pos })
+      html << content unless view == :inline
+    end
+
+    if new_pos
+      html << content_tag(:td, '...', class: cls + ['new_line'], data: { linenumber: new_pos })
+      html << content
+    end
 
-  def unfold_class(unfold)
-    unfold ? 'unfold js-unfold' : ''
+    html.html_safe
   end
 
   def diff_line_content(line, line_type = nil)
@@ -71,11 +77,11 @@ module DiffHelper
   end
 
   def inline_diff_btn
-    diff_btn('Inline', 'inline', diff_view == 'inline')
+    diff_btn('Inline', 'inline', diff_view == :inline)
   end
 
   def parallel_diff_btn
-    diff_btn('Side-by-side', 'parallel', diff_view == 'parallel')
+    diff_btn('Side-by-side', 'parallel', diff_view == :parallel)
   end
 
   def submodule_link(blob, ref, repository = @repository)
@@ -103,11 +109,11 @@ module DiffHelper
     end
   end
 
-  def diff_file_html_data(project, diff_file)
-    commit = commit_for_diff(diff_file)
+  def diff_file_html_data(project, diff_file_path, diff_commit_id)
     {
       blob_diff_path: namespace_project_blob_diff_path(project.namespace, project,
-                                                       tree_join(commit.id, diff_file.file_path))
+                                                       tree_join(diff_commit_id, diff_file_path)),
+      view: diff_view
     }
   end
 
diff --git a/app/helpers/explore_helper.rb b/app/helpers/explore_helper.rb
index 337b0aacbb52685d26e2fb7b77db94c4c5b194ab..2b1f3825adc1525284df97aef4f1439f28c28d15 100644
--- a/app/helpers/explore_helper.rb
+++ b/app/helpers/explore_helper.rb
@@ -1,5 +1,5 @@
 module ExploreHelper
-  def filter_projects_path(options={})
+  def filter_projects_path(options = {})
     exist_opts = {
       sort: params[:sort],
       scope: params[:scope],
diff --git a/app/helpers/issuables_helper.rb b/app/helpers/issuables_helper.rb
index 47d174361dbfdfd55f786bfe479c7330c13f15db..b9baeb1d6c47f2affdec34ed0e56d097865622d1 100644
--- a/app/helpers/issuables_helper.rb
+++ b/app/helpers/issuables_helper.rb
@@ -72,6 +72,15 @@ 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
+
   private
 
   def sidebar_gutter_collapsed?
diff --git a/app/helpers/issues_helper.rb b/app/helpers/issues_helper.rb
index 2b0defd1dda0a6fc42c26378bc945857e013e07d..8b212b0327ab63bbf880a1c122f6caae6478a076 100644
--- a/app/helpers/issues_helper.rb
+++ b/app/helpers/issues_helper.rb
@@ -13,38 +13,6 @@ module IssuesHelper
     OpenStruct.new(id: 0, title: 'None (backlog)', name: 'Unassigned')
   end
 
-  def url_for_project_issues(project = @project, options = {})
-    return '' if project.nil?
-
-    url =
-      if options[:only_path]
-        project.issues_tracker.project_path
-      else
-        project.issues_tracker.project_url
-      end
-
-    # Ensure we return a valid URL to prevent possible XSS.
-    URI.parse(url).to_s
-  rescue URI::InvalidURIError
-    ''
-  end
-
-  def url_for_new_issue(project = @project, options = {})
-    return '' if project.nil?
-
-    url =
-      if options[:only_path]
-        project.issues_tracker.new_issue_path
-      else
-        project.issues_tracker.new_issue_url
-      end
-
-    # Ensure we return a valid URL to prevent possible XSS.
-    URI.parse(url).to_s
-  rescue URI::InvalidURIError
-    ''
-  end
-
   def url_for_issue(issue_iid, project = @project, options = {})
     return '' if project.nil?
 
@@ -146,9 +114,17 @@ module IssuesHelper
   end
 
   def award_user_list(awards, current_user)
-    awards.map do |award|
-      award.user == current_user ? 'me' : award.user.name
-    end.join(', ')
+    names = awards.map do |award|
+      award.user == current_user ? 'You' : award.user.name
+    end
+
+    # Take first 9 OR current user + first 9
+    current_user_name = names.delete('You')
+    names = names.first(9).insert(0, current_user_name).compact
+
+    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/lfs_helper.rb b/app/helpers/lfs_helper.rb
new file mode 100644
index 0000000000000000000000000000000000000000..eb651e3687eb64eb8d0eaf66e43faf7ad6e78128
--- /dev/null
+++ b/app/helpers/lfs_helper.rb
@@ -0,0 +1,67 @@
+module LfsHelper
+  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",
+      },
+      status: 501
+    )
+  end
+
+  def lfs_check_access!
+    return if download_request? && lfs_download_access?
+    return if upload_request? && lfs_upload_access?
+
+    if project.public? || (user && user.can?(:read_project, project))
+      render_lfs_forbidden
+    else
+      render_lfs_not_found
+    end
+  end
+
+  def lfs_download_access?
+    project.public? || ci? || (user && user.can?(:download_code, project))
+  end
+
+  def lfs_upload_access?
+    user && user.can?(:push_code, project)
+  end
+
+  def render_lfs_forbidden
+    render(
+      json: {
+        message: 'Access forbidden. Check your access level.',
+        documentation_url: "#{Gitlab.config.gitlab.url}/help",
+      },
+      content_type: "application/vnd.git-lfs+json",
+      status: 403
+    )
+  end
+
+  def render_lfs_not_found
+    render(
+      json: {
+        message: 'Not found.',
+        documentation_url: "#{Gitlab.config.gitlab.url}/help",
+      },
+      content_type: "application/vnd.git-lfs+json",
+      status: 404
+    )
+  end
+
+  def storage_project
+    @storage_project ||= begin
+      result = project
+
+      loop do
+        break unless result.forked?
+        result = result.forked_from_project
+      end
+
+      result
+    end
+  end
+end
diff --git a/app/helpers/members_helper.rb b/app/helpers/members_helper.rb
index ec106418f2dc4f69b051688ab30ab1dcd8fa59d4..877c77050bed0979d2814dacb549fd279f3d89e0 100644
--- a/app/helpers/members_helper.rb
+++ b/app/helpers/members_helper.rb
@@ -6,12 +6,6 @@ module MembersHelper
     "#{action}_#{member.type.underscore}".to_sym
   end
 
-  def default_show_roles(member)
-    can?(current_user, action_member_permission(:update, member), member) ||
-      can?(current_user, action_member_permission(:destroy, member), member) ||
-      can?(current_user, action_member_permission(:admin, member), member.source)
-  end
-
   def remove_member_message(member, user: nil)
     user = current_user if defined?(current_user)
 
diff --git a/app/helpers/nav_helper.rb b/app/helpers/nav_helper.rb
index 3ff8be5e284cbe4126a16e309d46be7ce071973b..6c1cc6ef072df5a99f6e156ab2dd350db1b1e0e4 100644
--- a/app/helpers/nav_helper.rb
+++ b/app/helpers/nav_helper.rb
@@ -24,6 +24,7 @@ module NavHelper
       current_path?('merge_requests#diffs') ||
       current_path?('merge_requests#commits') ||
       current_path?('merge_requests#builds') ||
+      current_path?('merge_requests#conflicts') ||
       current_path?('issues#show')
       if cookies[:collapsed_gutter] == 'true'
         "page-gutter right-sidebar-collapsed"
diff --git a/app/helpers/notes_helper.rb b/app/helpers/notes_helper.rb
index 0c47abe0fbad45987c509358ee9cca70db463af8..da230f76baedfac413e2ca644a053e7b9d841ae9 100644
--- a/app/helpers/notes_helper.rb
+++ b/app/helpers/notes_helper.rb
@@ -49,7 +49,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 +60,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 +81,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)
@@ -92,6 +90,10 @@ module NotesHelper
     project.team.max_member_access_for_user_ids(user_ids)
   end
 
+  def preload_noteable_for_regular_notes(notes)
+    ActiveRecord::Associations::Preloader.new.preload(notes.select { |note| !note.for_commit? }, :noteable)
+  end
+
   def note_max_access_for_user(note)
     note.project.team.human_max_access(note.author_id)
   end
diff --git a/app/helpers/projects_helper.rb b/app/helpers/projects_helper.rb
index a733dff1579d2dd1a4028bc24261613ae8fc5f07..249d18c4486975ef40a63283a818757397384623 100644
--- a/app/helpers/projects_helper.rb
+++ b/app/helpers/projects_helper.rb
@@ -236,6 +236,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(
@@ -263,6 +317,10 @@ module ProjectsHelper
     filename_path(project, :version)
   end
 
+  def ci_configuration_path(project)
+    filename_path(project, :gitlab_ci_yml)
+  end
+
   def project_wiki_path_with_version(proj, page, version, is_newest)
     url_params = is_newest ? {} : { version_id: version }
     namespace_project_wiki_path(proj.namespace, proj, page, url_params)
diff --git a/app/helpers/search_helper.rb b/app/helpers/search_helper.rb
index a2bba139c170e21a16d43ad53810faa0073fbdeb..c0195713f4a5090713f7243f5a6c96d18f4634b9 100644
--- a/app/helpers/search_helper.rb
+++ b/app/helpers/search_helper.rb
@@ -107,7 +107,7 @@ module SearchHelper
     Sanitize.clean(str)
   end
 
-  def search_filter_path(options={})
+  def search_filter_path(options = {})
     exist_opts = {
       search: params[:search],
       project_id: params[:project_id],
diff --git a/app/helpers/sorting_helper.rb b/app/helpers/sorting_helper.rb
index e1c0b49755099054fef8cf210562a00a996be6b0..8b138a8e69f6d4015fcf415b1dad7e68f07616c7 100644
--- a/app/helpers/sorting_helper.rb
+++ b/app/helpers/sorting_helper.rb
@@ -20,13 +20,19 @@ module SortingHelper
   end
 
   def projects_sort_options_hash
-    {
+    options = {
       sort_value_name => sort_title_name,
       sort_value_recently_updated => sort_title_recently_updated,
       sort_value_oldest_updated => sort_title_oldest_updated,
       sort_value_recently_created => sort_title_recently_created,
       sort_value_oldest_created => sort_title_oldest_created,
     }
+
+    if current_controller?('admin/projects')
+      options.merge!(sort_value_largest_repo => sort_title_largest_repo)
+    end
+
+    options
   end
 
   def sort_title_priority
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 e3a208f826a0fa8d5d1d165010057d9a4f0dbffc..0465327060ee1ee1a32c13bcc4e72e8532aa73db 100644
--- a/app/helpers/todos_helper.rb
+++ b/app/helpers/todos_helper.rb
@@ -1,10 +1,10 @@
 module TodosHelper
   def todos_pending_count
-    @todos_pending_count ||= TodosFinder.new(current_user, state: :pending).execute.count
+    @todos_pending_count ||= current_user.todos_pending_count
   end
 
   def todos_done_count
-    @todos_done_count ||= TodosFinder.new(current_user, state: :done).execute.count
+    @todos_done_count ||= current_user.todos_done_count
   end
 
   def todo_action_name(todo)
diff --git a/app/helpers/tree_helper.rb b/app/helpers/tree_helper.rb
index dbedf417fa5a1eda4998901954b0257c00dcd42f..4a76c679bad5bd00da09db7df4656e705a6434a1 100644
--- a/app/helpers/tree_helper.rb
+++ b/app/helpers/tree_helper.rb
@@ -4,23 +4,11 @@ module TreeHelper
   #
   # contents - A Grit::Tree object for the current tree
   def render_tree(tree)
-    # Render Folders before Files/Submodules
+    # Sort submodules and folders together by name ahead of files
     folders, files, submodules = tree.trees, tree.blobs, tree.submodules
-
     tree = ""
-
-    # Render folders if we have any
-    tree << render(partial: 'projects/tree/tree_item', collection: folders,
-                   locals: { type: 'folder' }) if folders.present?
-
-    # Render files if we have any
-    tree << render(partial: 'projects/tree/blob_item', collection: files,
-                   locals: { type: 'file' }) if files.present?
-
-    # Render submodules if we have any
-    tree << render(partial: 'projects/tree/submodule_item',
-                   collection: submodules) if submodules.present?
-
+    items = (folders + submodules).sort_by(&:name) + files
+    tree << render(partial: "projects/tree/tree_row", collection: items) if items.present?
     tree.html_safe
   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/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/models/ability.rb b/app/models/ability.rb
index d95a250719977937a88c8a140b04b295e464a126..a49dd7039262df49d81f84d6a7f97837c1458728 100644
--- a/app/models/ability.rb
+++ b/app/models/ability.rb
@@ -6,6 +6,10 @@ class Ability
       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)
@@ -86,6 +90,8 @@ class Ability
       if project && project.public?
         rules = [
           :read_project,
+          :read_board,
+          :read_list,
           :read_wiki,
           :read_label,
           :read_milestone,
@@ -160,38 +166,44 @@ class Ability
     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))
+      if RequestStore.active?
+        RequestStore.store[key] ||= uncached_project_abilities(user, project)
+      else
+        uncached_project_abilities(user, project)
+      end
+    end
 
-        owner = user.admin? ||
-                project.owner == user ||
-                (project.group && project.group.has_owner?(user))
+    def uncached_project_abilities(user, project)
+      rules = []
+      # Push abilities on the users team role
+      rules.push(*project_team_rules(project.team, user))
 
-        if owner
-          rules.push(*project_owner_rules)
-        end
+      owner = user.admin? ||
+              project.owner == user ||
+              (project.group && project.group.has_owner?(user))
 
-        if project.public? || (project.internal? && !user.external?)
-          rules.push(*public_project_rules)
+      if owner
+        rules.push(*project_owner_rules)
+      end
 
-          # Allow to read builds for internal projects
-          rules << :read_build if project.public_builds?
+      if project.public? || (project.internal? && !user.external?)
+        rules.push(*public_project_rules)
 
-          unless owner || project.team.member?(user) || project_group_member?(project, user)
-            rules << :request_access if project.request_access_enabled
-          end
-        end
+        # Allow to read builds for internal projects
+        rules << :read_build if project.public_builds?
 
-        if project.archived?
-          rules -= project_archived_rules
+        unless owner || project.team.member?(user) || project_group_member?(project, user)
+          rules << :request_access if project.request_access_enabled
         end
+      end
 
-        rules - project_disabled_features_rules(project)
+      if project.archived?
+        rules -= project_archived_rules
       end
+
+      (rules - project_disabled_features_rules(project)).uniq
     end
 
     def project_team_rules(team, user)
@@ -224,6 +236,8 @@ class Ability
         :read_project,
         :read_wiki,
         :read_issue,
+        :read_board,
+        :read_list,
         :read_label,
         :read_milestone,
         :read_project_snippet,
@@ -245,6 +259,7 @@ class Ability
         :update_issue,
         :admin_issue,
         :admin_label,
+        :admin_list,
         :read_commit_status,
         :read_build,
         :read_container_image,
@@ -267,6 +282,7 @@ class Ability
         :create_merge_request,
         :create_wiki,
         :push_code,
+        :resolve_note,
         :create_container_image,
         :update_container_image,
         :create_environment,
@@ -448,7 +464,8 @@ class Ability
         rules += [
           :read_note,
           :update_note,
-          :admin_note
+          :admin_note,
+          :resolve_note
         ]
       end
 
@@ -456,6 +473,10 @@ class Ability
         rules += project_abilities(user, note.project)
       end
 
+      if note.for_merge_request? && note.noteable.author == user
+        rules << :resolve_note
+      end
+
       rules
     end
 
diff --git a/app/models/application_setting.rb b/app/models/application_setting.rb
index 8c19d9dc9c8c8481ce8d29f7cbc9cca425d32be8..f0bcb2d7cdac7ffa6110450750c73a0378d9392f 100644
--- a/app/models/application_setting.rb
+++ b/app/models/application_setting.rb
@@ -55,6 +55,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 }
@@ -149,6 +153,8 @@ class ApplicationSetting < ActiveRecord::Base
       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,
diff --git a/app/models/blob.rb b/app/models/blob.rb
index 0df2805e448653029aa0d5629bd9325c4f623d13..12cc5aaafba1bbeb13a361e93d88f8bdce6264cb 100644
--- a/app/models/blob.rb
+++ b/app/models/blob.rb
@@ -3,6 +3,9 @@ class Blob < SimpleDelegator
   CACHE_TIME = 60 # Cache raw blobs referred to by a (mutable) ref for 1 minute
   CACHE_TIME_IMMUTABLE = 3600 # Cache blobs referred to by an immutable reference for 1 hour
 
+  # The maximum size of an SVG that can be displayed.
+  MAXIMUM_SVG_SIZE = 2.megabytes
+
   # Wrap a Gitlab::Git::Blob object, or return nil when given nil
   #
   # This method prevents the decorated object from evaluating to "truthy" when
@@ -31,6 +34,10 @@ class Blob < SimpleDelegator
     text? && language && language.name == 'SVG'
   end
 
+  def size_within_svg_limits?
+    size <= MAXIMUM_SVG_SIZE
+  end
+
   def video?
     UploaderHelper::VIDEO_EXT.include?(extname.downcase.delete('.'))
   end
diff --git a/app/models/board.rb b/app/models/board.rb
new file mode 100644
index 0000000000000000000000000000000000000000..3240c4bede3f5b5cab9d61ebcc3c5a31e855a143
--- /dev/null
+++ b/app/models/board.rb
@@ -0,0 +1,7 @@
+class Board < ActiveRecord::Base
+  belongs_to :project
+
+  has_many :lists, -> { order(:list_type, :position) }, dependent: :delete_all
+
+  validates :project, presence: true
+end
diff --git a/app/models/ci/build.rb b/app/models/ci/build.rb
index a22caa7d296f469402e8707d18842cf0470ca73c..096b3b801af1f5fbec964f70438e32a3a8fe00a5 100644
--- a/app/models/ci/build.rb
+++ b/app/models/ci/build.rb
@@ -16,7 +16,7 @@ module Ci
     scope :with_artifacts_not_expired, ->() { with_artifacts.where('artifacts_expire_at IS NULL OR artifacts_expire_at > ?', Time.now) }
     scope :with_expired_artifacts, ->() { with_artifacts.where('artifacts_expire_at < ?', Time.now) }
     scope :last_month, ->() { where('created_at > ?', Date.today - 1.month) }
-    scope :manual_actions, ->() { where(when: :manual) }
+    scope :manual_actions, ->() { where(when: :manual).relevant }
 
     mount_uploader :artifacts_file, ArtifactUploader
     mount_uploader :artifacts_metadata, ArtifactUploader
@@ -42,40 +42,36 @@ module Ci
       end
 
       def retry(build, user = nil)
-        new_build = Ci::Build.new(status: 'pending')
-        new_build.ref = build.ref
-        new_build.tag = build.tag
-        new_build.options = build.options
-        new_build.commands = build.commands
-        new_build.tag_list = build.tag_list
-        new_build.project = build.project
-        new_build.pipeline = build.pipeline
-        new_build.name = build.name
-        new_build.allow_failure = build.allow_failure
-        new_build.stage = build.stage
-        new_build.stage_idx = build.stage_idx
-        new_build.trigger_request = build.trigger_request
-        new_build.yaml_variables = build.yaml_variables
-        new_build.when = build.when
-        new_build.user = user
-        new_build.environment = build.environment
-        new_build.save
+        new_build = Ci::Build.create(
+          ref: build.ref,
+          tag: build.tag,
+          options: build.options,
+          commands: build.commands,
+          tag_list: build.tag_list,
+          project: build.project,
+          pipeline: build.pipeline,
+          name: build.name,
+          allow_failure: build.allow_failure,
+          stage: build.stage,
+          stage_idx: build.stage_idx,
+          trigger_request: build.trigger_request,
+          yaml_variables: build.yaml_variables,
+          when: build.when,
+          user: user,
+          environment: build.environment,
+          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, initial: :pending do
+    state_machine :status do
       after_transition pending: :running do |build|
         build.execute_hooks
       end
 
-      # We use around_transition to create builds for next stage as soon as possible, before the `after_*` is executed
-      around_transition any => [:success, :failed, :canceled] do |build, block|
-        block.call
-        build.pipeline.create_next_builds(build) if build.pipeline
-      end
-
       after_transition any => [:success, :failed, :canceled] do |build|
         build.update_coverage
         build.execute_hooks
@@ -102,12 +98,12 @@ module Ci
     end
 
     def playable?
-      project.builds_enabled? && commands.present? && manual?
+      project.builds_enabled? && commands.present? && manual? && skipped?
     end
 
     def play(current_user = nil)
       # Try to queue a current build
-      if self.queue
+      if self.enqueue
         self.update(user: current_user)
         self
       else
@@ -349,7 +345,7 @@ module Ci
 
     def execute_hooks
       return unless project
-      build_data = Gitlab::BuildDataBuilder.build(self)
+      build_data = Gitlab::DataBuilder::Build.build(self)
       project.execute_hooks(build_data.dup, :build_hooks)
       project.execute_services(build_data.dup, :build_hooks)
       project.running_or_pending_build_count(force: true)
diff --git a/app/models/ci/pipeline.rb b/app/models/ci/pipeline.rb
index 4e6ccf48c68cf63f43026be550448053179a1ab4..087abe4cbb1b9f03e27d56a5e41338471b7aee9d 100644
--- a/app/models/ci/pipeline.rb
+++ b/app/models/ci/pipeline.rb
@@ -13,13 +13,57 @@ module Ci
     has_many :trigger_requests, dependent: :destroy, class_name: 'Ci::TriggerRequest', foreign_key: :commit_id
 
     validates_presence_of :sha
+    validates_presence_of :ref
     validates_presence_of :status
     validate :valid_commit_sha
 
-    # Invalidate object and save if when touched
-    after_touch :update_state
     after_save :keep_around_commits
 
+    delegate :stages, to: :statuses
+
+    state_machine :status, initial: :created do
+      event :enqueue do
+        transition created: :pending
+        transition [:success, :failed, :canceled, :skipped] => :running
+      end
+
+      event :run do
+        transition any => :running
+      end
+
+      event :skip do
+        transition any => :skipped
+      end
+
+      event :drop do
+        transition any => :failed
+      end
+
+      event :succeed do
+        transition any => :success
+      end
+
+      event :cancel do
+        transition any => :canceled
+      end
+
+      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
+      end
+
+      before_transition do |pipeline|
+        pipeline.update_duration
+      end
+
+      after_transition do |pipeline, transition|
+        pipeline.execute_hooks unless transition.loopback?
+      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)
@@ -34,6 +78,14 @@ 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.order(:stage_idx).group_by(&:stage)
+    end
+
     def project_id
       project.id
     end
@@ -98,6 +150,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)
@@ -109,37 +165,6 @@ module Ci
       trigger_requests.any?
     end
 
-    def create_builds(user, trigger_request = nil)
-      ##
-      # We persist pipeline only if there are builds available
-      #
-      return unless config_processor
-
-      build_builds_for_stages(config_processor.stages, user,
-                              'success', trigger_request) && save
-    end
-
-    def create_next_builds(build)
-      return unless config_processor
-
-      # don't create other builds if this one is retried
-      latest_builds = builds.latest
-      return unless latest_builds.exists?(build.id)
-
-      # get list of stages after this build
-      next_stages = config_processor.stages.drop_while { |stage| stage != build.stage }
-      next_stages.delete(build.stage)
-
-      # get status for all prior builds
-      prior_builds = latest_builds.where.not(stage: next_stages)
-      prior_status = prior_builds.status
-
-      # build builds for next stage that has builds available
-      # and save pipeline if we have builds
-      build_builds_for_stages(next_stages, build.user, prior_status,
-                              build.trigger_request) && save
-    end
-
     def retried
       @retried ||= (statuses.order(id: :desc) - statuses.latest)
     end
@@ -151,6 +176,14 @@ module Ci
       end
     end
 
+    def config_builds_attributes
+      return [] unless config_processor
+
+      config_processor.
+        builds_for_ref(ref, tag?, trigger_requests.first).
+        sort_by { |build| build[:stage_idx] }
+    end
+
     def has_warnings?
       builds.latest.ignored.any?
     end
@@ -182,10 +215,6 @@ module Ci
       end
     end
 
-    def skip_ci?
-      git_commit_message =~ /\[(ci skip|skip ci)\]/i if git_commit_message
-    end
-
     def environments
       builds.where.not(environment: nil).success.pluck(:environment).uniq
     end
@@ -207,45 +236,47 @@ module Ci
       Note.for_commit_id(sha)
     end
 
+    def process!
+      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
+      end
+    end
+
     def predefined_variables
       [
         { key: 'CI_PIPELINE_ID', value: id.to_s, public: true }
       ]
     end
 
-    private
+    def update_duration
+      self.duration = calculate_duration
+    end
 
-    def build_builds_for_stages(stages, user, status, trigger_request)
-      ##
-      # Note that `Array#any?` implements a short circuit evaluation, so we
-      # build builds only for the first stage that has builds available.
-      #
-      stages.any? do |stage|
-        CreateBuildsService.new(self).
-          execute(stage, user, status, trigger_request).
-          any?(&:active?)
-      end
+    def execute_hooks
+      data = pipeline_data
+      project.execute_hooks(data, :pipeline_hooks)
+      project.execute_services(data, :pipeline_hooks)
     end
 
-    def update_state
-      statuses.reload
-      self.status = if yaml_errors.blank?
-                      statuses.latest.status || 'skipped'
-                    else
-                      'failed'
-                    end
-      self.started_at = statuses.started_at
-      self.finished_at = statuses.finished_at
-      self.duration = statuses.latest.duration
-      saved = save
-      execute_hooks if saved && !skip_ci?
-      saved
+    private
+
+    def pipeline_data
+      Gitlab::DataBuilder::Pipeline.build(self)
     end
 
-    def execute_hooks
-      pipeline_data = Gitlab::DataBuilder::PipelineDataBuilder.build(self)
-      project.execute_hooks(pipeline_data, :pipeline_hooks)
-      project.execute_services(pipeline_data.dup, :pipeline_hooks)
+    def latest_builds_status
+      return 'failed' unless yaml_errors.blank?
+
+      statuses.latest.status || 'skipped'
     end
 
     def keep_around_commits
diff --git a/app/models/commit.rb b/app/models/commit.rb
index 486ad6714d9d1cb217043c840488ba580063cb2a..817d063e4a2ac88f5eff91cc0112b31ec7a1fe21 100644
--- a/app/models/commit.rb
+++ b/app/models/commit.rb
@@ -104,7 +104,7 @@ class Commit
   end
 
   def diff_line_count
-    @diff_line_count ||= Commit::diff_line_count(self.diffs)
+    @diff_line_count ||= Commit::diff_line_count(raw_diffs)
     @diff_line_count
   end
 
@@ -123,15 +123,17 @@ class Commit
   # In case this first line is longer than 100 characters, it is cut off
   # after 80 characters and ellipses (`&hellp;`) are appended.
   def title
-    title = safe_message
+    full_title.length > 100 ? full_title[0..79] << "…" : full_title
+  end
 
-    return no_commit_message if title.blank?
+  # Returns the full commits title
+  def full_title
+    return @full_title if @full_title
 
-    title_end = title.index("\n")
-    if (!title_end && title.length > 100) || (title_end && title_end > 100)
-      title[0..79] << "…"
+    if safe_message.blank?
+      @full_title = no_commit_message
     else
-      title.split("\n", 2).first
+      @full_title = safe_message.split("\n", 2).first
     end
   end
 
@@ -227,7 +229,7 @@ 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
@@ -315,6 +317,14 @@ class Commit
     nil
   end
 
+  def raw_diffs(*args)
+    raw.diffs(*args)
+  end
+
+  def diffs(diff_options = nil)
+    Gitlab::Diff::FileCollection::Commit.new(self, diff_options: diff_options)
+  end
+
   private
 
   def find_author_by_any_email
@@ -324,7 +334,7 @@ class Commit
   def repo_changes
     changes = { added: [], modified: [], removed: [] }
 
-    diffs.each do |diff|
+    raw_diffs(deltas_only: true).each do |diff|
       if diff.deleted_file
         changes[:removed] << diff.old_path
       elsif diff.renamed_file || diff.new_file
diff --git a/app/models/commit_status.rb b/app/models/commit_status.rb
index 2d185c28809d69eced89d1057662c5c7d309d7e1..84ceeac7d3eb29dd005e57514107a834a96b89e8 100644
--- a/app/models/commit_status.rb
+++ b/app/models/commit_status.rb
@@ -5,7 +5,7 @@ class CommitStatus < ActiveRecord::Base
   self.table_name = 'ci_builds'
 
   belongs_to :project, class_name: '::Project', foreign_key: :gl_project_id
-  belongs_to :pipeline, class_name: 'Ci::Pipeline', foreign_key: :commit_id, touch: true
+  belongs_to :pipeline, class_name: 'Ci::Pipeline', foreign_key: :commit_id
   belongs_to :user
 
   delegate :commit, to: :pipeline
@@ -21,32 +21,45 @@ 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]) }
 
-  state_machine :status, initial: :pending do
-    event :queue do
-      transition skipped: :pending
+  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
 
+    event :skip do
+      transition [:created, :pending] => :skipped
+    end
+
     event :drop do
-      transition [:pending, :running] => :failed
+      transition [:created, :pending, :running] => :failed
     end
 
     event :success do
-      transition [:pending, :running] => :success
+      transition [:created, :pending, :running] => :success
     end
 
     event :cancel do
-      transition [:pending, :running] => :canceled
+      transition [:created, :pending, :running] => :canceled
+    end
+
+    after_transition created: [:pending, :running] do |commit_status|
+      commit_status.update_attributes queued_at: Time.now
     end
 
-    after_transition pending: :running do |commit_status|
+    after_transition [:created, :pending] => :running do |commit_status|
       commit_status.update_attributes started_at: Time.now
     end
 
@@ -54,7 +67,18 @@ class CommitStatus < ActiveRecord::Base
       commit_status.update_attributes finished_at: Time.now
     end
 
-    after_transition [:pending, :running] => :success do |commit_status|
+    # 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!)
+    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)
     end
 
@@ -88,13 +112,7 @@ class CommitStatus < ActiveRecord::Base
   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
new file mode 100644
index 0000000000000000000000000000000000000000..4856510f5263360306304d6b1be45fdab2db6297
--- /dev/null
+++ b/app/models/compare.rb
@@ -0,0 +1,66 @@
+class Compare
+  delegate :same, :head, :base, to: :@compare
+
+  attr_reader :project
+
+  def self.decorate(compare, project)
+    if compare.is_a?(Compare)
+      compare
+    else
+      self.new(compare, project)
+    end
+  end
+
+  def initialize(compare, project)
+    @compare = compare
+    @project = project
+  end
+
+  def commits
+    @commits ||= Commit.decorate(@compare.commits, project)
+  end
+
+  def start_commit
+    return @start_commit if defined?(@start_commit)
+
+    commit = @compare.base
+    @start_commit = commit ? ::Commit.new(commit, project) : nil
+  end
+
+  def head_commit
+    return @head_commit if defined?(@head_commit)
+
+    commit = @compare.head
+    @head_commit = commit ? ::Commit.new(commit, project) : nil
+  end
+  alias_method :commit, :head_commit
+
+  def base_commit
+    return @base_commit if defined?(@base_commit)
+
+    @base_commit = if start_commit && head_commit
+                     project.merge_base_commit(start_commit.id, head_commit.id)
+                   else
+                     nil
+                   end
+  end
+
+  def raw_diffs(*args)
+    @compare.diffs(*args)
+  end
+
+  def diffs(diff_options = nil)
+    Gitlab::Diff::FileCollection::Compare.new(self,
+      project: project,
+      diff_options: diff_options,
+      diff_refs: diff_refs)
+  end
+
+  def diff_refs
+    Gitlab::Diff::DiffRefs.new(
+      base_sha:  base_commit.try(:sha),
+      start_sha: start_commit.try(:sha),
+      head_sha: commit.try(:sha)
+    )
+  end
+end
diff --git a/app/models/concerns/expirable.rb b/app/models/concerns/expirable.rb
new file mode 100644
index 0000000000000000000000000000000000000000..be93435453bfb3ca2eefa3c21e756b90ddbb6607
--- /dev/null
+++ b/app/models/concerns/expirable.rb
@@ -0,0 +1,15 @@
+module Expirable
+  extend ActiveSupport::Concern
+
+  included do
+    scope :expired, -> { where('expires_at <= ?', Time.current) }
+  end
+
+  def expires?
+    expires_at.present?
+  end
+
+  def expires_soon?
+    expires_at < 7.days.from_now
+  end
+end
diff --git a/app/models/concerns/faster_cache_keys.rb b/app/models/concerns/faster_cache_keys.rb
new file mode 100644
index 0000000000000000000000000000000000000000..5b14723fa2db71db1c02aa1f6886683622961729
--- /dev/null
+++ b/app/models/concerns/faster_cache_keys.rb
@@ -0,0 +1,16 @@
+module FasterCacheKeys
+  # A faster version of Rails' "cache_key" method.
+  #
+  # Rails' default "cache_key" method uses all kind of complex logic to figure
+  # out the cache key. In many cases this complexity and overhead may not be
+  # needed.
+  #
+  # This method does not do any timestamp parsing as this process is quite
+  # expensive and not needed when generating cache keys. This method also relies
+  # on the table name instead of the cache namespace name as the latter uses
+  # complex logic to generate the exact same value (as when using the table
+  # name) in 99% of the cases.
+  def cache_key
+    "#{self.class.table_name}/#{id}-#{read_attribute_before_type_cast(:updated_at)}"
+  end
+end
diff --git a/app/models/concerns/issuable.rb b/app/models/concerns/issuable.rb
index cbae1cd439bc3d0ffdd0757679570e35ee62c3ef..8e11d4f57cf41c6e81bdf793bfa12978bed7745d 100644
--- a/app/models/concerns/issuable.rb
+++ b/app/models/concerns/issuable.rb
@@ -87,6 +87,12 @@ module Issuable
       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 +137,10 @@ module Issuable
     end
 
     def order_labels_priority(excluded_labels: [])
-      select("#{table_name}.*, (#{highest_label_priority(excluded_labels).to_sql}) AS highest_priority").
+      condition_field = "#{table_name}.id"
+      highest_priority = highest_label_priority(name, condition_field, excluded_labels: excluded_labels).to_sql
+
+      select("#{table_name}.*, (#{highest_priority}) AS highest_priority").
         group(arel_table[:id]).
         reorder(Gitlab::Database.nulls_last_order('highest_priority', 'ASC'))
     end
@@ -159,20 +168,6 @@ 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
-    end
   end
 
   def today?
diff --git a/app/models/concerns/note_on_diff.rb b/app/models/concerns/note_on_diff.rb
index 4be6a2f621b322b5c2b9d582d757454b1108d19a..a881fb83b7f6fee492c63895e008e02942222711 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
diff --git a/app/models/concerns/protected_branch_access.rb b/app/models/concerns/protected_branch_access.rb
new file mode 100644
index 0000000000000000000000000000000000000000..5a7b36070e79e8fe43c3bcddb3ac12cd27efbe81
--- /dev/null
+++ b/app/models/concerns/protected_branch_access.rb
@@ -0,0 +1,7 @@
+module ProtectedBranchAccess
+  extend ActiveSupport::Concern
+
+  def humanize
+    self.class.human_access_levels[self.access_level]
+  end
+end
diff --git a/app/models/concerns/sortable.rb b/app/models/concerns/sortable.rb
index 8b47b9e0abd2879d2b67275b90e8e3c78c81849a..1ebecd86af9b4d961fa1d42e29ccc77d75d1a137 100644
--- a/app/models/concerns/sortable.rb
+++ b/app/models/concerns/sortable.rb
@@ -35,5 +35,19 @@ module Sortable
         all
       end
     end
+
+    private
+
+    def highest_label_priority(object_types, condition_field, excluded_labels: [])
+      query = Label.select(Label.arel_table[:priority].minimum).
+        joins(:label_links).
+        where(label_links: { target_type: object_types }).
+        where("label_links.target_id = #{condition_field}").
+        reorder(nil)
+
+      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 3b8e6df2da9e992583d995ad0228b9e7b8d034de..1aa97debe426fcfe3c893603833e6119da366191 100644
--- a/app/models/concerns/spammable.rb
+++ b/app/models/concerns/spammable.rb
@@ -1,9 +1,32 @@
 module Spammable
   extend ActiveSupport::Concern
 
+  module ClassMethods
+    def attr_spammable(attr, options = {})
+      spammable_attrs << [attr.to_s, options]
+    end
+  end
+
   included do
+    has_one :user_agent_detail, as: :subject, dependent: :destroy
+
     attr_accessor :spam
+
     after_validation :check_for_spam, on: :create
+
+    cattr_accessor :spammable_attrs, instance_accessor: false do
+      []
+    end
+
+    delegate :ip_address, :user_agent, to: :user_agent_detail, allow_nil: true
+  end
+
+  def submittable_as_spam?
+    if user_agent_detail
+      user_agent_detail.submittable? && current_application_settings.akismet_enabled
+    else
+      false
+    end
   end
 
   def spam?
@@ -13,4 +36,33 @@ module Spammable
   def check_for_spam
     self.errors.add(:base, "Your #{self.class.name.underscore} has been recognized as spam and has been discarded.") if spam?
   end
+
+  def spam_title
+    attr = self.class.spammable_attrs.find do |_, options|
+      options.fetch(:spam_title, false)
+    end
+
+    public_send(attr.first) if attr && respond_to?(attr.first.to_sym)
+  end
+
+  def spam_description
+    attr = self.class.spammable_attrs.find do |_, options|
+      options.fetch(:spam_description, false)
+    end
+
+    public_send(attr.first) if attr && respond_to?(attr.first.to_sym)
+  end
+
+  def spammable_text
+    result = self.class.spammable_attrs.map do |attr|
+      public_send(attr.first)
+    end
+
+    result.reject(&:blank?).join("\n")
+  end
+
+  # Override in Spammable if further checks are necessary
+  def check_for_spam?
+    true
+  end
 end
diff --git a/app/models/concerns/statuseable.rb b/app/models/concerns/statuseable.rb
index 44c6b30f2788a89a4425950f6b0d70eaafc6f7b5..750f937b72407bbdd9e827c41093c4edc86b5bd9 100644
--- a/app/models/concerns/statuseable.rb
+++ b/app/models/concerns/statuseable.rb
@@ -1,18 +1,22 @@
 module Statuseable
   extend ActiveSupport::Concern
 
-  AVAILABLE_STATUSES = %w(pending running success failed canceled skipped)
+  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]
 
   class_methods do
     def status_sql
-      builds = all.select('count(*)').to_sql
-      success = all.success.select('count(*)').to_sql
-      ignored = all.ignored.select('count(*)').to_sql if all.respond_to?(:ignored)
+      scope = all.relevant
+      builds = scope.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 = all.pending.select('count(*)').to_sql
-      running = all.running.select('count(*)').to_sql
-      canceled = all.canceled.select('count(*)').to_sql
-      skipped = all.skipped.select('count(*)').to_sql
+      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
 
       deduce_status = "(CASE
         WHEN (#{builds})=0 THEN NULL
@@ -31,11 +35,6 @@ module Statuseable
       all.pluck(self.status_sql).first
     end
 
-    def duration
-      duration_array = all.map(&:duration).compact
-      duration_array.reduce(:+)
-    end
-
     def started_at
       all.minimum(:started_at)
     end
@@ -48,7 +47,8 @@ module Statuseable
   included do
     validates :status, inclusion: { in: AVAILABLE_STATUSES }
 
-    state_machine :status, initial: :pending do
+    state_machine :status, initial: :created do
+      state :created, value: 'created'
       state :pending, value: 'pending'
       state :running, value: 'running'
       state :failed, value: 'failed'
@@ -57,6 +57,8 @@ module Statuseable
       state :skipped, value: 'skipped'
     end
 
+    scope :created, -> { where(status: 'created') }
+    scope :relevant, -> { where.not(status: 'created') }
     scope :running, -> { where(status: 'running') }
     scope :pending, -> { where(status: 'pending') }
     scope :success, -> { where(status: 'success') }
@@ -68,14 +70,24 @@ module Statuseable
   end
 
   def started?
-    !pending? && !canceled? && started_at
+    STARTED_STATUSES.include?(status) && started_at
   end
 
   def active?
-    running? || pending?
+    ACTIVE_STATUSES.include?(status)
   end
 
   def complete?
-    canceled? || success? || failed?
+    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/deployment.rb b/app/models/deployment.rb
index 1a7cd60817e7505fdc40728091efb6f53a843283..1e338889714ccd5b69980fc5ad57a725258a0b5c 100644
--- a/app/models/deployment.rb
+++ b/app/models/deployment.rb
@@ -36,4 +36,10 @@ class Deployment < ActiveRecord::Base
   def manual_actions
     deployable.try(:other_actions)
   end
+
+  def includes_commit?(commit)
+    return false unless commit
+
+    project.repository.is_ancestor?(commit.id, sha)
+  end
 end
diff --git a/app/models/diff_note.rb b/app/models/diff_note.rb
index 9671955db360c77283082556fc3ba9314b1dc3be..c8320ff87fa735cef0e79ea80f91e0242753928b 100644
--- a/app/models/diff_note.rb
+++ b/app/models/diff_note.rb
@@ -9,11 +9,16 @@ 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
 
+  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
@@ -30,14 +35,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,19 +60,69 @@ 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?
 
-    diff_refs ||= self.noteable.diff_refs
+    diff_refs ||= noteable_diff_refs
 
     self.position.diff_refs == diff_refs
   end
 
+  def resolvable?
+    !system? && for_merge_request?
+  end
+
+  def resolved?
+    return false unless resolvable?
+
+    self.resolved_at.present?
+  end
+
+  def resolve!(current_user)
+    return unless resolvable?
+    return if resolved?
+
+    self.resolved_at = Time.now
+    self.resolved_by = current_user
+    save!
+  end
+
+  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
+
+  def to_discussion
+    Discussion.new([self])
+  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
+    if noteable.respond_to?(:diff_sha_refs)
+      noteable.diff_sha_refs
+    else
+      noteable.diff_refs
+    end
   end
 
   def set_original_position
@@ -86,6 +133,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?
@@ -96,7 +163,7 @@ class DiffNote < Note
       self.project,
       nil,
       old_diff_refs: self.position.diff_refs,
-      new_diff_refs: self.noteable.diff_refs,
+      new_diff_refs: noteable_diff_refs,
       paths: self.position.paths
     ).execute(self)
   end
diff --git a/app/models/discussion.rb b/app/models/discussion.rb
index 74facfd1c9c7066afd07b99386c933d72702dde5..9676bc034708630130e3fe79b8d63f3c47830f74 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 :first_note, :last_note, :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)
@@ -30,13 +37,30 @@ class Discussion
 
   def initialize(notes)
     @first_note = notes.first
+    @last_note = notes.last
     @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,12 +69,74 @@ class Discussion
     notes.any?(&:legacy_diff_note?)
   end
 
+  def resolvable?
+    return @resolvable if defined?(@resolvable)
+
+    @resolvable = diff_discussion? && notes.any?(&:resolvable?)
+  end
+
+  def resolved?
+    return @resolved if defined?(@resolved)
+
+    @resolved = resolvable? && notes.none?(&:to_be_resolved?)
+  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?
+
+    notes.each do |note|
+      note.resolve!(current_user) if note.resolvable?
+    end
+  end
+
+  def unresolve!
+    return unless resolvable?
+
+    notes.each do |note|
+      note.unresolve! if note.resolvable?
+    end
+  end
+
   def for_target?(target)
     self.noteable == target && !diff_discussion?
   end
 
+  def active?
+    return @active if defined?(@active)
+
+    @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
diff --git a/app/models/environment.rb b/app/models/environment.rb
index baed106e8c884e313ddc587f9572d107da840f5a..75e6f869786e5bbc513ce1a677b0889fe97a6bcc 100644
--- a/app/models/environment.rb
+++ b/app/models/environment.rb
@@ -25,4 +25,10 @@ class Environment < ActiveRecord::Base
   def nullify_external_url
     self.external_url = nil if self.external_url.blank?
   end
+
+  def includes_commit?(commit)
+    return false unless last_deployment
+
+    last_deployment.includes_commit?(commit)
+  end
 end
diff --git a/app/models/group.rb b/app/models/group.rb
index 37631b997014c644d467827dcd0986a464f5e125..c48869ae465ea904f7b18b6af8ff120bef03ea12 100644
--- a/app/models/group.rb
+++ b/app/models/group.rb
@@ -95,34 +95,40 @@ class Group < Namespace
     end
   end
 
-  def add_users(user_ids, access_level, current_user = nil)
+  def add_users(user_ids, access_level, current_user: nil, expires_at: nil)
     user_ids.each do |user_id|
-      Member.add_user(self.group_members, user_id, access_level, current_user)
+      Member.add_user(
+        self.group_members,
+        user_id,
+        access_level,
+        current_user: current_user,
+        expires_at: expires_at
+      )
     end
   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)
+    add_users([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, Gitlab::Access::GUEST, current_user: current_user)
   end
 
   def add_reporter(user, current_user = nil)
-    add_user(user, Gitlab::Access::REPORTER, current_user)
+    add_user(user, Gitlab::Access::REPORTER, current_user: current_user)
   end
 
   def add_developer(user, current_user = nil)
-    add_user(user, Gitlab::Access::DEVELOPER, current_user)
+    add_user(user, Gitlab::Access::DEVELOPER, current_user: current_user)
   end
 
   def add_master(user, current_user = nil)
-    add_user(user, Gitlab::Access::MASTER, current_user)
+    add_user(user, Gitlab::Access::MASTER, current_user: current_user)
   end
 
   def add_owner(user, current_user = nil)
-    add_user(user, Gitlab::Access::OWNER, current_user)
+    add_user(user, Gitlab::Access::OWNER, current_user: current_user)
   end
 
   def has_owner?(user)
diff --git a/app/models/issue.rb b/app/models/issue.rb
index 11f734cfc6d309de607c8a09338c6370d24eb6ef..788611305fec4b5b9229e556b04aa949336a8229 100644
--- a/app/models/issue.rb
+++ b/app/models/issue.rb
@@ -7,6 +7,7 @@ class Issue < ActiveRecord::Base
   include Sortable
   include Taskable
   include Spammable
+  include FasterCacheKeys
 
   DueDateStruct = Struct.new(:title, :name).freeze
   NoDueDate     = DueDateStruct.new('No Due Date', '0').freeze
@@ -35,6 +36,9 @@ 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') }
 
+  attr_spammable :title, spam_title: true
+  attr_spammable :description, spam_description: true
+
   state_machine :state, initial: :opened do
     event :close do
       transition [:reopened, :opened] => :closed
@@ -261,4 +265,9 @@ class Issue < ActiveRecord::Base
   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/key.rb b/app/models/key.rb
index b9bc38a04364d7fafb5233ac64ee3798ac4a645b..568a60b8af3b53c8143e1640a1bec4ebe39bb2d1 100644
--- a/app/models/key.rb
+++ b/app/models/key.rb
@@ -26,8 +26,9 @@ class Key < ActiveRecord::Base
   end
 
   def publishable_key
-    # Removes anything beyond the keytype and key itself
-    self.key.split[0..1].join(' ')
+    # Strip out the keys comment so we don't leak email addresses
+    # Replace with simple ident of user_name (hostname)
+    self.key.split[0..1].push("#{self.user_name} (#{Gitlab.config.gitlab.host})").join(' ')
   end
 
   # projects that has this key
diff --git a/app/models/label.rb b/app/models/label.rb
index 35e678001dc6c29765fa987a23d5ae45ce202139..a23140b7d64b45e32432b137b157a1d3f8ca7d69 100644
--- a/app/models/label.rb
+++ b/app/models/label.rb
@@ -13,6 +13,8 @@ class Label < ActiveRecord::Base
   default_value_for :color, DEFAULT_COLOR
 
   belongs_to :project
+
+  has_many :lists, dependent: :destroy
   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'
diff --git a/app/models/legacy_diff_note.rb b/app/models/legacy_diff_note.rb
index 865712268a018bbdaccb4f21ea93d6c4943a501c..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,
@@ -85,7 +85,7 @@ class LegacyDiffNote < Note
     return nil unless noteable
     return @diff if defined?(@diff)
 
-    @diff = noteable.diffs(Commit.max_diff_options).find do |d|
+    @diff = noteable.raw_diffs(Commit.max_diff_options).find do |d|
       d.new_path && Digest::SHA1.hexdigest(d.new_path) == diff_file_hash
     end
   end
@@ -116,7 +116,11 @@ class LegacyDiffNote < Note
 
   # Find the diff on noteable that matches our own
   def find_noteable_diff
-    diffs = noteable.diffs(Commit.max_diff_options)
+    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/list.rb b/app/models/list.rb
new file mode 100644
index 0000000000000000000000000000000000000000..eb87decdbc803705671408861990277a76e0355d
--- /dev/null
+++ b/app/models/list.rb
@@ -0,0 +1,34 @@
+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
+
+  private
+
+  def can_be_destroyed
+    destroyable?
+  end
+end
diff --git a/app/models/member.rb b/app/models/member.rb
index 24ab1276ee936f9a81098648802df78c9a8e585a..64e0d33fb208adca2e7c3677adafa93a1347c1de 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
@@ -73,7 +74,7 @@ class Member < ActiveRecord::Base
       user
     end
 
-    def add_user(members, user_id, access_level, current_user = nil)
+    def add_user(members, user_id, access_level, current_user: nil, expires_at: nil)
       user = user_for_id(user_id)
 
       # `user` can be either a User object or an email to be invited
@@ -87,6 +88,7 @@ class Member < ActiveRecord::Base
       if can_update_member?(current_user, member) || project_creator?(member, access_level)
         member.created_by ||= current_user
         member.access_level = access_level
+        member.expires_at = expires_at
 
         member.save
       end
diff --git a/app/models/members/project_member.rb b/app/models/members/project_member.rb
index f39afc61ce95ae5086f1d0b1f07d027b73ee1059..ec2d40eb11c564725d3a8a4890fb2f9a5acf975d 100644
--- a/app/models/members/project_member.rb
+++ b/app/models/members/project_member.rb
@@ -8,6 +8,7 @@ class ProjectMember < Member
   # Make sure project member points only to project as it source
   default_value_for :source_type, SOURCE_TYPE
   validates_format_of :source_type, with: /\AProject\z/
+  validates :access_level, inclusion: { in: Gitlab::Access.values }
   default_scope { where(source_type: SOURCE_TYPE) }
 
   scope :in_project, ->(project) { where(source_id: project.id) }
@@ -21,19 +22,19 @@ class ProjectMember < Member
     # or symbol like :master representing role
     #
     # Ex.
-    #   add_users_into_projects(
+    #   add_users_to_projects(
     #     project_ids,
     #     user_ids,
     #     ProjectMember::MASTER
     #   )
     #
-    #   add_users_into_projects(
+    #   add_users_to_projects(
     #     project_ids,
     #     user_ids,
     #     :master
     #   )
     #
-    def add_users_into_projects(project_ids, user_ids, access, current_user = nil)
+    def add_users_to_projects(project_ids, user_ids, access, current_user: nil, expires_at: nil)
       access_level = if roles_hash.has_key?(access)
                        roles_hash[access]
                      elsif roles_hash.values.include?(access.to_i)
@@ -49,7 +50,13 @@ class ProjectMember < Member
           project = Project.find(project_id)
 
           users.each do |user|
-            Member.add_user(project.project_members, user, access_level, current_user)
+            Member.add_user(
+              project.project_members,
+              user,
+              access_level,
+              current_user: current_user,
+              expires_at: expires_at
+            )
           end
         end
       end
diff --git a/app/models/merge_request.rb b/app/models/merge_request.rb
index f1b9c1d6feb062e206bd177198d78cdd732a104b..5330a07ee35ffd97b8a472cf79ad463b9bd19323 100644
--- a/app/models/merge_request.rb
+++ b/app/models/merge_request.rb
@@ -104,6 +104,7 @@ class MergeRequest < ActiveRecord::Base
   scope :from_project, ->(project) { where(source_project_id: project.id) }
   scope :merged, -> { with_state(:merged) }
   scope :closed_and_merged, -> { with_states(:closed, :merged) }
+  scope :from_source_branches, ->(branches) { where(source_branch: branches) }
 
   scope :join_project, -> { joins(:target_project) }
   scope :references_project, -> { references(:target_project) }
@@ -164,8 +165,16 @@ class MergeRequest < ActiveRecord::Base
     merge_request_diff ? merge_request_diff.first_commit : compare_commits.first
   end
 
-  def diffs(*args)
-    merge_request_diff ? merge_request_diff.diffs(*args) : compare.diffs(*args)
+  def raw_diffs(*args)
+    merge_request_diff ? merge_request_diff.raw_diffs(*args) : compare.raw_diffs(*args)
+  end
+
+  def diffs(diff_options = nil)
+    if self.compare
+      self.compare.diffs(diff_options)
+    else
+      Gitlab::Diff::FileCollection::MergeRequest.new(self, diff_options: diff_options)
+    end
   end
 
   def diff_size
@@ -255,6 +264,19 @@ class MergeRequest < ActiveRecord::Base
     )
   end
 
+  # 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
+      )
+    else
+      diff_refs
+    end
+  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"
@@ -300,6 +322,8 @@ class MergeRequest < ActiveRecord::Base
 
     merge_request_diff.reload_content
 
+    MergeRequests::MergeRequestDiffCacheService.new.execute(self)
+
     new_diff_refs = self.diff_refs
 
     update_diff_notes_positions(
@@ -394,6 +418,32 @@ 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 hook_attrs
     attrs = {
       source: source_project.try(:hook_attrs),
@@ -567,6 +617,14 @@ class MergeRequest < ActiveRecord::Base
     !pipeline || pipeline.success?
   end
 
+  def environments
+    return unless diff_head_commit
+
+    target_project.environments.select do |environment|
+      environment.includes_commit?(diff_head_commit)
+    end
+  end
+
   def state_human_name
     if merged?
       "Merged"
@@ -642,10 +700,21 @@ 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
   end
 
+  def all_pipelines
+    @all_pipelines ||=
+      if diff_head_sha && source_project
+        source_project.pipelines.order(id: :desc).where(sha: commits_sha, ref: source_branch)
+      end
+  end
+
   def merge_commit
     @merge_commit ||= project.commit(merge_commit_sha) if merge_commit_sha
   end
@@ -658,12 +727,12 @@ class MergeRequest < ActiveRecord::Base
     merge_commit
   end
 
-  def support_new_diff_notes?
-    diff_refs && diff_refs.complete?
+  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|
@@ -691,4 +760,26 @@ 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
+      @conflicts_can_be_resolved_in_ui = conflicts.files.each(&:lines)
+    rescue Gitlab::Conflict::Parser::ParserError, Gitlab::Conflict::FileCollection::ConflictSideMissing
+      @conflicts_can_be_resolved_in_ui = false
+    end
+  end
 end
diff --git a/app/models/merge_request_diff.rb b/app/models/merge_request_diff.rb
index 3f520c8f3ffa8146669e94bebfe992299d72d790..32cc6a3bfead9693385210c04a5b11bc3fbe5f53 100644
--- a/app/models/merge_request_diff.rb
+++ b/app/models/merge_request_diff.rb
@@ -33,12 +33,12 @@ class MergeRequestDiff < ActiveRecord::Base
   end
 
   def size
-    real_size.presence || diffs.size
+    real_size.presence || raw_diffs.size
   end
 
-  def diffs(options={})
+  def raw_diffs(options = {})
     if options[:ignore_whitespace_change]
-      @diffs_no_whitespace ||= begin
+      @raw_diffs_no_whitespace ||= begin
         compare = Gitlab::Git::Compare.new(
           repository.raw_repository,
           self.start_commit_sha || self.target_branch_sha,
@@ -47,8 +47,8 @@ class MergeRequestDiff < ActiveRecord::Base
         compare.diffs(options)
       end
     else
-      @diffs ||= {}
-      @diffs[options] ||= load_diffs(st_diffs, options)
+      @raw_diffs ||= {}
+      @raw_diffs[options] ||= load_diffs(st_diffs, options)
     end
   end
 
@@ -82,6 +82,10 @@ class MergeRequestDiff < ActiveRecord::Base
     project.commit(self.head_commit_sha)
   end
 
+  def diff_refs_by_sha?
+    base_commit_sha? && head_commit_sha? && start_commit_sha?
+  end
+
   def compare
     @compare ||=
       begin
diff --git a/app/models/namespace.rb b/app/models/namespace.rb
index 8b52cc824cd6c860cbf9af349cd7575bdb58170b..7c29d27ce9784c954960a7c0bf119b3ed79e5f0c 100644
--- a/app/models/namespace.rb
+++ b/app/models/namespace.rb
@@ -1,4 +1,6 @@
 class Namespace < ActiveRecord::Base
+  acts_as_paranoid
+
   include Sortable
   include Gitlab::ShellAdapter
 
diff --git a/app/models/note.rb b/app/models/note.rb
index b6b2ac6aa42f8a1db950833ab5be79ba28cf018e..f2656df028b2a282573f6b884714a37a7104ae1b 100644
--- a/app/models/note.rb
+++ b/app/models/note.rb
@@ -5,6 +5,7 @@ class Note < ActiveRecord::Base
   include Mentionable
   include Awardable
   include Importable
+  include FasterCacheKeys
 
   # Attribute containing rendered and redacted Markdown as generated by
   # Banzai::ObjectRenderer.
@@ -24,6 +25,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
 
@@ -58,7 +62,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]) }
@@ -69,7 +73,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
@@ -81,13 +87,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.
@@ -128,13 +139,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
@@ -242,4 +256,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/project.rb b/app/models/project.rb
index 83b848ded8be5338965360799bd010d23697f8cc..1855760e694208039f33241fc3ac8f0ee5246c7a 100644
--- a/app/models/project.rb
+++ b/app/models/project.rb
@@ -62,6 +62,8 @@ class Project < ActiveRecord::Base
   belongs_to :group, -> { where(type: Group) }, foreign_key: 'namespace_id'
   belongs_to :namespace
 
+  has_one :board, dependent: :destroy
+
   has_one :last_event, -> {order 'events.created_at DESC'}, class_name: 'Event', foreign_key: 'project_id'
 
   # Project services
@@ -197,6 +199,8 @@ class Project < ActiveRecord::Base
   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) }
 
+  scope :excluding_project, ->(project) { where.not(id: project) }
+
   state_machine :import_status, initial: :none do
     event :import_start do
       transition [:none, :finished] => :started
@@ -379,9 +383,10 @@ class Project < ActiveRecord::Base
       joins(join_body).reorder('join_note_counts.amount DESC')
     end
 
-    # Deletes gitlab project export files older than 24 hours
-    def remove_gitlab_exports!
-      Gitlab::Popen.popen(%W(find #{Gitlab::ImportExport.storage_path} -not -path #{Gitlab::ImportExport.storage_path} -mmin +1440 -delete))
+    def cached_count
+      Rails.cache.fetch('total_project_count', expires_in: 5.minutes) do
+        Project.count
+      end
     end
   end
 
@@ -606,7 +611,10 @@ class Project < ActiveRecord::Base
   end
 
   def new_issue_address(author)
-    if Gitlab::IncomingEmail.enabled? && author
+    # This feature is disabled for the time being.
+    return nil
+
+    if Gitlab::IncomingEmail.enabled? && author # rubocop:disable Lint/UnreachableCode
       Gitlab::IncomingEmail.reply_address(
         "#{path_with_namespace}+#{author.authentication_token}")
     end
@@ -870,10 +878,16 @@ class Project < ActiveRecord::Base
 
   # Check if current branch name is marked as protected in the system
   def protected_branch?(branch_name)
+    return true if empty_repo? && default_branch_protected?
+
     @protected_branches ||= self.protected_branches.to_a
     ProtectedBranch.matching(branch_name, protected_branches: @protected_branches).present?
   end
 
+  def user_can_push_to_empty_repo?(user)
+    !default_branch_protected? || team.max_member_access(user.id) > Gitlab::Access::DEVELOPER
+  end
+
   def forked?
     !(forked_project_link.nil? || forked_project_link.forked_from_project.nil?)
   end
@@ -992,6 +1006,10 @@ class Project < ActiveRecord::Base
     project_members.find_by(user_id: user)
   end
 
+  def add_user(user, access_level, current_user: nil, expires_at: nil)
+    team.add_user(user, access_level, current_user: current_user, expires_at: expires_at)
+  end
+
   def default_branch
     @default_branch ||= repository.root_ref if repository.exists?
   end
@@ -1154,16 +1172,6 @@ class Project < ActiveRecord::Base
     @wiki ||= ProjectWiki.new(self, self.owner)
   end
 
-  def schedule_delete!(user_id, params)
-    # Queue this task for after the commit, so once we mark pending_delete it will run
-    run_after_commit do
-      job_id = ProjectDestroyWorker.perform_async(id, user_id, params)
-      Rails.logger.info("User #{user_id} scheduled destruction of project #{path_with_namespace} with job ID #{job_id}")
-    end
-
-    update_attribute(:pending_delete, true)
-  end
-
   def running_or_pending_build_count(force: false)
     Rails.cache.fetch(['projects', id, 'running_or_pending_build_count'], force: force) do
       builds.running_or_pending.count(:all)
@@ -1265,6 +1273,11 @@ class Project < ActiveRecord::Base
 
   private
 
+  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
+  end
+
   def authorized_for_user_by_group?(user, min_access_level)
     member = user.group_members.find_by(source_id: group)
 
diff --git a/app/models/project_group_link.rb b/app/models/project_group_link.rb
index e52a6bd7c8473b69054824db5724ae4d12d77954..7613cbdea93751fe78dfe9cbbf99484588f479ae 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
@@ -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_services/builds_email_service.rb b/app/models/project_services/builds_email_service.rb
index 5e166471077ca69cf939591f81f74d9d8fca78b4..fa66e5864b8b002efd92d17aa737acc96a947a48 100644
--- a/app/models/project_services/builds_email_service.rb
+++ b/app/models/project_services/builds_email_service.rb
@@ -51,8 +51,7 @@ class BuildsEmailService < Service
   end
 
   def test_data(project = nil, user = nil)
-    build = project.builds.last
-    Gitlab::BuildDataBuilder.build(build)
+    Gitlab::DataBuilder::Build.build(project.builds.last)
   end
 
   def fields
diff --git a/app/models/project_services/campfire_service.rb b/app/models/project_services/campfire_service.rb
index 511b2eac792dbd1e0cdb0b92fc0d3673f9dd9b8d..5af93860d09df945945a21ed4621a6cbb7b97a71 100644
--- a/app/models/project_services/campfire_service.rb
+++ b/app/models/project_services/campfire_service.rb
@@ -1,4 +1,6 @@
 class CampfireService < Service
+  include HTTParty
+
   prop_accessor :token, :subdomain, :room
   validates :token, presence: true, if: :activated?
 
@@ -29,18 +31,53 @@ class CampfireService < Service
   def execute(data)
     return unless supported_events.include?(data[:object_kind])
 
-    room = gate.find_room_by_name(self.room)
-    return true unless room
-
+    self.class.base_uri base_uri
     message = build_message(data)
-
-    room.speak(message)
+    speak(self.room, message, auth)
   end
 
   private
 
-  def gate
-    @gate ||= Tinder::Campfire.new(subdomain, token: token)
+  def base_uri
+    @base_uri ||= "https://#{subdomain}.campfirenow.com"
+  end
+
+  def auth
+    # use a dummy password, as explained in the Campfire API doc:
+    # https://github.com/basecamp/campfire-api#authentication
+    @auth ||= {
+      basic_auth: {
+        username: token,
+        password: 'X'
+      }
+    }
+  end
+
+  # Post a message into a room, returns the message Hash in case of success.
+  # Returns nil otherwise.
+  # https://github.com/basecamp/campfire-api/blob/master/sections/messages.md#create-message
+  def speak(room_name, message, auth)
+    room = rooms(auth).find { |r| r["name"] == room_name }
+    return nil unless room
+
+    path = "/room/#{room["id"]}/speak.json"
+    body = {
+      body: {
+        message: {
+          type: 'TextMessage',
+          body: message
+        }
+      }
+    }
+    res = self.class.post(path, auth.merge(body))
+    res.code == 201 ? res : nil
+  end
+
+  # Returns a list of rooms, or [].
+  # https://github.com/basecamp/campfire-api/blob/master/sections/rooms.md#get-rooms
+  def rooms(auth)
+    res = self.class.get("/rooms.json", auth) 
+    res.code == 200 ? res["rooms"] : []
   end
 
   def build_message(push)
diff --git a/app/models/project_services/pivotaltracker_service.rb b/app/models/project_services/pivotaltracker_service.rb
index ad19b7795da86dab391d2c2358088b14779203e1..5301f9fa0ff600aa49ef7c5b7d68a1ea89ee2687 100644
--- a/app/models/project_services/pivotaltracker_service.rb
+++ b/app/models/project_services/pivotaltracker_service.rb
@@ -1,7 +1,9 @@
 class PivotaltrackerService < Service
   include HTTParty
 
-  prop_accessor :token
+  API_ENDPOINT = 'https://www.pivotaltracker.com/services/v5/source_commits'
+
+  prop_accessor :token, :restrict_to_branch
   validates :token, presence: true, if: :activated?
 
   def title
@@ -18,7 +20,17 @@ class PivotaltrackerService < Service
 
   def fields
     [
-      { type: 'text', name: 'token', placeholder: '' }
+      {
+        type: 'text',
+        name: 'token',
+        placeholder: 'Pivotal Tracker API token.'
+      },
+      {
+        type: 'text',
+        name: 'restrict_to_branch',
+        placeholder: 'Comma-separated list of branches which will be ' \
+          'automatically inspected. Leave blank to include all branches.'
+      }
     ]
   end
 
@@ -28,8 +40,8 @@ class PivotaltrackerService < Service
 
   def execute(data)
     return unless supported_events.include?(data[:object_kind])
+    return unless allowed_branch?(data[:ref])
 
-    url = 'https://www.pivotaltracker.com/services/v5/source_commits'
     data[:commits].each do |commit|
       message = {
         'source_commit' => {
@@ -40,7 +52,7 @@ class PivotaltrackerService < Service
         }
       }
       PivotaltrackerService.post(
-        url,
+        API_ENDPOINT,
         body: message.to_json,
         headers: {
           'Content-Type' => 'application/json',
@@ -49,4 +61,15 @@ class PivotaltrackerService < Service
       )
     end
   end
+
+  private
+
+  def allowed_branch?(ref)
+    return true unless ref.present? && restrict_to_branch.present?
+
+    branch = Gitlab::Git.ref_name(ref)
+    allowed_branches = restrict_to_branch.split(',').map(&:strip)
+
+    branch.present? && allowed_branches.include?(branch)
+  end
 end
diff --git a/app/models/project_team.rb b/app/models/project_team.rb
index fdfaf0527301079373a9abdf56b14754241dea34..ab6ea2aae36b0e362dc1b5900d4c8846d9188493 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,18 @@ class ProjectTeam
     member
   end
 
-  def add_users(users, access, current_user = nil)
-    ProjectMember.add_users_into_projects(
+  def add_users(users, access, current_user: nil, expires_at: nil)
+    ProjectMember.add_users_to_projects(
       [project.id],
       users,
       access,
-      current_user
+      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, current_user: nil, expires_at: nil)
+    add_users([user], access, current_user: current_user, expires_at: expires_at)
   end
 
   # Remove all users from project team
@@ -138,8 +139,13 @@ class ProjectTeam
   def max_member_access_for_user_ids(user_ids)
     user_ids = user_ids.uniq
     key = "max_member_access:#{project.id}"
-    RequestStore.store[key] ||= {}
-    access = RequestStore.store[key]
+
+    access = {}
+
+    if RequestStore.active?
+      RequestStore.store[key] ||= {}
+      access = RequestStore.store[key]
+    end
 
     # Lookup only the IDs we need
     user_ids = user_ids - access.keys
diff --git a/app/models/project_wiki.rb b/app/models/project_wiki.rb
index a255710f57711952d797d5ae5a8af20f27c0d579..46f70da2452e0294daa4cde6aacd732c83bd8924 100644
--- a/app/models/project_wiki.rb
+++ b/app/models/project_wiki.rb
@@ -56,6 +56,10 @@ class ProjectWiki
     end
   end
 
+  def repository_exists?
+    !!repository.exists?
+  end
+
   def empty?
     pages.empty?
   end
diff --git a/app/models/protected_branch.rb b/app/models/protected_branch.rb
index 226b3f54342134d15ef5a96e3c2ea5e67701a0d6..6240912a6e1aff65b4e0daeaeea4d4d92329690a 100644
--- a/app/models/protected_branch.rb
+++ b/app/models/protected_branch.rb
@@ -5,11 +5,14 @@ class ProtectedBranch < ActiveRecord::Base
   validates :name, presence: true
   validates :project, presence: true
 
-  has_one :merge_access_level, dependent: :destroy
-  has_one :push_access_level, dependent: :destroy
+  has_many :merge_access_levels, dependent: :destroy
+  has_many :push_access_levels, dependent: :destroy
 
-  accepts_nested_attributes_for :push_access_level
-  accepts_nested_attributes_for :merge_access_level
+  validates_length_of :merge_access_levels, is: 1, message: "are restricted to a single instance per protected branch."
+  validates_length_of :push_access_levels, is: 1, message: "are restricted to a single instance per protected branch."
+
+  accepts_nested_attributes_for :push_access_levels
+  accepts_nested_attributes_for :merge_access_levels
 
   def commit
     project.commit(self.name)
diff --git a/app/models/protected_branch/merge_access_level.rb b/app/models/protected_branch/merge_access_level.rb
index b1112ee737df98da6cac950331dba06aeec03c27..806b3ccd27591421c5718bac60d77c21a1848b43 100644
--- a/app/models/protected_branch/merge_access_level.rb
+++ b/app/models/protected_branch/merge_access_level.rb
@@ -1,4 +1,6 @@
 class ProtectedBranch::MergeAccessLevel < ActiveRecord::Base
+  include ProtectedBranchAccess
+
   belongs_to :protected_branch
   delegate :project, to: :protected_branch
 
@@ -17,8 +19,4 @@ class ProtectedBranch::MergeAccessLevel < ActiveRecord::Base
 
     project.team.max_member_access(user.id) >= access_level
   end
-
-  def humanize
-    self.class.human_access_levels[self.access_level]
-  end
 end
diff --git a/app/models/protected_branch/push_access_level.rb b/app/models/protected_branch/push_access_level.rb
index 6a5e49cf45394b0176075844c19f33b097a57e8f..92e9c51d883cd50b0ff6828cd94960f2d4fe0f21 100644
--- a/app/models/protected_branch/push_access_level.rb
+++ b/app/models/protected_branch/push_access_level.rb
@@ -1,4 +1,6 @@
 class ProtectedBranch::PushAccessLevel < ActiveRecord::Base
+  include ProtectedBranchAccess
+
   belongs_to :protected_branch
   delegate :project, to: :protected_branch
 
@@ -20,8 +22,4 @@ class ProtectedBranch::PushAccessLevel < ActiveRecord::Base
 
     project.team.max_member_access(user.id) >= access_level
   end
-
-  def humanize
-    self.class.human_access_levels[self.access_level]
-  end
 end
diff --git a/app/models/repository.rb b/app/models/repository.rb
index bac37483c470562ada62eadeb88d41aff641ab00..bdc3b9d1c1cb5c47809fdb4c274f47e543c86676 100644
--- a/app/models/repository.rb
+++ b/app/models/repository.rb
@@ -277,7 +277,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.
@@ -372,7 +372,7 @@ class Repository
     # We don't want to flush the cache if the commit didn't actually make any
     # changes to any of the possible avatar files.
     if revision && commit = self.commit(revision)
-      return unless commit.diffs.
+      return unless commit.raw_diffs(deltas_only: true).
         any? { |diff| AVATAR_FILES.include?(diff.new_path) }
     end
 
@@ -391,6 +391,8 @@ class Repository
     expire_exists_cache
     expire_root_ref_cache
     expire_emptiness_caches
+
+    repository_event(:create_repository)
   end
 
   # Runs code just before a repository is deleted.
@@ -407,6 +409,8 @@ class Repository
     expire_root_ref_cache
     expire_emptiness_caches
     expire_exists_cache
+
+    repository_event(:remove_repository)
   end
 
   # Runs code just before the HEAD of a repository is changed.
@@ -414,6 +418,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,12 +427,16 @@ 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
@@ -443,6 +453,8 @@ class Repository
   # 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 +462,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 +553,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?
 
@@ -601,7 +625,7 @@ class Repository
     commit(sha)
   end
 
-  def next_branch(name, opts={})
+  def next_branch(name, opts = {})
     branch_ids = self.branch_names.map do |n|
       next 1 if n == name
       result = n.match(/\A#{name}-([0-9]+)\z/)
@@ -636,9 +660,7 @@ class Repository
   def tags_sorted_by(value)
     case value
     when 'name'
-      # Would be better to use `sort_by` but `version_sorter` only exposes
-      # `sort` and `rsort`
-      VersionSorter.rsort(tag_names).map { |tag_name| find_tag(tag_name) }
+      VersionSorter.rsort(tags) { |tag| tag.name }
     when 'updated_desc'
       tags_sorted_by_committed_date.reverse
     when 'updated_asc'
@@ -871,6 +893,14 @@ class Repository
     end
   end
 
+  def resolve_conflicts(user, branch, params)
+    commit_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
     args       = [commit.id, source_sha]
@@ -1061,4 +1091,8 @@ class Repository
   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 e4cd44f542abe84df42ead069ffbfe112297e73b..09b4717a523e63456bcfc26a5444d1e78e37020c 100644
--- a/app/models/service.rb
+++ b/app/models/service.rb
@@ -80,7 +80,7 @@ class Service < ActiveRecord::Base
   end
 
   def test_data(project, user)
-    Gitlab::PushDataBuilder.build_sample(project, user)
+    Gitlab::DataBuilder::Push.build_sample(project, user)
   end
 
   def event_channel_names
diff --git a/app/models/spam_log.rb b/app/models/spam_log.rb
index 12df68ef83bf17771b16f079216c9b9fe3cd3a97..3b8b9833565100bd0b091085b4b0b5763442f42e 100644
--- a/app/models/spam_log.rb
+++ b/app/models/spam_log.rb
@@ -7,4 +7,8 @@ class SpamLog < ActiveRecord::Base
     user.block
     user.destroy
   end
+
+  def text
+    [title, description].join("\n")
+  end
 end
diff --git a/app/models/spam_report.rb b/app/models/spam_report.rb
deleted file mode 100644
index cdc7321b08e7cd5091d1a8cc23b5179f23f79e02..0000000000000000000000000000000000000000
--- a/app/models/spam_report.rb
+++ /dev/null
@@ -1,5 +0,0 @@
-class SpamReport < ActiveRecord::Base
-  belongs_to :user
-
-  validates :user, presence: true
-end
diff --git a/app/models/todo.rb b/app/models/todo.rb
index 8d7a5965aa15f4698fc869c3b39a3edadd39601c..6ae9956ade5f61ea16aa2318a2dbd9614bcb2812 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,23 @@ 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
+      highest_priority = highest_label_priority(["Issue", "MergeRequest"], "todos.target_id").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/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 db7474349598bce9670532c851ebb44d518e66c4..48e83ab7e56278a22f3f20cd429d8e5dd884c112 100644
--- a/app/models/user.rb
+++ b/app/models/user.rb
@@ -23,13 +23,13 @@ class User < ActiveRecord::Base
   default_value_for :theme_id, gitlab_config.default_theme
 
   attr_encrypted :otp_secret,
-    key:       Gitlab::Application.config.secret_key_base,
+    key:       Gitlab::Application.secrets.otp_key_base,
     mode:      :per_attribute_iv_and_salt,
     insecure_mode: true,
     algorithm: 'aes-256-cbc'
 
   devise :two_factor_authenticatable,
-         otp_secret_encryption_key: Gitlab::Application.config.secret_key_base
+         otp_secret_encryption_key: Gitlab::Application.secrets.otp_key_base
 
   devise :two_factor_backupable, otp_number_of_backup_codes: 10
   serialize :otp_backup_codes, JSON
@@ -429,6 +429,13 @@ class User < ActiveRecord::Base
                     owned_groups.select(:id), namespace.id).joins(:namespace)
   end
 
+  # Returns projects which user can admin issues on (for example to move an issue to that project).
+  #
+  # 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)
+  end
+
   def is_admin?
     admin
   end
@@ -809,13 +816,13 @@ class User < ActiveRecord::Base
 
   def todos_done_count(force: false)
     Rails.cache.fetch(['users', id, 'todos_done_count'], force: force) do
-      todos.done.count
+      TodosFinder.new(self, state: :done).execute.count
     end
   end
 
   def todos_pending_count(force: false)
     Rails.cache.fetch(['users', id, 'todos_pending_count'], force: force) do
-      todos.pending.count
+      TodosFinder.new(self, state: :pending).execute.count
     end
   end
 
diff --git a/app/models/user_agent_detail.rb b/app/models/user_agent_detail.rb
new file mode 100644
index 0000000000000000000000000000000000000000..0949c6ef08344092758e48d80093526f9de573ae
--- /dev/null
+++ b/app/models/user_agent_detail.rb
@@ -0,0 +1,9 @@
+class UserAgentDetail < ActiveRecord::Base
+  belongs_to :subject, polymorphic: true
+
+  validates :user_agent, :ip_address, :subject_id, :subject_type, presence: true
+
+  def submittable?
+    !submitted?
+  end
+end
diff --git a/app/services/akismet_service.rb b/app/services/akismet_service.rb
new file mode 100644
index 0000000000000000000000000000000000000000..5c60addbe7c4d120e15c62d8f33654d38ec38081
--- /dev/null
+++ b/app/services/akismet_service.rb
@@ -0,0 +1,79 @@
+class AkismetService
+  attr_accessor :owner, :text, :options
+
+  def initialize(owner, text, options = {})
+    @owner = owner
+    @text = text
+    @options = options
+  end
+
+  def is_spam?
+    return false unless akismet_enabled?
+
+    params = {
+      type: 'comment',
+      text: text,
+      created_at: DateTime.now,
+      author: owner.name,
+      author_email: owner.email,
+      referrer: options[:referrer],
+    }
+
+    begin
+      is_spam, is_blatant = akismet_client.check(options[:ip_address], options[:user_agent], params)
+      is_spam || is_blatant
+    rescue => e
+      Rails.logger.error("Unable to connect to Akismet: #{e}, skipping check")
+      false
+    end
+  end
+
+  def submit_ham
+    return false unless akismet_enabled?
+
+    params = {
+      type: 'comment',
+      text: text,
+      author: owner.name,
+      author_email: owner.email
+    }
+
+    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
+  end
+
+  def submit_spam
+    return false unless akismet_enabled?
+
+    params = {
+      type: 'comment',
+      text: text,
+      author: owner.name,
+      author_email: owner.email
+    }
+
+    begin
+      akismet_client.submit_spam(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/boards/base_service.rb b/app/services/boards/base_service.rb
new file mode 100644
index 0000000000000000000000000000000000000000..b2069ca825a3c3370a11c06351ddacd53ccd5401
--- /dev/null
+++ b/app/services/boards/base_service.rb
@@ -0,0 +1,5 @@
+module Boards
+  class BaseService < ::BaseService
+    delegate :board, to: :project
+  end
+end
diff --git a/app/services/boards/create_service.rb b/app/services/boards/create_service.rb
new file mode 100644
index 0000000000000000000000000000000000000000..072a07492850863c24e411c6104be0e37de12c6f
--- /dev/null
+++ b/app/services/boards/create_service.rb
@@ -0,0 +1,16 @@
+module Boards
+  class CreateService < Boards::BaseService
+    def execute
+      create_board! unless project.board.present?
+      project.board
+    end
+
+    private
+
+    def create_board!
+      project.create_board
+      project.board.lists.create(list_type: :backlog)
+      project.board.lists.create(list_type: :done)
+    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..435a8c6e6819849f6aa1bb3a38a434b42ac57deb
--- /dev/null
+++ b/app/services/boards/issues/list_service.rb
@@ -0,0 +1,63 @@
+module Boards
+  module Issues
+    class ListService < Boards::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 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..84dc3f70e7661021c1fc94e4e61482748cf9aa54
--- /dev/null
+++ b/app/services/boards/issues/move_service.rb
@@ -0,0 +1,59 @@
+module Boards
+  module Issues
+    class MoveService < Boards::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 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
+            board.lists.movable.pluck(:label_id)
+          end
+
+        Array(label_ids).compact
+      end
+    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..5cb408b9d2021ae369ae63b897daa42c2cc6919c
--- /dev/null
+++ b/app/services/boards/lists/create_service.rb
@@ -0,0 +1,22 @@
+module Boards
+  module Lists
+    class CreateService < Boards::BaseService
+      def execute
+        List.transaction do
+          create_list_at(next_position)
+        end
+      end
+
+      private
+
+      def next_position
+        max_position = board.lists.movable.maximum(:position)
+        max_position.nil? ? 0 : max_position.succ
+      end
+
+      def create_list_at(position)
+        board.lists.create(params.merge(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..25da3bfb56dd7678af2e17dd62164be65e976e89
--- /dev/null
+++ b/app/services/boards/lists/destroy_service.rb
@@ -0,0 +1,25 @@
+module Boards
+  module Lists
+    class DestroyService < Boards::BaseService
+      def execute(list)
+        return false unless list.destroyable?
+
+        list.with_lock do
+          decrement_higher_lists(list)
+          remove_list(list)
+        end
+      end
+
+      private
+
+      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..1c48b9786e464a8c7619023b56cf5af2c4d87121
--- /dev/null
+++ b/app/services/boards/lists/generate_service.rb
@@ -0,0 +1,36 @@
+module Boards
+  module Lists
+    class GenerateService < Boards::BaseService
+      def execute
+        return false unless board.lists.movable.empty?
+
+        List.transaction do
+          label_params.each { |params| create_list(params) }
+        end
+
+        true
+      end
+
+      private
+
+      def create_list(params)
+        label = find_or_create_label(params)
+        Lists::CreateService.new(project, current_user, label_id: label.id).execute
+      end
+
+      def find_or_create_label(params)
+        project.labels.create_with(color: params[:color])
+                      .find_or_create_by(name: params[:name])
+      end
+
+      def label_params
+        [
+          { name: 'Development', color: '#5CB85C' },
+          { name: 'Testing',     color: '#F0AD4E' },
+          { name: 'Production',  color: '#FF5F00' },
+          { name: 'Ready',       color: '#FF0000' }
+        ]
+      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..020ff69f4a7a0bf6ad1df958b3b7256b6af73840
--- /dev/null
+++ b/app/services/boards/lists/move_service.rb
@@ -0,0 +1,51 @@
+module Boards
+  module Lists
+    class MoveService < Boards::BaseService
+      def execute(list)
+        @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 :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/create_builds_service.rb b/app/services/ci/create_builds_service.rb
deleted file mode 100644
index 4946f7076fdd515ee1781bccd9bf2af28336ca1d..0000000000000000000000000000000000000000
--- a/app/services/ci/create_builds_service.rb
+++ /dev/null
@@ -1,62 +0,0 @@
-module Ci
-  class CreateBuildsService
-    def initialize(pipeline)
-      @pipeline = pipeline
-      @config = pipeline.config_processor
-    end
-
-    def execute(stage, user, status, trigger_request = nil)
-      builds_attrs = @config.builds_for_stage_and_ref(stage, @pipeline.ref, @pipeline.tag, trigger_request)
-
-      # check when to create next build
-      builds_attrs = builds_attrs.select do |build_attrs|
-        case build_attrs[:when]
-        when 'on_success'
-          status == 'success'
-        when 'on_failure'
-          status == 'failed'
-        when 'always', 'manual'
-          %w(success failed).include?(status)
-        end
-      end
-
-      # don't create the same build twice
-      builds_attrs.reject! do |build_attrs|
-        @pipeline.builds.find_by(ref: @pipeline.ref,
-                                 tag: @pipeline.tag,
-                                 trigger_request: trigger_request,
-                                 name: build_attrs[:name])
-      end
-
-      builds_attrs.map do |build_attrs|
-        build_attrs.slice!(:name,
-                           :commands,
-                           :tag_list,
-                           :options,
-                           :allow_failure,
-                           :stage,
-                           :stage_idx,
-                           :environment,
-                           :when,
-                           :yaml_variables)
-
-        build_attrs.merge!(pipeline: @pipeline,
-                           ref: @pipeline.ref,
-                           tag: @pipeline.tag,
-                           trigger_request: trigger_request,
-                           user: user,
-                           project: @pipeline.project)
-
-        # TODO: The proper implementation for this is in
-        # https://gitlab.com/gitlab-org/gitlab-ce/merge_requests/5295
-        build_attrs[:status] = 'skipped' if build_attrs[:when] == 'manual'
-
-        ##
-        # We do not persist new builds here.
-        # Those will be persisted when @pipeline is saved.
-        #
-        @pipeline.builds.new(build_attrs)
-      end
-    end
-  end
-end
diff --git a/app/services/ci/create_pipeline_builds_service.rb b/app/services/ci/create_pipeline_builds_service.rb
new file mode 100644
index 0000000000000000000000000000000000000000..005014fa1ded2e94b350f06aad0ec2bf1613106f
--- /dev/null
+++ b/app/services/ci/create_pipeline_builds_service.rb
@@ -0,0 +1,42 @@
+module Ci
+  class CreatePipelineBuildsService < BaseService
+    attr_reader :pipeline
+
+    def execute(pipeline)
+      @pipeline = pipeline
+
+      new_builds.map do |build_attributes|
+        create_build(build_attributes)
+      end
+    end
+
+    private
+
+    def create_build(build_attributes)
+      build_attributes = build_attributes.merge(
+        pipeline: pipeline,
+        project: pipeline.project,
+        ref: pipeline.ref,
+        tag: pipeline.tag,
+        user: current_user,
+        trigger_request: trigger_request
+      )
+      pipeline.builds.create(build_attributes)
+    end
+
+    def new_builds
+      @new_builds ||= pipeline.config_builds_attributes.
+        reject { |build| existing_build_names.include?(build[:name]) }
+    end
+
+    def existing_build_names
+      @existing_build_names ||= pipeline.builds.pluck(:name)
+    end
+
+    def trigger_request
+      return @trigger_request if defined?(@trigger_request)
+
+      @trigger_request ||= pipeline.trigger_requests.first
+    end
+  end
+end
diff --git a/app/services/ci/create_pipeline_service.rb b/app/services/ci/create_pipeline_service.rb
index 7a8b0683acbbc342fdbf47f24ed93ae845943e17..cde856b0186833916b5848f0762e8fdfedf3b725 100644
--- a/app/services/ci/create_pipeline_service.rb
+++ b/app/services/ci/create_pipeline_service.rb
@@ -1,50 +1,101 @@
 module Ci
   class CreatePipelineService < BaseService
-    def execute
-      pipeline = project.pipelines.new(params)
-      pipeline.user = current_user
+    attr_reader :pipeline
 
-      unless ref_names.include?(params[:ref])
-        pipeline.errors.add(:base, 'Reference not found')
-        return pipeline
+    def execute(ignore_skip_ci: false, save_on_errors: true, trigger_request: nil)
+      @pipeline = Ci::Pipeline.new(
+        project: project,
+        ref: ref,
+        sha: sha,
+        before_sha: before_sha,
+        tag: tag?,
+        trigger_requests: Array(trigger_request),
+        user: current_user
+      )
+
+      unless project.builds_enabled?
+        return error('Pipeline is disabled')
       end
 
-      if commit
-        pipeline.sha = commit.id
-      else
-        pipeline.errors.add(:base, 'Commit not found')
-        return pipeline
+      unless trigger_request || can?(current_user, :create_pipeline, project)
+        return error('Insufficient permissions to create a new pipeline')
       end
 
-      unless can?(current_user, :create_pipeline, project)
-        pipeline.errors.add(:base, 'Insufficient permissions to create a new pipeline')
-        return pipeline
+      unless branch? || tag?
+        return error('Reference not found')
+      end
+
+      unless commit
+        return error('Commit not found')
       end
 
       unless pipeline.config_processor
-        pipeline.errors.add(:base, pipeline.yaml_errors || 'Missing .gitlab-ci.yml file')
-        return pipeline
+        unless pipeline.ci_yaml_file
+          return error('Missing .gitlab-ci.yml file')
+        end
+        return error(pipeline.yaml_errors, save: save_on_errors)
       end
 
-      pipeline.save!
-      pipeline.touch
+      if !ignore_skip_ci && skip_ci?
+        pipeline.skip if save_on_errors
+        return pipeline
+      end
 
-      unless pipeline.create_builds(current_user)
-        pipeline.errors.add(:base, 'No builds for this pipeline.')
+      unless pipeline.config_builds_attributes.present?
+        return error('No builds for this pipeline.')
       end
 
       pipeline.save
+      pipeline.process!
       pipeline
     end
 
     private
 
-    def ref_names
-      @ref_names ||= project.repository.ref_names
+    def skip_ci?
+      pipeline.git_commit_message =~ /\[(ci skip|skip ci)\]/i if pipeline.git_commit_message
     end
 
     def commit
-      @commit ||= project.commit(params[:ref])
+      @commit ||= project.commit(origin_sha || origin_ref)
+    end
+
+    def sha
+      commit.try(:id)
+    end
+
+    def before_sha
+      params[:checkout_sha] || params[:before] || Gitlab::Git::BLANK_SHA
+    end
+
+    def origin_sha
+      params[:checkout_sha] || params[:after]
+    end
+
+    def origin_ref
+      params[:ref]
+    end
+
+    def branch?
+      project.repository.ref_exists?(Gitlab::Git::BRANCH_REF_PREFIX + ref)
+    end
+
+    def tag?
+      project.repository.ref_exists?(Gitlab::Git::TAG_REF_PREFIX + ref)
+    end
+
+    def ref
+      Gitlab::Git.ref_name(origin_ref)
+    end
+
+    def valid_sha?
+      origin_sha && origin_sha != Gitlab::Git::BLANK_SHA
+    end
+
+    def error(message, save: false)
+      pipeline.errors.add(:base, message)
+      pipeline.drop if save
+      pipeline
     end
   end
 end
diff --git a/app/services/ci/create_trigger_request_service.rb b/app/services/ci/create_trigger_request_service.rb
index 1e629cf119aa5120997d1dbfa892b741c38deb1d..6af3c1ca5b130d304bbb7b06f44f3877d931034a 100644
--- a/app/services/ci/create_trigger_request_service.rb
+++ b/app/services/ci/create_trigger_request_service.rb
@@ -1,20 +1,11 @@
 module Ci
   class CreateTriggerRequestService
     def execute(project, trigger, ref, variables = nil)
-      commit = project.commit(ref)
-      return unless commit
+      trigger_request = trigger.trigger_requests.create(variables: variables)
 
-      # check if ref is tag
-      tag = project.repository.find_tag(ref).present?
-
-      pipeline = project.pipelines.create(sha: commit.sha, ref: ref, tag: tag)
-
-      trigger_request = trigger.trigger_requests.create!(
-        variables: variables,
-        pipeline: pipeline,
-      )
-
-      if pipeline.create_builds(nil, trigger_request)
+      pipeline = Ci::CreatePipelineService.new(project, nil, ref: ref).
+        execute(ignore_skip_ci: true, trigger_request: trigger_request)
+      if pipeline.persisted?
         trigger_request
       end
     end
diff --git a/app/services/ci/process_pipeline_service.rb b/app/services/ci/process_pipeline_service.rb
new file mode 100644
index 0000000000000000000000000000000000000000..6f7610d42ba0ced02417ff428267fb0a6e9d34fc
--- /dev/null
+++ b/app/services/ci/process_pipeline_service.rb
@@ -0,0 +1,77 @@
+module Ci
+  class ProcessPipelineService < BaseService
+    attr_reader :pipeline
+
+    def execute(pipeline)
+      @pipeline = pipeline
+
+      # This method will ensure that our pipeline does have all builds for all stages created
+      if created_builds.empty?
+        create_builds!
+      end
+
+      new_builds =
+        stage_indexes_of_created_builds.map do |index|
+          process_stage(index)
+        end
+
+      # Return a flag if a when builds got enqueued
+      new_builds.flatten.any?
+    end
+
+    private
+
+    def create_builds!
+      Ci::CreatePipelineBuildsService.new(project, current_user).execute(pipeline)
+    end
+
+    def process_stage(index)
+      current_status = status_for_prior_stages(index)
+
+      created_builds_in_stage(index).select do |build|
+        process_build(build, current_status)
+      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
+      else
+        build.skip
+        false
+      end
+    end
+
+    def valid_statuses_for_when(value)
+      case value
+      when 'on_success'
+        %w[success]
+      when 'on_failure'
+        %w[failed]
+      when 'always'
+        %w[success failed]
+      else
+        []
+      end
+    end
+
+    def status_for_prior_stages(index)
+      pipeline.builds.where('stage_idx < ?', index).latest.status || 'success'
+    end
+
+    def stage_indexes_of_created_builds
+      created_builds.order(:stage_idx).pluck('distinct stage_idx')
+    end
+
+    def created_builds_in_stage(index)
+      created_builds.where(stage_idx: index)
+    end
+
+    def created_builds
+      pipeline.builds.created
+    end
+  end
+end
diff --git a/app/services/compare_service.rb b/app/services/compare_service.rb
index 149822aa64743ad5637c2058893b6c444d4b7601..6d6075628af89a0c113a2f637af5b728f3009d60 100644
--- a/app/services/compare_service.rb
+++ b/app/services/compare_service.rb
@@ -20,10 +20,12 @@ class CompareService
       )
     end
 
-    Gitlab::Git::Compare.new(
+    raw_compare = Gitlab::Git::Compare.new(
       target_project.repository.raw_repository,
       target_branch,
-      source_sha,
+      source_sha
     )
+
+    Compare.new(raw_compare, target_project)
   end
 end
diff --git a/app/services/create_commit_builds_service.rb b/app/services/create_commit_builds_service.rb
deleted file mode 100644
index 0b66b854deabd245b02259f8cb9cd7e3c424176d..0000000000000000000000000000000000000000
--- a/app/services/create_commit_builds_service.rb
+++ /dev/null
@@ -1,69 +0,0 @@
-class CreateCommitBuildsService
-  def execute(project, user, params)
-    return unless project.builds_enabled?
-
-    before_sha = params[:checkout_sha] || params[:before]
-    sha = params[:checkout_sha] || params[:after]
-    origin_ref = params[:ref]
-
-    ref = Gitlab::Git.ref_name(origin_ref)
-    tag = Gitlab::Git.tag_ref?(origin_ref)
-
-    # Skip branch removal
-    if sha == Gitlab::Git::BLANK_SHA
-      return false
-    end
-
-    @pipeline = Ci::Pipeline.new(
-      project: project,
-      sha: sha,
-      ref: ref,
-      before_sha: before_sha,
-      tag: tag,
-      user: user)
-
-    ##
-    # Skip creating pipeline if no gitlab-ci.yml is found
-    #
-    unless @pipeline.ci_yaml_file
-      return false
-    end
-
-    ##
-    # Skip creating builds for commits that have [ci skip]
-    # but save pipeline object
-    #
-    if @pipeline.skip_ci?
-      return save_pipeline!
-    end
-
-    ##
-    # Skip creating builds when CI config is invalid
-    # but save pipeline object
-    #
-    unless @pipeline.config_processor
-      return save_pipeline!
-    end
-
-    ##
-    # Skip creating pipeline object if there are no builds for it.
-    #
-    unless @pipeline.create_builds(user)
-      @pipeline.errors.add(:base, 'No builds created')
-      return false
-    end
-
-    save_pipeline!
-  end
-
-  private
-
-  ##
-  # Create a new pipeline and touch object to calculate status
-  #
-  def save_pipeline!
-    @pipeline.save!
-    @pipeline.touch
-    @pipeline
-  end
-end
diff --git a/app/services/create_spam_log_service.rb b/app/services/create_spam_log_service.rb
deleted file mode 100644
index 59a66fde47abe81f56260f56dab8fa51af289c45..0000000000000000000000000000000000000000
--- a/app/services/create_spam_log_service.rb
+++ /dev/null
@@ -1,13 +0,0 @@
-class CreateSpamLogService < BaseService
-  def initialize(project, user, params)
-    super(project, user, params)
-  end
-
-  def execute
-    spam_params = params.merge({ user_id: @current_user.id,
-                                 project_id: @project.id } )
-    spam_log = SpamLog.new(spam_params)
-    spam_log.save
-    spam_log
-  end
-end
diff --git a/app/services/delete_branch_service.rb b/app/services/delete_branch_service.rb
index 87f066edb6f10f58f1c2d72d54e096851156e00a..918eddaa53a906844ab13dfb9f50b8f186e6ce62 100644
--- a/app/services/delete_branch_service.rb
+++ b/app/services/delete_branch_service.rb
@@ -39,7 +39,12 @@ class DeleteBranchService < BaseService
   end
 
   def build_push_data(branch)
-    Gitlab::PushDataBuilder
-      .build(project, current_user, branch.target.sha, Gitlab::Git::BLANK_SHA, "#{Gitlab::Git::BRANCH_REF_PREFIX}#{branch.name}", [])
+    Gitlab::DataBuilder::Push.build(
+      project,
+      current_user,
+      branch.target.sha,
+      Gitlab::Git::BLANK_SHA,
+      "#{Gitlab::Git::BRANCH_REF_PREFIX}#{branch.name}",
+      [])
   end
 end
diff --git a/app/services/delete_tag_service.rb b/app/services/delete_tag_service.rb
index 32e0eed6b63644116f74a0391aa49f5299cafc70..d0cb151a010cde5cd721514a3068972c49888afb 100644
--- a/app/services/delete_tag_service.rb
+++ b/app/services/delete_tag_service.rb
@@ -33,7 +33,12 @@ class DeleteTagService < BaseService
   end
 
   def build_push_data(tag)
-    Gitlab::PushDataBuilder
-      .build(project, current_user, tag.target.sha, Gitlab::Git::BLANK_SHA, "#{Gitlab::Git::TAG_REF_PREFIX}#{tag.name}", [])
+    Gitlab::DataBuilder::Push.build(
+      project,
+      current_user,
+      tag.target.sha,
+      Gitlab::Git::BLANK_SHA,
+      "#{Gitlab::Git::TAG_REF_PREFIX}#{tag.name}",
+      [])
   end
 end
diff --git a/app/services/delete_user_service.rb b/app/services/delete_user_service.rb
index ce79287e35a4924f034abeeda22187c558607333..eaff88d64632caec372c6148794107b45d75c48b 100644
--- a/app/services/delete_user_service.rb
+++ b/app/services/delete_user_service.rb
@@ -18,9 +18,14 @@ class DeleteUserService
     user.personal_projects.each do |project|
       # Skip repository removal because we remove directory with namespace
       # that contain all this repositories
-      ::Projects::DestroyService.new(project, current_user, skip_repo: true).pending_delete!
+      ::Projects::DestroyService.new(project, current_user, skip_repo: true).async_execute
     end
 
-    user.destroy
+    # Destroy the namespace after destroying the user since certain methods may depend on the namespace existing
+    namespace = user.namespace
+    user_data = user.destroy
+    namespace.really_destroy!
+
+    user_data
   end
 end
diff --git a/app/services/destroy_group_service.rb b/app/services/destroy_group_service.rb
index 3c42ac61be4e3e4ff070adcd6d404071d0a79cfe..0081364b8aaae9b0f8728c46a21e51b6cf2a6d00 100644
--- a/app/services/destroy_group_service.rb
+++ b/app/services/destroy_group_service.rb
@@ -5,13 +5,23 @@ class DestroyGroupService
     @group, @current_user = group, user
   end
 
+  def async_execute
+    group.transaction do
+      # Soft delete via paranoia gem
+      group.destroy
+      job_id = GroupDestroyWorker.perform_async(group.id, current_user.id)
+      Rails.logger.info("User #{current_user.id} scheduled a deletion of group ID #{group.id} with job ID #{job_id}")
+    end
+  end
+
   def execute
     group.projects.each do |project|
+      # Execute the destruction of the models immediately to ensure atomic cleanup.
       # Skip repository removal because we remove directory with namespace
-      # that contain all this repositories
-      ::Projects::DestroyService.new(project, current_user, skip_repo: true).pending_delete!
+      # that contain all these repositories
+      ::Projects::DestroyService.new(project, current_user, skip_repo: true).execute
     end
 
-    group.destroy
+    group.really_destroy!
   end
 end
diff --git a/app/services/files/base_service.rb b/app/services/files/base_service.rb
index c4a206f785e3b493b30a6b1abc74ec5ddf668902..ea94818713bedf82a9ed59c0c9c32ae4a49cd993 100644
--- a/app/services/files/base_service.rb
+++ b/app/services/files/base_service.rb
@@ -15,6 +15,7 @@ module Files
                         else
                           params[:file_content]
                         end
+      @last_commit_sha = params[:last_commit_sha]
 
       # Validate parameters
       validate
diff --git a/app/services/files/update_service.rb b/app/services/files/update_service.rb
index 8d2b5083179e0ed50c507a355e9067a6d5d05d67..4fc3b64079925085e2bbdc437d9e59585a09dce5 100644
--- a/app/services/files/update_service.rb
+++ b/app/services/files/update_service.rb
@@ -2,11 +2,34 @@ require_relative "base_service"
 
 module Files
   class UpdateService < Files::BaseService
+    class FileChangedError < StandardError; end
+
     def commit
       repository.update_file(current_user, @file_path, @file_content,
                              branch: @target_branch,
                              previous_path: @previous_path,
                              message: @commit_message)
     end
+
+    private
+
+    def validate
+      super
+
+      if file_has_changed?
+        raise FileChangedError.new("You are attempting to update a file that has changed since you started editing it.")
+      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)
+    end
   end
 end
diff --git a/app/services/git_push_service.rb b/app/services/git_push_service.rb
index 3f6a177bf3aff35d55ed58dfa1c05fb26bd66c67..78feb37aa2a653ceac90a046fe32e12ddc95ddfb 100644
--- a/app/services/git_push_service.rb
+++ b/app/services/git_push_service.rb
@@ -69,7 +69,7 @@ class GitPushService < BaseService
     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)
-    CreateCommitBuildsService.new.execute(@project, current_user, build_push_data)
+    Ci::CreatePipelineService.new(project, current_user, build_push_data).execute
     ProjectCacheWorker.perform_async(@project.id)
   end
 
@@ -91,12 +91,12 @@ class GitPushService < BaseService
 
       params = {
         name: @project.default_branch,
-        push_access_level_attributes: {
+        push_access_levels_attributes: [{
           access_level: current_application_settings.default_branch_protection == PROTECTION_DEV_CAN_PUSH ? Gitlab::Access::DEVELOPER : Gitlab::Access::MASTER
-        },
-        merge_access_level_attributes: {
+        }],
+        merge_access_levels_attributes: [{
           access_level: current_application_settings.default_branch_protection == PROTECTION_DEV_CAN_MERGE ? Gitlab::Access::DEVELOPER : Gitlab::Access::MASTER
-        }
+        }]
       }
 
       ProtectedBranches::CreateService.new(@project, current_user, params).execute
@@ -138,13 +138,23 @@ class GitPushService < BaseService
   end
 
   def build_push_data
-    @push_data ||= Gitlab::PushDataBuilder.
-      build(@project, current_user, params[:oldrev], params[:newrev], params[:ref], push_commits)
+    @push_data ||= Gitlab::DataBuilder::Push.build(
+      @project,
+      current_user,
+      params[:oldrev],
+      params[:newrev],
+      params[:ref],
+      push_commits)
   end
 
   def build_push_data_system_hook
-    @push_data_system ||= Gitlab::PushDataBuilder.
-      build(@project, current_user, params[:oldrev], params[:newrev], params[:ref], [])
+    @push_data_system ||= Gitlab::DataBuilder::Push.build(
+      @project,
+      current_user,
+      params[:oldrev],
+      params[:newrev],
+      params[:ref],
+      [])
   end
 
   def push_to_existing_branch?
diff --git a/app/services/git_tag_push_service.rb b/app/services/git_tag_push_service.rb
index 969530c4fdc3839dd965aa7f6536205185a66d66..e6002b03b933a269592590c97d864eee51506b48 100644
--- a/app/services/git_tag_push_service.rb
+++ b/app/services/git_tag_push_service.rb
@@ -11,7 +11,7 @@ class GitTagPushService < BaseService
     SystemHooksService.new.execute_hooks(build_system_push_data.dup, :tag_push_hooks)
     project.execute_hooks(@push_data.dup, :tag_push_hooks)
     project.execute_services(@push_data.dup, :tag_push_hooks)
-    CreateCommitBuildsService.new.execute(project, current_user, @push_data)
+    Ci::CreatePipelineService.new(project, current_user, @push_data).execute
     ProjectCacheWorker.perform_async(project.id)
 
     true
@@ -34,12 +34,24 @@ class GitTagPushService < BaseService
       end
     end
 
-    Gitlab::PushDataBuilder.
-      build(project, current_user, params[:oldrev], params[:newrev], params[:ref], commits, message)
+    Gitlab::DataBuilder::Push.build(
+      project,
+      current_user,
+      params[:oldrev],
+      params[:newrev],
+      params[:ref],
+      commits,
+      message)
   end
 
   def build_system_push_data
-    Gitlab::PushDataBuilder.
-      build(project, current_user, params[:oldrev], params[:newrev], params[:ref], [], '')
+    Gitlab::DataBuilder::Push.build(
+      project,
+      current_user,
+      params[:oldrev],
+      params[:newrev],
+      params[:ref],
+      [],
+      '')
   end
 end
diff --git a/app/services/ham_service.rb b/app/services/ham_service.rb
new file mode 100644
index 0000000000000000000000000000000000000000..b0e1799b48916e7220937fa7ca7518399646448e
--- /dev/null
+++ b/app/services/ham_service.rb
@@ -0,0 +1,26 @@
+class HamService
+  attr_accessor :spam_log
+
+  def initialize(spam_log)
+    @spam_log = spam_log
+  end
+
+  def mark_as_ham!
+    if akismet.submit_ham
+      spam_log.update_attribute(:submitted_as_ham, true)
+    else
+      false
+    end
+  end
+
+  private
+
+  def akismet
+    @akismet ||= AkismetService.new(
+      spam_log.user,
+      spam_log.text,
+      ip_address: spam_log.source_ip,
+      user_agent: spam_log.user_agent
+    )
+  end
+end
diff --git a/app/services/import_export_clean_up_service.rb b/app/services/import_export_clean_up_service.rb
new file mode 100644
index 0000000000000000000000000000000000000000..6442406d77ee99535e117219f661dd11c66221a7
--- /dev/null
+++ b/app/services/import_export_clean_up_service.rb
@@ -0,0 +1,24 @@
+class ImportExportCleanUpService
+  LAST_MODIFIED_TIME_IN_MINUTES = 1440
+
+  attr_reader :mmin, :path
+
+  def initialize(mmin = LAST_MODIFIED_TIME_IN_MINUTES)
+    @mmin = mmin
+    @path = Gitlab::ImportExport.storage_path
+  end
+
+  def execute
+    Gitlab::Metrics.measure(:import_export_clean_up) do
+      return unless File.directory?(path)
+
+      clean_up_export_files
+    end
+  end
+
+  private
+
+  def clean_up_export_files
+    Gitlab::Popen.popen(%W(find #{path} -not -path #{path} -mmin +#{mmin} -delete))
+  end
+end
diff --git a/app/services/issuable_base_service.rb b/app/services/issuable_base_service.rb
index 2d96efe1042c955fc984e0c895ae6ae9e84a6100..e06c37c323ed3bc4e47a67cf64e028f5945e9948 100644
--- a/app/services/issuable_base_service.rb
+++ b/app/services/issuable_base_service.rb
@@ -69,14 +69,9 @@ 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)
-    else
-      filter_labels_in_param(:label_ids)
-    end
+    filter_labels_in_param(:add_label_ids)
+    filter_labels_in_param(:remove_label_ids)
+    filter_labels_in_param(:label_ids)
   end
 
   def filter_labels_in_param(key)
@@ -85,30 +80,90 @@ class IssuableBaseService < BaseService
     params[key] = project.labels.where(id: params[key]).pluck(:id)
   end
 
-  def update_issuable(issuable, attributes)
+  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
+      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 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 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
 
-      issuable.assign_attributes(attributes.merge(updated_by: current_user))
+  def before_create(issuable)
+    # To be overridden by subclasses
+  end
+
+  def after_create(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)
+      handle_changes(issuable, old_labels: old_labels, old_mentioned_users: old_mentioned_users)
       issuable.create_new_cross_references!(current_user)
       execute_hooks(issuable, 'update')
     end
@@ -134,6 +189,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/close_service.rb b/app/services/issues/close_service.rb
index 859c934ea3bf0493195fb6d106bb9c858b4baf13..45cca216ccce68016b13a423295f3854c9c1f020 100644
--- a/app/services/issues/close_service.rb
+++ b/app/services/issues/close_service.rb
@@ -1,6 +1,8 @@
 module Issues
   class CloseService < Issues::BaseService
     def execute(issue, commit: nil, notifications: true, system_note: true)
+      return issue unless can?(current_user, :update_issue, issue)
+
       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 5e2de2ccf64528942cddf0482320ac5f5d7a2483..ea1690f3e381b630425225059dfcb1074a77a5a5 100644
--- a/app/services/issues/create_service.rb
+++ b/app/services/issues/create_service.rb
@@ -1,31 +1,33 @@
 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
+      @request = params.delete(:request)
+      @api = params.delete(:api)
 
-      issue.spam = spam_check_service.execute(request, 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)
-        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
 
-    def spam_check_service
-      SpamCheckService.new(project, current_user, params)
+    def spam_service
+      SpamService.new(@issue, @request)
+    end
+
+    def user_agent_detail_service
+      UserAgentDetailService.new(@issue, @request)
     end
   end
 end
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/members/authorized_destroy_service.rb b/app/services/members/authorized_destroy_service.rb
new file mode 100644
index 0000000000000000000000000000000000000000..ca9db59cac706762c7586ffb5f6fb16308ab6d4c
--- /dev/null
+++ b/app/services/members/authorized_destroy_service.rb
@@ -0,0 +1,19 @@
+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
+    end
+  end
+end
diff --git a/app/services/members/destroy_service.rb b/app/services/members/destroy_service.rb
index 15358f80208ea5f024b79c6b1ba78d2e465e486e..9a2bf82ef516e2ac9b3f1fa6510eb634e8d3ba45 100644
--- a/app/services/members/destroy_service.rb
+++ b/app/services/members/destroy_service.rb
@@ -2,20 +2,16 @@ module Members
   class DestroyService < BaseService
     attr_accessor :member, :current_user
 
-    def initialize(member, user)
-      @member, @current_user = member, user
+    def initialize(member, current_user)
+      @member = member
+      @current_user = current_user
     end
 
     def execute
       unless member && can?(current_user, "destroy_#{member.type.underscore}".to_sym, member)
         raise Gitlab::Access::AccessDeniedError
       end
-
-      member.destroy
-
-      if member.request? && member.user != current_user
-        notification_service.decline_access_request(member)
-      end
+      AuthorizedDestroyService.new(member, current_user).execute
     end
   end
 end
diff --git a/app/services/merge_requests/build_service.rb b/app/services/merge_requests/build_service.rb
index 7fe57747265156320a76b5b4ac84c1b7cd28f19b..290742f1506dda44fa2dda5c6add2d888b900731 100644
--- a/app/services/merge_requests/build_service.rb
+++ b/app/services/merge_requests/build_service.rb
@@ -34,7 +34,7 @@ module MergeRequests
       # 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 = Commit.decorate(commits, merge_request.source_project)
+        merge_request.compare_commits = commits
         merge_request.can_be_created = true
         merge_request.compare = compare
       else
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..73247e62421b3576f5068d52d97bf6b5023e0032 100644
--- a/app/services/merge_requests/create_service.rb
+++ b/app/services/merge_requests/create_service.rb
@@ -7,26 +7,19 @@ 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)
     end
   end
 end
diff --git a/app/services/merge_requests/get_urls_service.rb b/app/services/merge_requests/get_urls_service.rb
new file mode 100644
index 0000000000000000000000000000000000000000..08c1f72d65a1190525a31f14dd49ef0421256b74
--- /dev/null
+++ b/app/services/merge_requests/get_urls_service.rb
@@ -0,0 +1,63 @@
+module MergeRequests
+  class GetUrlsService < BaseService
+    attr_reader :project
+
+    def initialize(project)
+      @project = project
+    end
+
+    def execute(changes)
+      branches = get_branches(changes)
+      merge_requests_map = opened_merge_requests_from_source_branches(branches)
+      branches.map do |branch|
+        existing_merge_request = merge_requests_map[branch]
+        if existing_merge_request
+          url_for_existing_merge_request(existing_merge_request)
+        else
+          url_for_new_merge_request(branch)
+        end
+      end
+    end
+
+    private
+
+    def opened_merge_requests_from_source_branches(branches)
+      merge_requests = MergeRequest.from_project(project).opened.from_source_branches(branches)
+      merge_requests.inject({}) do |hash, mr|
+        hash[mr.source_branch] = mr
+        hash
+      end
+    end
+
+    def get_branches(changes)
+      return [] if project.empty_repo?
+      return [] unless project.merge_requests_enabled
+
+      changes_list = Gitlab::ChangesList.new(changes)
+      changes_list.map do |change|
+        next unless Gitlab::Git.branch_ref?(change[:ref])
+
+        # Deleted branch
+        next if Gitlab::Git.blank_ref?(change[:newrev])
+
+        # Default branch
+        branch_name = Gitlab::Git.branch_name(change[:ref])
+        next if branch_name == project.default_branch
+
+        branch_name
+      end.compact
+    end
+
+    def url_for_new_merge_request(branch_name)
+      merge_request_params = { source_branch: branch_name }
+      url = Gitlab::Routing.url_helpers.new_namespace_project_merge_request_url(project.namespace, project, merge_request: merge_request_params)
+      { branch_name: branch_name, url: url, new_merge_request: true }
+    end
+
+    def url_for_existing_merge_request(merge_request)
+      target_project = merge_request.target_project
+      url = Gitlab::Routing.url_helpers.namespace_project_merge_request_url(target_project.namespace, target_project, merge_request)
+      { branch_name: merge_request.source_branch, url: url, new_merge_request: false }
+    end
+  end
+end
diff --git a/app/services/merge_requests/merge_request_diff_cache_service.rb b/app/services/merge_requests/merge_request_diff_cache_service.rb
new file mode 100644
index 0000000000000000000000000000000000000000..2945a7fd4e4042f811cc1c1783bbf2b0d5503700
--- /dev/null
+++ b/app/services/merge_requests/merge_request_diff_cache_service.rb
@@ -0,0 +1,8 @@
+module MergeRequests
+  class MergeRequestDiffCacheService
+    def execute(merge_request)
+      # Executing the iteration we cache all the highlighted diff information
+      merge_request.diffs.diff_files.to_a
+    end
+  end
+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..adc71b0c2bcfb47ff7e5cda058af6197a06e2ace
--- /dev/null
+++ b/app/services/merge_requests/resolve_service.rb
@@ -0,0 +1,31 @@
+module MergeRequests
+  class ResolveService < MergeRequests::BaseService
+    attr_accessor :conflicts, :rugged, :merge_index
+
+    def execute(merge_request)
+      @conflicts = merge_request.conflicts
+      @rugged = project.repository.rugged
+      @merge_index = conflicts.merge_index
+
+      conflicts.files.each do |file|
+        write_resolved_file_to_index(file, params[:sections])
+      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, resolutions)
+      new_file = file.resolve_lines(resolutions).map(&:text).join("\n")
+      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
+  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..30c5f24988c8d379c25e03b8a953ebd33f78ffb4 100644
--- a/app/services/merge_requests/update_service.rb
+++ b/app/services/merge_requests/update_service.rb
@@ -16,7 +16,7 @@ module MergeRequests
       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 +55,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
diff --git a/app/services/notes/create_service.rb b/app/services/notes/create_service.rb
index 18971bd0be3eec0572bbf2b23b8a9a5570f52012..a36008c3ef565033e8d50ccc82aef05d0df6a54f 100644
--- a/app/services/notes/create_service.rb
+++ b/app/services/notes/create_service.rb
@@ -11,10 +11,33 @@ module Notes
         return noteable.create_award_emoji(note.award_emoji_name, current_user)
       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/post_process_service.rb b/app/services/notes/post_process_service.rb
index 534c48aefffed7499805cfa50ac082c2cc01a6cb..e4cd3fc7833867763cff187fac320dbc37c55f8d 100644
--- a/app/services/notes/post_process_service.rb
+++ b/app/services/notes/post_process_service.rb
@@ -16,7 +16,7 @@ module Notes
     end
 
     def hook_data
-      Gitlab::NoteDataBuilder.build(@note, @note.author)
+      Gitlab::DataBuilder::Note.build(@note, @note.author)
     end
 
     def execute_note_hooks
diff --git a/app/services/notes/slash_commands_service.rb b/app/services/notes/slash_commands_service.rb
new file mode 100644
index 0000000000000000000000000000000000000000..4a9a8a646531450202565fe71598fe0ab3d3c50c
--- /dev/null
+++ b/app/services/notes/slash_commands_service.rb
@@ -0,0 +1,33 @@
+module Notes
+  class SlashCommandsService < BaseService
+    UPDATE_SERVICES = {
+      'Issue' => Issues::UpdateService,
+      'MergeRequest' => MergeRequests::UpdateService
+    }
+
+    def supported?(note)
+      noteable_update_service(note) &&
+        can?(current_user, :"update_#{note.noteable_type.underscore}", note.noteable)
+    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)
+
+      noteable_update_service(note).new(project, current_user, command_params).execute(note.noteable)
+    end
+
+    private
+
+    def noteable_update_service(note)
+      UPDATE_SERVICES[note.noteable_type]
+    end
+  end
+end
diff --git a/app/services/notification_service.rb b/app/services/notification_service.rb
index ab6e51209eeae9828bcd47d43bc26b017aa451b8..6139ed56e25559c4f499e0fa1b162bbf97ed3e32 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
@@ -120,6 +148,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 +213,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 +242,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 +268,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
@@ -471,6 +505,15 @@ class NotificationService
     end
   end
 
+  def new_mentions_in_resource_email(target, project, new_mentioned_users, current_user, method)
+    recipients = build_recipients(target, project, current_user, action: "new")
+    recipients = 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)
     action = method == :merged_merge_request_email ? "merge" : "close"
     recipients = build_recipients(target, project, current_user, action: action)
diff --git a/app/services/projects/autocomplete_service.rb b/app/services/projects/autocomplete_service.rb
index 23b6668e0d1c9de43a4fec73ddd5c8486ab56458..f578f8dbea23b3d3de2c67aed2179c69cb047d64 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])
     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/destroy_service.rb b/app/services/projects/destroy_service.rb
index 882606e38d0e9c907bfc8e56a1ed0ff65b7575c6..8a53f65aec1a82bf4770a1a9d0df637e694113ec 100644
--- a/app/services/projects/destroy_service.rb
+++ b/app/services/projects/destroy_service.rb
@@ -6,8 +6,12 @@ module Projects
 
     DELETED_FLAG = '+deleted'
 
-    def pending_delete!
-      project.schedule_delete!(current_user.id, params)
+    def async_execute
+      project.transaction do
+        project.update_attribute(:pending_delete, true)
+        job_id = ProjectDestroyWorker.perform_async(project.id, current_user.id, params)
+        Rails.logger.info("User #{current_user.id} scheduled destruction of project #{project.path_with_namespace} with job ID #{job_id}")
+      end
     end
 
     def execute
diff --git a/app/services/projects/enable_deploy_key_service.rb b/app/services/projects/enable_deploy_key_service.rb
new file mode 100644
index 0000000000000000000000000000000000000000..3cf4264ce9bf3b747d9acf80f735fa83fcf84235
--- /dev/null
+++ b/app/services/projects/enable_deploy_key_service.rb
@@ -0,0 +1,17 @@
+module Projects
+  class EnableDeployKeyService < BaseService
+    def execute
+      key = accessible_keys.find_by(id: params[:key_id] || params[:id])
+      return unless key
+
+      project.deploy_keys << key
+      key
+    end
+
+    private
+
+    def accessible_keys
+      current_user.accessible_deploy_keys
+    end
+  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/protected_branches/create_service.rb b/app/services/protected_branches/create_service.rb
index 6150a2a83c93fa0d8aef395c7ec062bc4f3934b7..a84e335340d2f3194639a5c7ea5e866dd645f7a1 100644
--- a/app/services/protected_branches/create_service.rb
+++ b/app/services/protected_branches/create_service.rb
@@ -5,23 +5,7 @@ module ProtectedBranches
     def execute
       raise Gitlab::Access::AccessDeniedError unless can?(current_user, :admin_project, project)
 
-      protected_branch = project.protected_branches.new(params)
-
-      ProtectedBranch.transaction do
-        protected_branch.save!
-
-        if protected_branch.push_access_level.blank?
-          protected_branch.create_push_access_level!(access_level: Gitlab::Access::MASTER)
-        end
-
-        if protected_branch.merge_access_level.blank?
-          protected_branch.create_merge_access_level!(access_level: Gitlab::Access::MASTER)
-        end
-      end
-
-      protected_branch
-    rescue ActiveRecord::RecordInvalid
-      protected_branch
+      project.protected_branches.create(params)
     end
   end
 end
diff --git a/app/services/repository_archive_clean_up_service.rb b/app/services/repository_archive_clean_up_service.rb
index 0b56b09738d7ce2ba6853340958801653d815789..aa84d36a206f9752a7dd42691665e8bf0870122f 100644
--- a/app/services/repository_archive_clean_up_service.rb
+++ b/app/services/repository_archive_clean_up_service.rb
@@ -1,6 +1,8 @@
 class RepositoryArchiveCleanUpService
   LAST_MODIFIED_TIME_IN_MINUTES = 120
 
+  attr_reader :mmin, :path
+
   def initialize(mmin = LAST_MODIFIED_TIME_IN_MINUTES)
     @mmin = mmin
     @path = Gitlab.config.gitlab.repository_downloads_path
@@ -17,8 +19,6 @@ class RepositoryArchiveCleanUpService
 
   private
 
-  attr_reader :mmin, :path
-
   def clean_up_old_archives
     run(%W(find #{path} -not -path #{path} -type f \( -name \*.tar -o -name \*.bz2 -o -name \*.tar.gz -o -name \*.zip \) -maxdepth 2 -mmin +#{mmin} -delete))
   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..9ac1124abc15f6ff4e32f8d1695014f98de0d85f
--- /dev/null
+++ b/app/services/slash_commands/interpret_service.rb
@@ -0,0 +1,236 @@
+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
+      current_user.can?(:"admin_#{issuable.to_ability_name}", project) &&
+        project.labels.any?
+    end
+    command :label do |labels_param|
+      label_ids = find_label_ids(labels_param)
+
+      @updates[:add_label_ids] = label_ids unless label_ids.empty?
+    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)
+
+        @updates[:remove_label_ids] = label_ids unless label_ids.empty?
+      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)
+
+      @updates[:label_ids] = label_ids unless label_ids.empty?
+    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?(:"update_#{issuable.to_ability_name}", issuable)
+    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?(:"update_#{issuable.to_ability_name}", issuable)
+    end
+    command :remove_due_date do
+      @updates[:due_date] = nil
+    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 = @project.labels.where(name: labels_param.split).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/spam_check_service.rb b/app/services/spam_check_service.rb
deleted file mode 100644
index 7c3e692bde977951a1640e535a32be8d1a67c492..0000000000000000000000000000000000000000
--- a/app/services/spam_check_service.rb
+++ /dev/null
@@ -1,38 +0,0 @@
-class SpamCheckService < BaseService
-  include Gitlab::AkismetHelper
-
-  attr_accessor :request, :api
-
-  def execute(request, api)
-    @request, @api = request, api
-    return false unless request || check_for_spam?(project)
-    return false unless is_spam?(request.env, current_user, text)
-    
-    create_spam_log
-
-    true
-  end
-
-  private
-
-  def text
-    [params[:title], params[:description]].reject(&:blank?).join("\n")
-  end
-  
-  def spam_log_attrs
-    {
-      user_id: current_user.id,
-      project_id: project.id,
-      title: params[:title],
-      description: params[:description],
-      source_ip: client_ip(request.env),
-      user_agent: user_agent(request.env),
-      noteable_type: 'Issue',
-      via_api: api
-    }
-  end
-
-  def create_spam_log
-    CreateSpamLogService.new(project, current_user, spam_log_attrs).execute
-  end
-end
diff --git a/app/services/spam_service.rb b/app/services/spam_service.rb
new file mode 100644
index 0000000000000000000000000000000000000000..48903291799b11039b899f6c0cbd2d2c93aa7de9
--- /dev/null
+++ b/app/services/spam_service.rb
@@ -0,0 +1,78 @@
+class SpamService
+  attr_accessor :spammable, :request, :options
+
+  def initialize(spammable, request = nil)
+    @spammable = spammable
+    @request = request
+    @options = {}
+
+    if @request
+      @options[:ip_address] = @request.env['action_dispatch.remote_ip'].to_s
+      @options[:user_agent] = @request.env['HTTP_USER_AGENT']
+      @options[:referrer] = @request.env['HTTP_REFERRER']
+    else
+      @options[:ip_address] = @spammable.ip_address
+      @options[:user_agent] = @spammable.user_agent
+    end
+  end
+
+  def check(api = false)
+    return false unless request && check_for_spam?
+
+    return false unless akismet.is_spam?
+
+    create_spam_log(api)
+    true
+  end
+
+  def mark_as_spam!
+    return false unless spammable.submittable_as_spam?
+
+    if akismet.submit_spam
+      spammable.user_agent_detail.update_attribute(:submitted, true)
+    else
+      false
+    end
+  end
+
+  private
+
+  def akismet
+    @akismet ||= AkismetService.new(
+      spammable_owner,
+      spammable.spammable_text,
+      options
+    )
+  end
+
+  def spammable_owner
+    @user ||= User.find(spammable_owner_id)
+  end
+
+  def spammable_owner_id
+    @owner_id ||=
+      if spammable.respond_to?(:author_id)
+        spammable.author_id
+      elsif spammable.respond_to?(:creator_id)
+        spammable.creator_id
+      end
+  end
+
+  def check_for_spam?
+    spammable.check_for_spam?
+  end
+
+  def create_spam_log(api)
+    SpamLog.create(
+      {
+        user_id: spammable_owner_id,
+        title: spammable.spam_title,
+        description: spammable.spam_description,
+        source_ip: options[:ip_address],
+        user_agent: options[:user_agent],
+        noteable_type: spammable.class.to_s,
+        via_api: api
+      }
+    )
+  end
+end
diff --git a/app/services/system_note_service.rb b/app/services/system_note_service.rb
index e13dc9265b83359d122f99aa06357284ff7bbca4..546a8f11330e14c23bc0bc429c150816a447c214 100644
--- a/app/services/system_note_service.rb
+++ b/app/services/system_note_service.rb
@@ -158,6 +158,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`
diff --git a/app/services/test_hook_service.rb b/app/services/test_hook_service.rb
index e85e58751e781ced2603aa0ea0310531b861d76b..280c81f7d2dc1b2f8d535629c5198c2b98715afe 100644
--- a/app/services/test_hook_service.rb
+++ b/app/services/test_hook_service.rb
@@ -1,6 +1,6 @@
 class TestHookService
   def execute(hook, current_user)
-    data = Gitlab::PushDataBuilder.build_sample(hook.project, current_user)
+    data = Gitlab::DataBuilder::Push.build_sample(hook.project, current_user)
     hook.execute(data, 'push_hooks')
   end
 end
diff --git a/app/services/todo_service.rb b/app/services/todo_service.rb
index 6b48d68cccb20c104017ee83fd66a5763fef64cd..e0ccb65459004da1543048b0f2a3f9d0464f9942 100644
--- a/app/services/todo_service.rb
+++ b/app/services/todo_service.rb
@@ -142,10 +142,15 @@ 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)
 
-    todos.update_all(state: :done)
+    marked_todos = todos.update_all(state: :done)
     current_user.update_todos_count_cache
+    marked_todos
   end
 
   # When user marks an issue as todo
@@ -154,6 +159,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)
diff --git a/app/services/user_agent_detail_service.rb b/app/services/user_agent_detail_service.rb
new file mode 100644
index 0000000000000000000000000000000000000000..a1ee3df5fe195acde45459d45c9cdec3347ba1f6
--- /dev/null
+++ b/app/services/user_agent_detail_service.rb
@@ -0,0 +1,13 @@
+class UserAgentDetailService
+  attr_accessor :spammable, :request
+
+  def initialize(spammable, request)
+    @spammable, @request = spammable, request
+  end
+
+  def create
+    return unless request
+
+    spammable.create_user_agent_detail(user_agent: request.env['HTTP_USER_AGENT'], ip_address: request.env['action_dispatch.remote_ip'].to_s)
+  end
+end
diff --git a/app/views/admin/abuse_reports/_abuse_report.html.haml b/app/views/admin/abuse_reports/_abuse_report.html.haml
index dd2e7ebd0309acde521152646fa11fba078bff1b..56bf6194914b6e06f6e8bd9d5e8ef41c0fac1ea2 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(abuse_report.message.squish!, pipeline: :single_line, author: reporter)
   %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/application_settings/_form.html.haml b/app/views/admin/application_settings/_form.html.haml
index 23b52d08df7ccb55f0debdfd383436bd72d4a39b..d929364fc965348a34a612519272a347cdacc30d 100644
--- a/app/views/admin/application_settings/_form.html.haml
+++ b/app/views/admin/application_settings/_form.html.haml
@@ -228,6 +228,9 @@
       = f.label :max_artifacts_size, 'Maximum artifacts size (MB)', class: 'control-label col-sm-2'
       .col-sm-10
         = f.number_field :max_artifacts_size, class: 'form-control'
+        .help-block
+          Set the maximum file size each build's artifacts can have
+          = link_to "(?)", help_page_path("user/admin_area/settings/continuous_integration", anchor: "maximum-artifacts-size")
 
   - if Gitlab.config.registry.enabled
     %fieldset
@@ -363,7 +366,9 @@
       .col-sm-10
         = f.select :repository_storage, repository_storage_options_for_select, {}, class: 'form-control'
         .help-block
-          You can manage the repository storage paths in your gitlab.yml configuration file
+          Manage repository storage paths. Learn more in the
+          = succeed "." do
+            = link_to "repository storages documentation", help_page_path("administration/repository_storages")
 
   %fieldset
     %legend Repository Checks
@@ -383,6 +388,25 @@
         .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")
+
 
   .form-actions
-    = f.submit 'Save', class: 'btn btn-save'
\ No newline at end of file
+    = f.submit 'Save', class: 'btn btn-save'
diff --git a/app/views/admin/builds/_build.html.haml b/app/views/admin/builds/_build.html.haml
index 352adbedee4e0a88401c8aed59bac7751537f215..f29d9c94441a96ce91f27cb17830e52f3fc2ef9f 100644
--- a/app/views/admin/builds/_build.html.haml
+++ b/app/views/admin/builds/_build.html.haml
@@ -51,7 +51,7 @@
     - 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
diff --git a/app/views/admin/dashboard/index.html.haml b/app/views/admin/dashboard/index.html.haml
index 452fc25ab07552fddce682739c1fb20fbb001d76..e6687f43816d81d36d3ef78b9425de668b4a17e4 100644
--- a/app/views/admin/dashboard/index.html.haml
+++ b/app/views/admin/dashboard/index.html.haml
@@ -112,7 +112,7 @@
           %h4 Projects
           .data
             = link_to admin_namespaces_projects_path do
-              %h1= number_with_delimiter(Project.count)
+              %h1= number_with_delimiter(Project.cached_count)
             %hr
             = link_to('New Project', new_project_path, class: "btn btn-new")
       .col-sm-4
diff --git a/app/views/admin/labels/_form.html.haml b/app/views/admin/labels/_form.html.haml
index 448aa95354818b0ac87f39ac546984585a2ff99e..602cfa9b6fc8e7529acf9d9d8f4415956d8891f1 100644
--- a/app/views/admin/labels/_form.html.haml
+++ b/app/views/admin/labels/_form.html.haml
@@ -28,6 +28,3 @@
   .form-actions
     = f.submit 'Save', class: 'btn btn-save js-save-button'
     = link_to "Cancel", admin_labels_path, class: 'btn btn-cancel'
-
-:javascript
-  new Labels();
diff --git a/app/views/admin/spam_logs/_spam_log.html.haml b/app/views/admin/spam_logs/_spam_log.html.haml
index 8aea67f4497aff45e871a840bc3e9e39e4768514..4ce4eab87531308fb7c20f44b41f4b4e88a20ac0 100644
--- a/app/views/admin/spam_logs/_spam_log.html.haml
+++ b/app/views/admin/spam_logs/_spam_log.html.haml
@@ -24,6 +24,11 @@
       = link_to 'Remove user', admin_spam_log_path(spam_log, remove_user: true),
         data: { confirm: "USER #{user.name} WILL BE REMOVED! Are you sure?" }, method: :delete, class: "btn btn-xs btn-remove"
   %td
+    - if spam_log.submitted_as_ham?
+      .btn.btn-xs.disabled
+        Submitted as ham
+    - else
+      = link_to 'Submit as ham', mark_as_ham_admin_spam_log_path(spam_log), method: :post, class: 'btn btn-xs btn-warning'
     - 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"
     - else
diff --git a/app/views/admin/users/show.html.haml b/app/views/admin/users/show.html.haml
index d37489bebea16cef5a112dc27cd098920787290e..76c9ed0ee8bb7d838567aa18083862715a5b0f6f 100644
--- a/app/views/admin/users/show.html.haml
+++ b/app/views/admin/users/show.html.haml
@@ -140,12 +140,10 @@
           .panel-heading
             This user is blocked
           .panel-body
-            %p Blocking user has the following effects:
+            %p A blocked user cannot:
             %ul
-              %li User will not be able to login
-              %li User will not be able to access git repositories
-              %li Personal projects will be left
-              %li Owned groups will be left
+              %li Log in
+              %li Access Git repositories
             %br
             = link_to 'Unblock user', unblock_admin_user_path(@user), method: :put, class: "btn btn-info", data: { confirm: 'Are you sure?' }
       - else
diff --git a/app/views/dashboard/todos/_todo.html.haml b/app/views/dashboard/todos/_todo.html.haml
index 98f302d2f937dc769547f9cf9848d0091f41eddb..b40395c74ded7e8812163e2aa450c0251f6382c9 100644
--- a/app/views/dashboard/todos/_todo.html.haml
+++ b/app/views/dashboard/todos/_todo.html.haml
@@ -1,6 +1,7 @@
 %li{class: "todo todo-#{todo.done? ? 'done' : 'pending'}", id: dom_id(todo), data:{url: todo_target_path(todo)} }
+  = author_avatar(todo, size: 40)
+
   .todo-item.todo-block
-    = image_tag avatar_icon(todo.author_email, 40), class: 'avatar s40', alt:''
     .todo-title.title
       - unless todo.build_failed?
         = todo_target_state_pill(todo)
@@ -19,13 +20,13 @@
 
       &middot; #{time_ago_with_tooltip(todo.created_at)}
 
-    - if todo.pending?
-      .todo-actions.pull-right
-        = link_to [:dashboard, todo], method: :delete, class: 'btn btn-loading done-todo' do
-          Done
-          = icon('spinner spin')
-
     .todo-body
       .todo-note
         .md
           = event_note(todo.body, project: todo.project)
+
+  - if todo.pending?
+    .todo-actions
+      = link_to [:dashboard, todo], method: :delete, class: 'btn btn-loading done-todo' do
+        Done
+        = icon('spinner spin')
diff --git a/app/views/dashboard/todos/index.html.haml b/app/views/dashboard/todos/index.html.haml
index 4e340b6ec16f3dad72ee06eb91ab93c8a717af5b..d320d3bcc1e31ffec36a9f09b11733ed93de2d84 100644
--- a/app/views/dashboard/todos/index.html.haml
+++ b/app/views/dashboard/todos/index.html.haml
@@ -43,6 +43,25 @@
           class: 'select2 trigger-submit', include_blank: true,
           data: {placeholder: 'Action'})
 
+      .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
+            %b.caret
+          %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
+
+
 .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} }
diff --git a/app/views/devise/sessions/_new_crowd.html.haml b/app/views/devise/sessions/_new_crowd.html.haml
index 8e81671b7e789485fc43b642df2723ec9e47ec3f..b7d3acac2b152e5c90c5e0e256fb380e317e8b21 100644
--- a/app/views/devise/sessions/_new_crowd.html.haml
+++ b/app/views/devise/sessions/_new_crowd.html.haml
@@ -1,4 +1,4 @@
-= form_tag(user_omniauth_authorize_path("crowd"), id: 'new_crowd_user' ) do
+= 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"}
   - if devise_mapping.rememberable?
diff --git a/app/views/devise/shared/_omniauth_box.html.haml b/app/views/devise/shared/_omniauth_box.html.haml
index de18bc2d844123a11a52d967c5d1065b85754c26..2e7da2747d0a421f86a995dc1844a7d467842bf0 100644
--- a/app/views/devise/shared/_omniauth_box.html.haml
+++ b/app/views/devise/shared/_omniauth_box.html.haml
@@ -5,4 +5,4 @@
   - providers.each do |provider|
     %span.light
       - has_icon = provider_has_icon?(provider)
-      = link_to provider_image_tag(provider), user_omniauth_authorize_path(provider), method: :post, class: (has_icon ? 'oauth-image-link' : 'btn'), "data-no-turbolink" => "true"
+      = 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/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..69bd416c4de8d753f92baa86d806422f5e8b0556
--- /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..fbe470bed2c7af30394531ff77d96a4440ef8050 100644
--- a/app/views/discussions/_notes.html.haml
+++ b/app/views/discussions/_notes.html.haml
@@ -1,5 +1,15 @@
-.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
+        = 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..7a8767ddba09ec30d707cb7bccdbc1524635d765
--- /dev/null
+++ b/app/views/discussions/_resolve_all.html.haml
@@ -0,0 +1,11 @@
+- if discussion.for_merge_request?
+  %resolve-discussion-btn{ ":namespace-path" => "'#{discussion.project.namespace.path}'",
+      ":project-path" => "'#{discussion.project.path}'",
+      ":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" }
+        = icon("spinner spin", "v-show" => "loading")
+        {{ buttonText }}
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..2fb3190ab11e304162f349f77feb95c75caaafc7 100644
--- a/app/views/groups/group_members/_new_group_member.html.haml
+++ b/app/views/groups/group_members/_new_group_member.html.haml
@@ -14,5 +14,14 @@
         Read more about role permissions
         %strong= link_to "here", help_page_path("user/permissions"), class: "vlink"
 
+  .form-group
+    = f.label :expires_at, 'Access expiration date', class: 'control-label'
+    .col-sm-10
+      .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, the user(s) will automatically lose access to this group and all of its projects.
+
   .form-actions
     = f.submit 'Add users to group', class: "btn btn-create"
diff --git a/app/views/groups/group_members/update.js.haml b/app/views/groups/group_members/update.js.haml
index da71de4cd1e52ec2e6e940404770e62d4238905d..742f9d7a433edd00e623c4d5c3d317405244ca1b 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))}');
+  new MemberExpirationDate();
diff --git a/app/views/import/github/status.html.haml b/app/views/import/github/status.html.haml
index deaaf9af8751862c8cb991e7dd1e9a74a0d2e7ef..54ff1d27c67bd1c58057df9ed768aacdb55abe39 100644
--- a/app/views/import/github/status.html.haml
+++ b/app/views/import/github/status.html.haml
@@ -4,10 +4,6 @@
   %i.fa.fa-github
   Import projects from GitHub
 
-%p
-  %i.fa.fa-warning
-  To import GitHub pull requests, any pull request source branches that had been deleted are temporarily restored on GitHub. To prevent any connected CI services from being overloaded with dozens of irrelevant branches being created and deleted again, GitHub webhooks are temporarily disabled during the import process, but only if you have admin access to the GitHub repository.
-
 %p.light
   Select projects you want to import.
 %hr
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/_init_auto_complete.html.haml b/app/views/layouts/_init_auto_complete.html.haml
index 351100f3523383a3a723f261f6ab5fe96b6fcab0..67ff4b272b9886a30016c469d71cb7ca760b7255 100644
--- a/app/views/layouts/_init_auto_complete.html.haml
+++ b/app/views/layouts/_init_auto_complete.html.haml
@@ -1,7 +1,7 @@
 - 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.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..bf50633af244025eaf404f88f3537bbd46370655 100644
--- a/app/views/layouts/_page.html.haml
+++ b/app/views/layouts/_page.html.haml
@@ -23,7 +23,6 @@
     = 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/application.html.haml b/app/views/layouts/application.html.haml
index 33cedaaf2eee64e17ae15780b35309b275cb9f83..15a94ac23c56675e974b7d2b732a32a7a77554f6 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
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..67f558c854b1e1ac65a99765bf721ebe95490aad 100644
--- a/app/views/layouts/nav/_dashboard.html.haml
+++ b/app/views/layouts/nav/_dashboard.html.haml
@@ -12,6 +12,11 @@
     = 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
diff --git a/app/views/layouts/nav/_project.html.haml b/app/views/layouts/nav/_project.html.haml
index 9e65d94186b99924eee01a1a224803ebcddaf923..f7012595a5a72729b298ac3e9773560c9f3dce3d 100644
--- a/app/views/layouts/nav/_project.html.haml
+++ b/app/views/layouts/nav/_project.html.haml
@@ -65,8 +65,8 @@
             Graphs
 
     - if project_nav_tab? :issues
-      = nav_link(controller: [:issues, :labels, :milestones]) do
-        = link_to url_for_project_issues(@project, only_path: true), title: 'Issues', class: 'shortcuts-issues' 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
             - if @project.default_issues_tracker?
diff --git a/app/views/layouts/project.html.haml b/app/views/layouts/project.html.haml
index ee9c0366f2bdea8131a4f63883492dea0aa60298..9fe94291db749dfa5c8c337213dfa04339678daa 100644
--- a/app/views/layouts/project.html.haml
+++ b/app/views/layouts/project.html.haml
@@ -6,13 +6,13 @@
 - content_for :scripts_body_top do
   - project = @target_project || @project
   - if @project_wiki && @page
-    - markdown_preview_path = namespace_project_wiki_markdown_preview_path(project.namespace, project, @page.slug)
+    - preview_markdown_path = namespace_project_wiki_preview_markdown_path(project.namespace, project, @page.slug)
   - else
-    - markdown_preview_path = markdown_preview_namespace_project_path(project.namespace, project)
+    - preview_markdown_path = preview_markdown_namespace_project_path(project.namespace, project)
   - if current_user
     :javascript
       window.project_uploads_path = "#{namespace_project_uploads_path project.namespace,project}";
-      window.markdown_preview_path = "#{markdown_preview_path}";
+      window.preview_markdown_path = "#{preview_markdown_path}";
 
 - content_for :scripts_body do
   = render "layouts/init_auto_complete" if current_user
diff --git a/app/views/notify/new_issue_email.text.erb b/app/views/notify/new_issue_email.text.erb
index fc64c98038b42a6a7125cb2802538bc92a2e19eb..ca5c2f2688c0c9d01444235fddb16564332d14de 100644
--- a/app/views/notify/new_issue_email.text.erb
+++ b/app/views/notify/new_issue_email.text.erb
@@ -3,3 +3,5 @@ New Issue was created.
 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_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/new_merge_request_email.text.erb b/app/views/notify/new_merge_request_email.text.erb
index d4aad8d1862641254549c9a615f5bf9d46c3265f..3c8f178ac778cf34fd2e0a32191111fbe97496ca 100644
--- a/app/views/notify/new_merge_request_email.text.erb
+++ b/app/views/notify/new_merge_request_email.text.erb
@@ -6,3 +6,5 @@ New Merge Request <%= @merge_request.to_reference %>
 Author:    <%= @merge_request.author_name %>
 Assignee:  <%= @merge_request.assignee_name %>
 
+<%= @merge_request.description %>
+
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 57d16d291586de7bd62dc43518a44b40a120a3ad..c80f22457b4eb8d673a7e39d46010bb86b7341f8 100644
--- a/app/views/profiles/accounts/show.html.haml
+++ b/app/views/profiles/accounts/show.html.haml
@@ -70,7 +70,7 @@
               = link_to unlink_profile_account_path(provider: provider), method: :delete, class: 'provider-btn' do
                 Disconnect
           - else
-            = link_to user_omniauth_authorize_path(provider), method: :post, class: 'provider-btn not-active', "data-no-turbolink" => "true" do
+            = link_to omniauth_authorize_path(:user, provider), method: :post, class: 'provider-btn not-active', "data-no-turbolink" => "true" do
               Connect
   %hr
 - if current_user.can_change_username?
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/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/projects/_home_panel.html.haml b/app/views/projects/_home_panel.html.haml
index 51f74f3b7ce6dd8e3f177aac29af77f460b69dcc..8ef31ca3bda74bba0c4ec63d8bdb3fafdc0125dd 100644
--- a/app/views/projects/_home_panel.html.haml
+++ b/app/views/projects/_home_panel.html.haml
@@ -24,6 +24,3 @@
 
       .project-clone-holder
         = render "shared/clone_panel"
-
-:javascript
-  new Star();
diff --git a/app/views/projects/_zen.html.haml b/app/views/projects/_zen.html.haml
index 413477a2d3a0082b846aeec6f4b8d330d1f6f1d5..3978fa60d663f2396c2950c962b86985dc8ffc53 100644
--- a/app/views/projects/_zen.html.haml
+++ b/app/views/projects/_zen.html.haml
@@ -1,7 +1,8 @@
+- 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/badges/badge.svg.erb b/app/views/projects/badges/badge.svg.erb
new file mode 100644
index 0000000000000000000000000000000000000000..a5fef4fc56faf2a2ff341969936dcd214c04853b
--- /dev/null
+++ b/app/views/projects/badges/badge.svg.erb
@@ -0,0 +1,36 @@
+<svg xmlns="http://www.w3.org/2000/svg" width="<%= badge.width %>" height="20">
+  <linearGradient id="b" x2="0" y2="100%">
+    <stop offset="0" stop-color="#bbb" stop-opacity=".1"/>
+    <stop offset="1" stop-opacity=".1"/>
+  </linearGradient>
+
+  <mask id="a">
+    <rect width="<%= badge.width %>" height="20" rx="3" fill="#fff"/>
+  </mask>
+
+  <g mask="url(#a)">
+    <path fill="<%= badge.key_color %>"
+          d="M0 0 h<%= badge.key_width %> v20 H0 z"/>
+    <path fill="<%= badge.value_color %>"
+          d="M<%= badge.key_width %> 0 h<%= badge.value_width %> v20 H<%= badge.key_width %> z"/>
+    <path fill="url(#b)"
+          d="M0 0 h<%= badge.width %> v20 H0 z"/>
+  </g>
+
+  <g fill="#fff" text-anchor="middle">
+    <g font-family="DejaVu Sans,Verdana,Geneva,sans-serif" font-size="11">
+      <text x="<%= badge.key_text_anchor %>" y="15" fill="#010101" fill-opacity=".3">
+        <%= badge.key_text %>
+      </text>
+      <text x="<%= badge.key_text_anchor %>" y="14">
+        <%= badge.key_text %>
+      </text>
+      <text x="<%= badge.value_text_anchor %>" y="15" fill="#010101" fill-opacity=".3">
+        <%= badge.value_text %>
+      </text>
+      <text x="<%= badge.value_text_anchor %>" y="14">
+        <%= badge.value_text %>
+      </text>
+    </g>
+  </g>
+</svg>
diff --git a/app/views/projects/blob/_editor.html.haml b/app/views/projects/blob/_editor.html.haml
index ff379bafb26759800baee0a81780423ada586aa7..0237e152b54b577f27035b4d40b444fd4ff521c3 100644
--- a/app/views/projects/blob/_editor.html.haml
+++ b/app/views/projects/blob/_editor.html.haml
@@ -24,7 +24,7 @@
       .encoding-selector
         = select_tag :encoding, options_for_select([ "base64", "text" ], "text"), class: 'select2'
 
-  .file-content.code
+  .file-editor.code
     %pre.js-edit-mode-pane#editor #{params[:content] || local_assigns[:blob_data]}
     - if local_assigns[:path]
       .js-edit-mode-pane#preview.hide
diff --git a/app/views/projects/blob/_image.html.haml b/app/views/projects/blob/_image.html.haml
index 18caddabd3921cbb85c2f83ee168d970a62fe534..4c356d1f07f26be1093662ce622b45f6505bdb19 100644
--- a/app/views/projects/blob/_image.html.haml
+++ b/app/views/projects/blob/_image.html.haml
@@ -1,9 +1,15 @@
 .file-content.image_file
   - if blob.svg?
-    - # We need to scrub SVG but we cannot do so in the RawController: it would
-    - # be wrong/strange if RawController modified the data.
-    - blob.load_all_data!(@repository)
-    - blob = sanitize_svg(blob)
-    %img{src: "data:#{blob.mime_type};base64,#{Base64.encode64(blob.data)}"}
+    - if blob.size_within_svg_limits?
+      - # We need to scrub SVG but we cannot do so in the RawController: it would
+      - # be wrong/strange if RawController modified the data.
+      - blob.load_all_data!(@repository)
+      - blob = sanitize_svg(blob)
+      %img{src: "data:#{blob.mime_type};base64,#{Base64.encode64(blob.data)}"}
+    - else
+      .nothing-here-block
+        The SVG could not be displayed as it is too large, you can
+        #{link_to('view the raw file', namespace_project_raw_path(@project.namespace, @project, @id), target: '_blank')}
+        instead.
   - else
     %img{src: namespace_project_raw_path(@project.namespace, @project, tree_join(@commit.id, blob.path))}
diff --git a/app/views/projects/blob/diff.html.haml b/app/views/projects/blob/diff.html.haml
index 5926d181ba3c13129d327df5459f9bb14e59b8c1..a79ae53c7803e1a39e65a0657298bf49d5b38dfb 100644
--- a/app/views/projects/blob/diff.html.haml
+++ b/app/views/projects/blob/diff.html.haml
@@ -1,20 +1,30 @@
 - if @lines.present?
+  - line_class = diff_view == :inline ? '' : diff_view
   - if @form.unfold? && @form.since != 1 && !@form.bottom?
-    %tr.line_holder
-      = render "projects/diffs/match_line", { line: @match_line,
-        line_old: @form.since, line_new: @form.since, bottom: false, new_file: false }
+    %tr.line_holder{ class: line_class }
+      = diff_match_line @form.since, @form.since, text: @match_line, view: diff_view
 
   - @lines.each_with_index do |line, index|
     - line_new = index + @form.since
     - line_old = line_new - @form.offset
-    %tr.line_holder{ id: line_old }
-      %td.old_line.diff-line-num{ data: { linenumber: line_old } }
-        = link_to raw(line_old), "##{line_old}"
-      %td.new_line.diff-line-num{ data: { linenumber: line_old } }
-        = link_to raw(line_new) , "##{line_old}"
-      %td.line_content.noteable_line==#{' ' * @form.indent}#{line}
+    - line_content = capture do
+      %td.line_content.noteable_line{ class: line_class }==#{' ' * @form.indent}#{line}
+    %tr.line_holder{ id: line_old, class: line_class }
+      - case diff_view
+      - when :inline
+        %td.old_line.diff-line-num{ data: { linenumber: line_old } }
+          %a{href: "##{line_old}", data: { linenumber: line_old }}
+        %td.new_line.diff-line-num{ data: { linenumber: line_new } }
+          %a{href: "##{line_new}", data: { linenumber: line_new }}
+        = line_content
+      - when :parallel
+        %td.old_line.diff-line-num{data: { linenumber: line_old }}
+          = link_to raw(line_old), "##{line_old}"
+        = line_content
+        %td.new_line.diff-line-num{data: { linenumber: line_new }}
+          = link_to raw(line_new), "##{line_new}"
+        = line_content
 
   - if @form.unfold? && @form.bottom? && @form.to < @blob.loc
-    %tr.line_holder{ id: @form.to }
-      = render "projects/diffs/match_line", { line: @match_line,
-        line_old: @form.to, line_new: @form.to, bottom: true, new_file: false }
+    %tr.line_holder{ id: @form.to, class: line_class }
+      = diff_match_line @form.to, @form.to, text: @match_line, view: diff_view, bottom: true
diff --git a/app/views/projects/blob/edit.html.haml b/app/views/projects/blob/edit.html.haml
index b1c9895f43e879297f0a6a450f6109d57ab2ec73..680e95ac6b5bb7015f4bfe92986cdd9e2a3b7700 100644
--- a/app/views/projects/blob/edit.html.haml
+++ b/app/views/projects/blob/edit.html.haml
@@ -1,4 +1,13 @@
 - 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')
+
+- 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
@@ -10,15 +19,10 @@
       = 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
+  = 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', @last_commit
+    = 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'))
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..de53a298f8480f1b98aa074d85ea3e0f9b9487f2
--- /dev/null
+++ b/app/views/projects/boards/components/_board.html.haml
@@ -0,0 +1,43 @@
+%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) }" }
+          {{ list.title }}
+          %span.pull-right{ "v-if" => "list.type !== 'blank'" }
+            {{ list.issues.length }}
+          - 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")
+          = icon("spinner spin", class: "board-header-loading-spinner pull-right", "v-show" => "list.loadingMore")
+      .board-inner-container.board-search-container{ "v-if" => "list.canSearch()" }
+        %input.form-control{ type: "text", placeholder: "Search issues", "v-model" => "query", "debounce" => "250" }
+        = icon("search", class: "board-search-icon", "v-show" => "!query")
+        %button.board-search-clear-btn{ type: "button", role: "button", "aria-label" => "Clear search", "@click" => "query = ''", "v-show" => "query" }
+          = icon("times", class: "board-search-clear")
+      %board-list{ "inline-template" => true,
+        "v-if" => "list.type !== 'blank'",
+        ":list" => "list",
+        ":issues" => "list.issues",
+        ":loading" => "list.loading",
+        ":disabled" => "disabled",
+        ":issue-link-base" => "issueLinkBase" }
+        .board-list-loading.text-center{ "v-if" => "loading" }
+          = icon("spinner spin")
+        %ul.board-list{ "v-el:list" => true,
+          "v-show" => "!loading",
+          ":data-board" => "list.id" }
+          = render "projects/boards/components/card"
+      - 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..e8b60b54d8028bb3e7078618caad2d5d709346be
--- /dev/null
+++ b/app/views/projects/boards/components/_card.html.haml
@@ -0,0 +1,33 @@
+%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 }",
+    ":index" => "index" }
+    %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
+        = precede '#' do
+          {{ issue.id }}
+      %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 }}
+      %a.has-tooltip{ ":href" => "'/u/' + 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 }
diff --git a/app/views/projects/boards/show.html.haml b/app/views/projects/boards/show.html.haml
new file mode 100644
index 0000000000000000000000000000000000000000..edbbd3f3d2a01c90f1ed151aacebd1c1a9f0af4f
--- /dev/null
+++ b/app/views/projects/boards/show.html.haml
@@ -0,0 +1,19 @@
+- @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
+
+.boards-list#board-app{ "v-cloak" => true,
+  "data-endpoint" => "#{namespace_project_board_path(@project.namespace, @project)}",
+  "data-disabled" => "#{!can?(current_user, :admin_list, @project)}",
+  "data-issue-link-base" => "#{namespace_project_issues_path(@project.namespace, @project)}" }
+  .boards-app-loading.text-center{ "v-if" => "loading" }
+    = icon("spinner spin")
+  = render "projects/boards/components/board"
diff --git a/app/views/projects/branches/_branch.html.haml b/app/views/projects/branches/_branch.html.haml
index 4bd85061240a86b2859c0f7b65880c79fe79a7f9..6192ccb710b3d99b3ba4c9a53a745ae1b3c5a422 100644
--- a/app/views/projects/branches/_branch.html.haml
+++ b/app/views/projects/branches/_branch.html.haml
@@ -5,8 +5,8 @@
 - number_commits_ahead = diverging_commit_counts[:ahead]
 %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
diff --git a/app/views/projects/builds/_sidebar.html.haml b/app/views/projects/builds/_sidebar.html.haml
index a8bc53c284995b93a4466d95707e49e122ef84b2..5b0b58e087be7dc0c895a516f446ba07f6750fad 100644
--- a/app/views/projects/builds/_sidebar.html.haml
+++ b/app/views/projects/builds/_sidebar.html.haml
@@ -11,98 +11,133 @@
       %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
+  - builds = @build.pipeline.builds.latest.to_a
+  - statuses = ["failed", "pending", "running", "canceled", "success", "skipped"]
+  - if builds.size > 1
+    .dropdown.build-dropdown
+      .build-light-text Stage
+      %button.dropdown-menu-toggle{type: 'button', 'data-toggle' => 'dropdown'}
+        %span.stage-selection More
+        = icon('caret-down')
+      %ul.dropdown-menu
+        - builds.map(&:stage).uniq.each do |stage|
+          %li
+            %a.stage-item= stage
 
-      - 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
+    .builds-container
+      - statuses.each do |build_status|
+        - builds.select{|build| build.status == build_status}.each do |build|
+          .build-job{class: ('active' if build == @build), data: {stage: build.stage}}
+            = link_to namespace_project_build_path(@project.namespace, @project, build) do
+              = icon('check')
+              = ci_icon_for_status(build.status)
+              %span
+                - if build.name
+                  = build.name
+                - else
+                  = build.id
 
-          = link_to download_namespace_project_build_artifacts_path(@project.namespace, @project, @build), class: 'btn btn-sm btn-default' do
-            Download
+        - if @build.retried?
+          %li.active
+            %a
+              Build ##{@build.id}
+              &middot;
+              %i.fa.fa-warning
+              This build was retried.
 
-          - if @build.artifacts_metadata?
-            = link_to browse_namespace_project_build_artifacts_path(@project.namespace, @project, @build), class: 'btn btn-sm btn-default' do
-              Browse
+  .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
 
-  .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
-      %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
+        - 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
 
-  - if @build.trigger_request
-    .build-widget
-      %h4.title
-        Trigger
+            - if @build.artifacts_metadata?
+              = link_to browse_namespace_project_build_artifacts_path(@project.namespace, @project, @build), class: 'btn btn-sm btn-default' do
+                Browse
 
-      %p
-        %span.build-light-text Token:
-        #{@build.trigger_request.trigger.short_token}
+    .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
+        %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
+
+    - if @build.trigger_request
+      .build-widget
+        %h4.title
+          Trigger
 
-      - 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
+            %span.build-light-text 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|
+            %code
+              #{key}=#{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
diff --git a/app/views/projects/builds/show.html.haml b/app/views/projects/builds/show.html.haml
index 4421f3b9562e3cb40e0cc87252f1d8c5e3d390a0..e4d41288aa6f34aef495e01eef019acc8e9c0cc2 100644
--- a/app/views/projects/builds/show.html.haml
+++ b/app/views/projects/builds/show.html.haml
@@ -5,26 +5,6 @@
 .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
@@ -67,4 +47,10 @@
 = 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]}")
+  new Build({
+    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: "#{trace_with_state[:state]}"
+  })
diff --git a/app/views/projects/buttons/_dropdown.html.haml b/app/views/projects/buttons/_dropdown.html.haml
index 16b8e1cca9134e9fab9867a7f579b31153f0a0e6..ca907077c2b33fce8d190addbc173b1561ed00b9 100644
--- a/app/views/projects/buttons/_dropdown.html.haml
+++ b/app/views/projects/buttons/_dropdown.html.haml
@@ -9,7 +9,7 @@
 
       - if can_create_issue
         %li
-          = link_to url_for_new_issue(@project, only_path: true) do
+          = link_to new_namespace_project_issue_path(@project.namespace, @project) do
             = icon('exclamation-circle fw')
             New issue
 
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..311583037e5626505e8dbadcf89d1f5a35873fd2 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
     - 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..1fdf32466f234ac7c99e032935fa185d0be9b59a 100644
--- a/app/views/projects/ci/builds/_build.html.haml
+++ b/app/views/projects/ci/builds/_build.html.haml
@@ -63,7 +63,7 @@
     - 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")
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..04cbd0c3591b04a800f8333c8a3860845057ecd8
--- /dev/null
+++ b/app/views/projects/ci/builds/_build_pipeline.html.haml
@@ -0,0 +1,14 @@
+- is_playable = subject.playable? && can?(current_user, :update_build, @project)
+%li.build{class: ("playable" if is_playable)}
+  .build-content
+    - if is_playable
+      = link_to play_namespace_project_build_path(subject.project.namespace, subject.project, subject, return_to: request.original_url), method: :post, title: 'Play' do
+        = render_status_with_link('build', 'play')
+        = subject.name
+    - elsif can?(current_user, :read_build, @project)
+      = link_to namespace_project_build_path(subject.project.namespace, subject.project, subject) do
+        = render_status_with_link('build', subject.status)
+        = subject.name
+    - else
+      = render_status_with_link('build', subject.status)
+      = 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 558c35553da5c938269c2a231be12b8eb11191d9..b119f6edf14909ca5711c58e230453fc91a2fadb 100644
--- a/app/views/projects/ci/pipelines/_pipeline.html.haml
+++ b/app/views/projects/ci/pipelines/_pipeline.html.haml
@@ -1,21 +1,23 @@
 - status = pipeline.status
 %tr.commit
   %td.commit-link
-    = link_to namespace_project_pipeline_path(@project.namespace, @project, pipeline.id) do
-      = ci_status_with_icon(status)
-
-
+    = link_to namespace_project_pipeline_path(pipeline.project.namespace, pipeline.project, pipeline.id) do
+      - if defined?(status_icon_only) && status_icon_only
+        = ci_icon_for_status(status)
+      - else
+        = ci_status_with_icon(status)
   %td
     .branch-commit
-      = 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
         %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"
+        - unless defined?(hide_branch) && hide_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"
+      .icon-container
+        = custom_icon("icon_commit")
+      = link_to pipeline.short_sha, namespace_project_commit_path(pipeline.project.namespace, pipeline.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?
@@ -28,32 +30,32 @@
       %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"
+          = link_to_gfm truncate(commit.title, length: 60), 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.latest.stages_status
+    - stages_status = pipeline.statuses.relevant.latest.stages_status
     - 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
+          = 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)}
+        #{time_ago_with_tooltip(pipeline.finished_at, short_format: false, skip_js: true)}
 
   %td.pipeline-actions
     .controls.hidden-xs.pull-right
@@ -69,7 +71,7 @@
               %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
+                    = link_to play_namespace_project_build_path(pipeline.project.namespace, pipeline.project, build), method: :post, rel: 'nofollow' do
                       = icon("play")
                       %span= build.name.humanize
           - if artifacts.present?
@@ -80,15 +82,15 @@
               %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/_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 ea33aa472a65dfd78c8d959613f11dc6e09bfb29..935433306ea020d1bc58a645e7fc289596ff6fd6 100644
--- a/app/views/projects/commit/_ci_menu.html.haml
+++ b/app/views/projects/commit/_ci_menu.html.haml
@@ -2,7 +2,7 @@
   = nav_link(path: 'commit#show') do
     = link_to namespace_project_commit_path(@project.namespace, @project, @commit.id) do
       Changes
-      %span.badge= @diffs.count
+      %span.badge= @diffs.size
   = 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/_commit_box.html.haml b/app/views/projects/commit/_commit_box.html.haml
index 3ad866bb2f1a6fa1a913580df00fb0d3e804300a..29d767e776967b2b01d960b1edb512bea203f64e 100644
--- a/app/views/projects/commit/_commit_box.html.haml
+++ b/app/views/projects/commit/_commit_box.html.haml
@@ -56,10 +56,10 @@
     = 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
+      %span.ci-status-label
+        = ci_label_for_status(@commit.status)
+    in
+    = time_interval_in_words @commit.pipelines.total_duration
 
 .commit-box.content-block
   %h3.commit-title
diff --git a/app/views/projects/commit/_pipeline.html.haml b/app/views/projects/commit/_pipeline.html.haml
index 540689f4a6147717d3c50ec801c8e21eb92cfbbb..20a85148ab5cdf06b20bf641867a6f3fc5c68fef 100644
--- a/app/views/projects/commit/_pipeline.html.haml
+++ b/app/views/projects/commit/_pipeline.html.haml
@@ -1,5 +1,9 @@
-.row-content-block.build-content.middle-block
+.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
@@ -23,6 +27,22 @@
         in
         = time_interval_in_words pipeline.duration
 
+.row-content-block.build-content.middle-block.pipeline-graph
+  .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
+              - statuses.each do |status|
+                = render "projects/#{status.to_partial_path}_pipeline", subject: status
+
+
 - if pipeline.yaml_errors.present?
   .bs-callout.bs-callout-danger
     %h4 Found errors in your .gitlab-ci.yml:
@@ -46,5 +66,5 @@
         - if pipeline.project.build_coverage_enabled?
           %th Coverage
         %th
-    - pipeline.statuses.stages.each do |stage|
-      = render 'projects/commit/ci_stage', stage: stage, statuses: pipeline.statuses.where(stage: stage)
+    - pipeline.statuses.relevant.stages.each do |stage|
+      = render 'projects/commit/ci_stage', stage: stage, statuses: pipeline.statuses.relevant.where(stage: stage)
diff --git a/app/views/projects/commit/_pipelines_list.haml b/app/views/projects/commit/_pipelines_list.haml
new file mode 100644
index 0000000000000000000000000000000000000000..29f4ef8f49e10edf3be8f17959eeafd62a300590
--- /dev/null
+++ b/app/views/projects/commit/_pipelines_list.haml
@@ -0,0 +1,17 @@
+%ul.content-list.pipelines
+  - if pipelines.blank?
+    %li
+      .nothing-here-block No pipelines to show
+  - else
+    .table-holder
+      %table.table.builds
+        %tbody
+          %th Status
+          %th Commit
+          - pipelines.stages.each do |stage|
+            %th.stage
+              %span.has-tooltip{ title: "#{stage.titleize}" }
+                = stage.titleize
+          %th
+          %th
+        = render pipelines, commit_sha: true, stage: true, allow_retry: true, stages: pipelines.stages, status_icon_only: true, hide_branch: true
diff --git a/app/views/projects/commit/show.html.haml b/app/views/projects/commit/show.html.haml
index d0da26065879af4445fd6476df7628607cf4cbba..ed44d86a687d0a1e36456d8b507650e12711c12b 100644
--- a/app/views/projects/commit/show.html.haml
+++ b/app/views/projects/commit/show.html.haml
@@ -7,7 +7,7 @@
   = render "ci_menu"
 - else
   %div.block-connector
-= render "projects/diffs/diffs", diffs: @diffs, project: @project, diff_refs: @commit.diff_refs
+= render "projects/diffs/diffs", diffs: @diffs
 = render "projects/notes/notes_with_form"
 - if can_collaborate_with_project?
   - %w(revert cherry-pick).each do |type|
diff --git a/app/views/projects/compare/_form.html.haml b/app/views/projects/compare/_form.html.haml
index af09b3418ea888f1043f17f8c6a127c11f078cf4..d79336f5a6035e4cef05bd618d5765d4dc84c2fa 100644
--- a/app/views/projects/compare/_form.html.haml
+++ b/app/views/projects/compare/_form.html.haml
@@ -1,7 +1,7 @@
 = 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 'switch', {from: params[:to], to: params[:from]}, {class: 'commits-compare-switch has-tooltip', title: 'Switch base of comparison'}
+      = 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
       .input-group.inline-input-group
         %span.input-group-addon from
diff --git a/app/views/projects/compare/show.html.haml b/app/views/projects/compare/show.html.haml
index 28a50e7031a14ca45ebd6b329c0d57627b4af008..819e9bc15ae591b2ae222f0760b4d71e62e0b8ff 100644
--- a/app/views/projects/compare/show.html.haml
+++ b/app/views/projects/compare/show.html.haml
@@ -8,7 +8,7 @@
 
   - if @commits.present?
     = render "projects/commits/commit_list"
-    = render "projects/diffs/diffs", diffs: @diffs, project: @project, diff_refs: @diff_refs
+    = render "projects/diffs/diffs", diffs: @diffs
   - else
     .light-well
       .center
diff --git a/app/views/projects/deployments/_actions.haml b/app/views/projects/deployments/_actions.haml
index f70dba224faabe12d806c82879f5bf405ddc017f..f7bf3b834ef469da187f4297d8c0a0ae2b54ca14 100644
--- a/app/views/projects/deployments/_actions.haml
+++ b/app/views/projects/deployments/_actions.haml
@@ -2,9 +2,9 @@
   .pull-right
     - actions = deployment.manual_actions
     - if actions.present?
-      .btn-group.inline
-        .btn-group
-          %a.dropdown-toggle.btn.btn-default{type: 'button', 'data-toggle' => 'dropdown'}
+      .inline
+        .dropdown
+          %a.dropdown-new.btn.btn-default{type: 'button', 'data-toggle' => 'dropdown'}
             = icon("play")
             %b.caret
           %ul.dropdown-menu.dropdown-menu-align-right
diff --git a/app/views/projects/deployments/_commit.html.haml b/app/views/projects/deployments/_commit.html.haml
index 0f9d9512d887b122d111ebfcee2ebdffbeadfa23..28813babd7be6b54c0245e5cea545cd2fff47073 100644
--- a/app/views/projects/deployments/_commit.html.haml
+++ b/app/views/projects/deployments/_commit.html.haml
@@ -1,12 +1,16 @@
 %div.branch-commit
   - if deployment.ref
-    = link_to deployment.ref, namespace_project_commits_path(@project.namespace, @project, deployment.ref), class: "monospace"
-    &middot;
+    .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
+    = custom_icon("icon_commit")
   = link_to deployment.short_sha, namespace_project_commit_path(@project.namespace, @project, deployment.sha), class: "commit-id monospace"
 
   %p.commit-title
     %span
       - if commit_title = deployment.commit_title
+        = author_avatar(deployment.commit, size: 20)
         = link_to_gfm commit_title, namespace_project_commit_path(@project.namespace, @project, deployment.sha), class: "commit-row-message"
       - else
         Cant find HEAD commit for this branch
diff --git a/app/views/projects/deployments/_deployment.html.haml b/app/views/projects/deployments/_deployment.html.haml
index baf02f1e6a013ca74f5af737d0f86195b6c80f5b..cd95841ca5a9fb23c4d97bec62518d4f9aea9b63 100644
--- a/app/views/projects/deployments/_deployment.html.haml
+++ b/app/views/projects/deployments/_deployment.html.haml
@@ -8,6 +8,7 @@
   %td
     - if deployment.deployable
       = link_to [@project.namespace.becomes(Namespace), @project, deployment.deployable] do
+        = user_avatar(user: deployment.user, size: 20)
         = "#{deployment.deployable.name} (##{deployment.deployable.id})"
 
   %td
diff --git a/app/views/projects/diffs/_content.html.haml b/app/views/projects/diffs/_content.html.haml
index a1b071f130c73fb1c0640f59d89a793862c71330..d37961c4e40d56b09512d7e8f33dfb51def7dfa9 100644
--- a/app/views/projects/diffs/_content.html.haml
+++ b/app/views/projects/diffs/_content.html.haml
@@ -13,7 +13,7 @@
       .nothing-here-block.diff-collapsed{data: { diff_for_path: url } }
         This diff is collapsed. Click to expand it.
     - elsif diff_file.diff_lines.length > 0
-      - if diff_view == 'parallel'
+      - if diff_view == :parallel
         = render "projects/diffs/parallel_view", diff_file: diff_file, project: project, blob: blob
       - else
         = render "projects/diffs/text_file", diff_file: diff_file
diff --git a/app/views/projects/diffs/_diffs.html.haml b/app/views/projects/diffs/_diffs.html.haml
index 4bf3ccace2022ee14e29cef67526cbe966519394..62aff36aadd7f417de3c8d345ec5acb043a30064 100644
--- a/app/views/projects/diffs/_diffs.html.haml
+++ b/app/views/projects/diffs/_diffs.html.haml
@@ -1,20 +1,19 @@
 - show_whitespace_toggle = local_assigns.fetch(:show_whitespace_toggle, true)
-- if diff_view == 'parallel'
+- diff_files = diffs.diff_files
+- if diff_view == :parallel
   - fluid_layout true
 
-- diff_files = safe_diff_files(diffs, diff_refs: diff_refs, repository: project.repository)
-
 .content-block.oneline-block.files-changed
   .inline-parallel-buttons
     - if !expand_all_diffs? && diff_files.any? { |diff_file| diff_file.collapsed? }
       = link_to 'Expand all', url_for(params.merge(expand_all_diffs: 1, format: nil)), class: 'btn btn-default'
     - if show_whitespace_toggle
       - if current_controller?(:commit)
-        = commit_diff_whitespace_link(@project, @commit, class: 'hidden-xs')
+        = commit_diff_whitespace_link(diffs.project, @commit, class: 'hidden-xs')
       - elsif current_controller?(:merge_requests)
-        = diff_merge_request_whitespace_link(@project, @merge_request, class: 'hidden-xs')
+        = diff_merge_request_whitespace_link(diffs.project, @merge_request, class: 'hidden-xs')
       - elsif current_controller?(:compare)
-        = diff_compare_whitespace_link(@project, params[:from], params[:to], class: 'hidden-xs')
+        = diff_compare_whitespace_link(diffs.project, params[:from], params[:to], class: 'hidden-xs')
     .btn-group
       = inline_diff_btn
       = parallel_diff_btn
@@ -23,12 +22,12 @@
 - 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, @project))}}
+.files{data: {can_create_note: (!@diff_notes_disabled && can?(current_user, :create_note, diffs.project))}}
   - diff_files.each_with_index do |diff_file, index|
     - diff_commit = commit_for_diff(diff_file)
     - blob = diff_file.blob(diff_commit)
     - next unless blob
-    - blob.load_all_data!(project.repository) unless blob.only_display_raw?
+    - blob.load_all_data!(diffs.project.repository) unless blob.only_display_raw?
 
-    = render 'projects/diffs/file', i: index, project: project,
-      diff_file: diff_file, diff_commit: diff_commit, blob: blob, diff_refs: diff_refs
+    = render 'projects/diffs/file', index: index, project: diffs.project,
+      diff_file: diff_file, diff_commit: diff_commit, blob: blob
diff --git a/app/views/projects/diffs/_file.html.haml b/app/views/projects/diffs/_file.html.haml
index 1854c64cbd77561ec30dedaa49ca2b8af46c2738..ad2eb3e504f50f8f651be4ab2022dd9c199acb09 100644
--- a/app/views/projects/diffs/_file.html.haml
+++ b/app/views/projects/diffs/_file.html.haml
@@ -1,6 +1,6 @@
-.diff-file.file-holder{id: "diff-#{i}", data: diff_file_html_data(project, diff_file)}
+.diff-file.file-holder{id: "diff-#{index}", data: diff_file_html_data(project, diff_file.file_path, diff_commit.id)}
   .file-title{id: "file-path-#{hexdigest(diff_file.file_path)}"}
-    = render "projects/diffs/file_header", diff_file: diff_file, blob: blob, diff_commit: diff_commit, project: project, url: "#diff-#{i}"
+    = render "projects/diffs/file_header", diff_file: diff_file, blob: blob, diff_commit: diff_commit, project: project, url: "#diff-#{index}"
 
     - unless diff_file.submodule?
       .file-actions.hidden-xs
@@ -10,11 +10,10 @@
           \
 
           - 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, project)
+        = view_file_btn(diff_commit.id, diff_file.new_path, project)
 
-  = render 'projects/diffs/content', diff_file: diff_file, diff_commit: diff_commit, diff_refs: diff_refs, blob: blob, project: project
+  = render 'projects/diffs/content', diff_file: diff_file, diff_commit: diff_commit, blob: blob, project: project
diff --git a/app/views/projects/diffs/_line.html.haml b/app/views/projects/diffs/_line.html.haml
index 4d3af905b5890bf54b7a8be86e56535345455a9b..7042e9f1fc97310885b8e8c0cdc7b9b1ea5cd034 100644
--- a/app/views/projects/diffs/_line.html.haml
+++ b/app/views/projects/diffs/_line.html.haml
@@ -1,11 +1,11 @@
+- 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'
-    = render "projects/diffs/match_line", { line: line.text,
-      line_old: line.old_pos, line_new: line.new_pos, bottom: false, new_file: diff_file.new_file }
+    = diff_match_line line.old_pos, line.new_pos, text: line.text
   - when 'nonewline'
     %td.old_line.diff-line-num
     %td.new_line.diff-line-num
@@ -23,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, type)
+      - else
+        = diff_line_content(line.text, type)
+
+- 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/_match_line.html.haml b/app/views/projects/diffs/_match_line.html.haml
deleted file mode 100644
index d6dddd97879ce85f9f53667eaa91e7055ff1a0f4..0000000000000000000000000000000000000000
--- a/app/views/projects/diffs/_match_line.html.haml
+++ /dev/null
@@ -1,7 +0,0 @@
-%td.old_line.diff-line-num{data: {linenumber: line_old},
- class: [unfold_bottom_class(bottom), unfold_class(!new_file)]}
-  \...
-%td.new_line.diff-line-num{data: {linenumber: line_new},
- class: [unfold_bottom_class(bottom), unfold_class(!new_file)]}
-  \...
-%td.line_content.match= line
diff --git a/app/views/projects/diffs/_parallel_view.html.haml b/app/views/projects/diffs/_parallel_view.html.haml
index 7f30faa20d88e057011c2634acf073f6e4a7f14e..28aad3f472554467ef8696d8dd31aaa6cb27d054 100644
--- a/app/views/projects/diffs/_parallel_view.html.haml
+++ b/app/views/projects/diffs/_parallel_view.html.haml
@@ -1,14 +1,15 @@
 / Side-by-side diff view
 %div.text-file.diff-wrap-lines.code.file-content.js-syntax-highlight{ data: diff_view_data }
   %table
+    - last_line = 0
     - diff_file.parallel_diff_lines.each do |line|
       - left = line[:left]
       - right = line[:right]
+      - last_line = right.new_pos if right
       %tr.line_holder.parallel
         - if left
           - if left.meta?
-            %td.old_line.diff-line-num.empty-cell
-            %td.line_content.parallel.match= left.text
+            = diff_match_line left.old_pos, nil, text: left.text, view: :parallel
           - else
             - left_line_code = diff_file.line_code(left)
             - left_position = diff_file.position(left)
@@ -21,8 +22,7 @@
 
         - if right
           - if right.meta?
-            %td.old_line.diff-line-num.empty-cell
-            %td.line_content.parallel.match= left.text
+            = diff_match_line nil, right.new_pos, text: left.text, view: :parallel
           - else
             - right_line_code = diff_file.line_code(right)
             - right_position = diff_file.position(right)
@@ -37,3 +37,5 @@
         - discussion_left, discussion_right = parallel_diff_discussions(left, right, diff_file)
         - if discussion_left || discussion_right
           = render "discussions/parallel_diff_discussion", discussion_left: discussion_left, discussion_right: discussion_right
+    - if !diff_file.new_file && last_line > 0
+      = diff_match_line last_line, last_line, bottom: true, view: :parallel
diff --git a/app/views/projects/diffs/_text_file.html.haml b/app/views/projects/diffs/_text_file.html.haml
index 5970b9abf2b2e16b73f12cfdf0bbfa890f2daea3..f1d2d4bf2689d3cb3245ca9d4a68d14e2addeaf3 100644
--- a/app/views/projects/diffs/_text_file.html.haml
+++ b/app/views/projects/diffs/_text_file.html.haml
@@ -5,16 +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
+  - 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 }
 
-    - 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
-
-  - if last_line > 0
-    = render "projects/diffs/match_line", { line: "",
-      line_old: last_line, line_new: last_line, bottom: true, new_file: diff_file.new_file }
+  - 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/environments/_environment.html.haml b/app/views/projects/environments/_environment.html.haml
index e2453395602c1f6fe82a61a73c2268e491c3942f..36a6162a5a862e33b77518f17908b20c90253190 100644
--- a/app/views/projects/environments/_environment.html.haml
+++ b/app/views/projects/environments/_environment.html.haml
@@ -2,8 +2,12 @@
 
 %tr.environment
   %td
-    %strong
-      = link_to environment.name, namespace_project_environment_path(@project.namespace, @project, environment)
+    = link_to environment.name, namespace_project_environment_path(@project.namespace, @project, environment)
+
+  %td
+    - if last_deployment
+      = user_avatar(user: last_deployment.user, size: 20)
+      %strong ##{last_deployment.id}
 
   %td
     - if last_deployment
diff --git a/app/views/projects/environments/index.html.haml b/app/views/projects/environments/index.html.haml
index a6dd34653abd84e4b0db61e0ae5aa38388ae7364..b3eb5b0011a9617efc1b7252764c89bd429e955f 100644
--- a/app/views/projects/environments/index.html.haml
+++ b/app/views/projects/environments/index.html.haml
@@ -23,10 +23,11 @@
           New environment
   - else
     .table-holder
-      %table.table.environments
+      %table.table.builds.environments
         %tbody
           %th Environment
-          %th Last deployment
-          %th Date
+          %th Last Deployment
+          %th Commit
+          %th
           %th
         = render @environments
diff --git a/app/views/projects/environments/show.html.haml b/app/views/projects/environments/show.html.haml
index a07436ad7c97dd5f3b246644f5c3680a0cd8d852..8f8c1c4ce22c09b11b9ad4fde9b7ec5a91d28042 100644
--- a/app/views/projects/environments/show.html.haml
+++ b/app/views/projects/environments/show.html.haml
@@ -23,13 +23,13 @@
       = link_to "Read more", help_page_path("ci/environments"), class: "btn btn-success"
   - else
     .table-holder
-      %table.table.environments
+      %table.table.builds.environments
         %thead
           %tr
             %th ID
             %th Commit
             %th Build
-            %th Date
+            %th
             %th
 
         = render @deployments
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..584c0fa18ae4eb8468980d7a7a19d3ac302cb4d2
--- /dev/null
+++ b/app/views/projects/generic_commit_statuses/_generic_commit_status_pipeline.html.haml
@@ -0,0 +1,9 @@
+%li.build
+  .build-content
+    - if subject.target_url
+      - link_to subject.target_url do
+        = render_status_with_link('commit status', subject.status)
+        = subject.name
+    - else
+      = render_status_with_link('commit status', subject.status)
+      = subject.name
diff --git a/app/views/projects/graphs/show.html.haml b/app/views/projects/graphs/show.html.haml
index a985b442b2d6e6c303c701d1cd702f4c2c948916..ac5f792d1402dbc1b3a90fe66019d84e55ea2417 100644
--- a/app/views/projects/graphs/show.html.haml
+++ b/app/views/projects/graphs/show.html.haml
@@ -32,7 +32,7 @@
 :javascript
   $.ajax({
     type: "GET",
-    url: location.href,
+    url: "#{namespace_project_graph_path(@project.namespace, @project, current_ref, format: :json)}",
     dataType: "json",
     success: function (data) {
       var graph = new ContributorsStatGraph();
diff --git a/app/views/projects/group_links/index.html.haml b/app/views/projects/group_links/index.html.haml
index 2b904544f28fab5c5eb35323768e9529ae79d11a..ca700cb3a3b90b807c37dc2db2191a54c7425bea 100644
--- a/app/views/projects/group_links/index.html.haml
+++ b/app/views/projects/group_links/index.html.haml
@@ -17,6 +17,13 @@
         .select-wrapper
           = select_tag :link_group_access, options_for_select(ProjectGroupLink.access_options, ProjectGroupLink.default_access), class: "form-control select-control"
           %span.caret
+      .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/issues/_head.html.haml b/app/views/projects/issues/_head.html.haml
index 403adb7426bacc3a40da31a80924baf72dfb0c38..b6cb559afcbaad942cac010634729b137162efab 100644
--- a/app/views/projects/issues/_head.html.haml
+++ b/app/views/projects/issues/_head.html.haml
@@ -2,10 +2,15 @@
   %ul{ class: (container_class) }
     - if project_nav_tab?(:issues) && !current_controller?(:merge_requests)
       = nav_link(controller: :issues) do
-        = link_to url_for_project_issues(@project, only_path: true), title: 'Issues' do
+        = link_to namespace_project_issues_path(@project.namespace, @project), title: 'Issues' do
           %span
             Issues
 
+      = nav_link(controller: :boards) do
+        = link_to namespace_project_board_path(@project.namespace, @project), title: 'Board' do
+          %span
+            Board
+
     - 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
diff --git a/app/views/projects/issues/show.html.haml b/app/views/projects/issues/show.html.haml
index e5cce16a171677e1f91ede838020c67c5e3d0b0b..3fb4191c60e04ae7967f3a80ab59c964410a71bc 100644
--- a/app/views/projects/issues/show.html.haml
+++ b/app/views/projects/issues/show.html.haml
@@ -22,7 +22,7 @@
   - 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" } }
+        %button.btn.btn-default.pull-left.hidden-md.hidden-lg{ type: "button", data: { toggle: "dropdown" } }
           %span.caret
           Options
         .dropdown-menu.dropdown-menu-align-right.hidden-lg
@@ -37,14 +37,19 @@
                 = link_to 'Close issue', issue_path(@issue, issue: { state_event: :close }, status_only: true, format: 'json'), data: {no_turbolink: true}, class: "btn-close #{issue_button_visibility(@issue, true)}", title: 'Close issue'
               %li
                 = link_to 'Edit', edit_namespace_project_issue_path(@project.namespace, @project, @issue)
+            - if @issue.submittable_as_spam? && current_user.admin?
+              %li
+                = link_to 'Submit as spam', mark_as_spam_namespace_project_issue_path(@project.namespace, @project, @issue), method: :post, class: 'btn-spam', title: 'Submit as spam'
+
         - if can?(current_user, :create_issue, @project)
           = link_to new_namespace_project_issue_path(@project.namespace, @project), class: 'hidden-xs hidden-sm btn btn-grouped new-issue-link btn-new btn-inverted', title: 'New issue', id: 'new_issue_link' do
             New issue
         - if can?(current_user, :update_issue, @issue)
           = link_to 'Reopen issue', issue_path(@issue, issue: { state_event: :reopen }, status_only: true, format: 'json'), data: {no_turbolink: true}, class: "hidden-xs hidden-sm btn btn-grouped btn-reopen #{issue_button_visibility(@issue, false)}", title: 'Reopen issue'
           = link_to 'Close issue', issue_path(@issue, issue: { state_event: :close }, status_only: true, format: 'json'), data: {no_turbolink: true}, class: "hidden-xs hidden-sm btn btn-grouped btn-close #{issue_button_visibility(@issue, true)}", title: 'Close issue'
-          = link_to edit_namespace_project_issue_path(@project.namespace, @project, @issue), class: 'hidden-xs hidden-sm btn btn-grouped issuable-edit' do
-            Edit
+          - if @issue.submittable_as_spam? && current_user.admin?
+            = link_to 'Submit as spam', mark_as_spam_namespace_project_issue_path(@project.namespace, @project, @issue), method: :post, class: 'hidden-xs hidden-sm btn btn-grouped btn-spam', title: 'Submit as spam'
+          = link_to 'Edit', edit_namespace_project_issue_path(@project.namespace, @project, @issue), class: 'hidden-xs hidden-sm btn btn-grouped issuable-edit'
 
 
 .issue-details.issuable-details
diff --git a/app/views/projects/merge_requests/_discussion.html.haml b/app/views/projects/merge_requests/_discussion.html.haml
index 53dd300c35c081945b6b4e2097bd04ba1fdfae08..d070979bcfe6bd06d59d866f7511c1f6fbbbbc2e 100644
--- a/app/views/projects/merge_requests/_discussion.html.haml
+++ b/app/views/projects/merge_requests/_discussion.html.haml
@@ -4,5 +4,8 @@
       = 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?
       = 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: { namespace_path: "#{@merge_request.project.namespace.path}", project_path: "#{@merge_request.project.path}" } }
+      {{ buttonText }}
 
 #notes= render "projects/notes/notes_with_form"
diff --git a/app/views/projects/merge_requests/_new_submit.html.haml b/app/views/projects/merge_requests/_new_submit.html.haml
index a5e67b95727f3f9714b39b09fcb8bb044ed6101b..00bd4e143dff1ad3d0a1a6a944867815c5e433a1 100644
--- a/app/views/projects/merge_requests/_new_submit.html.haml
+++ b/app/views/projects/merge_requests/_new_submit.html.haml
@@ -20,7 +20,7 @@
 .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
+      = link_to url_for(params), data: {target: 'div#commits', action: 'new', toggle: 'tab'} do
         Commits
         %span.badge= @commits.size
     - if @pipeline
@@ -42,7 +42,7 @@
           %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, project: @project, diff_refs: @merge_request.diff_refs, show_whitespace_toggle: false
+        = render "projects/diffs/diffs", diffs: @diffs, show_whitespace_toggle: false
     - if @pipeline
       #builds.builds.tab-pane
         = render "projects/merge_requests/show/builds"
@@ -52,11 +52,8 @@
     $('#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 ? 'diffs' : 'new')}",
+    setUrl: false
   });
diff --git a/app/views/projects/merge_requests/_show.html.haml b/app/views/projects/merge_requests/_show.html.haml
index 873ed9b59ee4c86237f29c0b430196cbf5b54be1..9d8b4cc56be7403ee4236e7ecea98da0badaec8a 100644
--- a/app/views/projects/merge_requests/_show.html.haml
+++ b/app/views/projects/merge_requests/_show.html.haml
@@ -1,8 +1,10 @@
 - page_title           "#{@merge_request.title} (#{@merge_request.to_reference})", "Merge Requests"
 - page_description     @merge_request.description
 - page_card_attributes @merge_request.card_attributes
+- content_for :page_specific_javascripts do
+  = page_specific_javascript_tag('diff_notes/diff_notes_bundle.js')
 
-- if diff_view == 'parallel'
+- if diff_view == :parallel
   - fluid_layout true
 
 .merge-request{'data-url' => merge_request_path(@merge_request)}
@@ -14,6 +16,9 @@
       - 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
 
@@ -45,24 +50,38 @@
     - 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
+          = 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
+          = 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= @merge_request.all_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
+            = 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
+          = 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
             = render 'award_emoji/awards_block', awardable: @merge_request, inline: true
@@ -76,6 +95,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/conflicts.html.haml b/app/views/projects/merge_requests/conflicts.html.haml
new file mode 100644
index 0000000000000000000000000000000000000000..a524936f73cb3179a97516ef01dfb006e7d7991e
--- /dev/null
+++ b/app/views/projects/merge_requests/conflicts.html.haml
@@ -0,0 +1,29 @@
+- class_bindings = "{ |
+    'head': line.isHead, |
+    'origin': line.isOrigin, |
+    'match': line.hasMatch, |
+    'selected': line.isSelected, |
+    'unselected': line.isUnselected }"
+
+- page_title "Merge Conflicts", "#{@merge_request.title} (#{@merge_request.to_reference}", "Merge Requests"
+= 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"}
+    = render partial: "projects/merge_requests/conflicts/parallel_view", locals: { class_bindings: class_bindings }
+    = render partial: "projects/merge_requests/conflicts/inline_view", locals: { class_bindings: class_bindings }
+    = render partial: "projects/merge_requests/conflicts/submit_form"
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..457c467fba9825e161cccbad4fd94686b241eb7c
--- /dev/null
+++ b/app/views/projects/merge_requests/conflicts/_commit_stats.html.haml
@@ -0,0 +1,20 @@
+.content-block.oneline-block.files-changed{"v-if" => "!isLoading && !hasError"}
+  .inline-parallel-buttons
+    .btn-group
+      %a.btn{ |
+        ":class" => "{'active': !isParallel}", |
+        "@click" => "handleViewTypeChange('inline')"}
+        Inline
+      %a.btn{ |
+        ":class" => "{'active': isParallel}", |
+        "@click" => "handleViewTypeChange('parallel')"}
+        Side-by-side
+
+  .js-toggle-container
+    .commit-stat-summary
+      Showing
+      %strong.cred {{conflictsCount}} {{conflictsData.conflictsText}}
+      between
+      %strong {{conflictsData.source_branch}}
+      and
+      %strong {{conflictsData.target_branch}}
diff --git a/app/views/projects/merge_requests/conflicts/_inline_view.html.haml b/app/views/projects/merge_requests/conflicts/_inline_view.html.haml
new file mode 100644
index 0000000000000000000000000000000000000000..19c7da4b5e39a6a9c1ccbfd2151b76e88ef5e286
--- /dev/null
+++ b/app/views/projects/merge_requests/conflicts/_inline_view.html.haml
@@ -0,0 +1,28 @@
+.files{"v-show" => "!isParallel"}
+  .diff-file.file-holder.conflict.inline-view{"v-for" => "file in conflictsData.files"}
+    .file-title
+      %i.fa.fa-fw{":class" => "file.iconClass"}
+      %strong {{file.filePath}}
+      .file-actions
+        %a.btn.view-file.btn-file-option{":href" => "file.blobPath"}
+          View file @{{conflictsData.shortCommitSha}}
+
+    .diff-content.diff-wrap-lines
+      .diff-wrap-lines.code.file-content.js-syntax-highlight
+        %table
+          %tr.line_holder.diff-inline{"v-for" => "line in file.inlineLines"}
+            %template{"v-if" => "!line.isHeader"}
+              %td.diff-line-num.new_line{":class" => class_bindings}
+                %a {{line.new_line}}
+              %td.diff-line-num.old_line{":class" => class_bindings}
+                %a {{line.old_line}}
+              %td.line_content{":class" => class_bindings}
+                {{{line.richText}}}
+
+            %template{"v-if" => "line.isHeader"}
+              %td.diff-line-num.header{":class" => class_bindings}
+              %td.diff-line-num.header{":class" => class_bindings}
+              %td.line_content.header{":class" => class_bindings}
+                %strong {{{line.richText}}}
+                %button.btn{"@click" => "handleSelected(line.id, line.section)"}
+                  {{line.buttonTitle}}
diff --git a/app/views/projects/merge_requests/conflicts/_parallel_view.html.haml b/app/views/projects/merge_requests/conflicts/_parallel_view.html.haml
new file mode 100644
index 0000000000000000000000000000000000000000..2e6f67c2eaf867c0d5aa94e255fd0dfb5458c1e1
--- /dev/null
+++ b/app/views/projects/merge_requests/conflicts/_parallel_view.html.haml
@@ -0,0 +1,27 @@
+.files{"v-show" => "isParallel"}
+  .diff-file.file-holder.conflict.parallel-view{"v-for" => "file in conflictsData.files"}
+    .file-title
+      %i.fa.fa-fw{":class" => "file.iconClass"}
+      %strong {{file.filePath}}
+      .file-actions
+        %a.btn.view-file.btn-file-option{":href" => "file.blobPath"}
+          View file @{{conflictsData.shortCommitSha}}
+
+    .diff-content.diff-wrap-lines
+      .diff-wrap-lines.code.file-content.js-syntax-highlight
+        %table
+          %tr.line_holder.parallel{"v-for" => "section in file.parallelLines"}
+            %template{"v-for" => "line in section"}
+
+              %template{"v-if" => "line.isHeader"}
+                %td.diff-line-num.header{":class" => class_bindings}
+                %td.line_content.header{":class" => class_bindings}
+                  %strong {{line.richText}}
+                  %button.btn{"@click" => "handleSelected(line.id, line.section)"}
+                    {{line.buttonTitle}}
+
+              %template{"v-if" => "!line.isHeader"}
+                %td.diff-line-num.old_line{":class" => class_bindings}
+                  {{line.lineNumber}}
+                %td.line_content.parallel{":class" => class_bindings}
+                  {{{line.richText}}}
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..78bd4133ea292416bf3b683b3bce89220be47b28
--- /dev/null
+++ b/app/views/projects/merge_requests/conflicts/_submit_form.html.haml
@@ -0,0 +1,15 @@
+.content-block.oneline-block.files-changed
+  %strong.resolved-count {{resolvedCount}}
+  of
+  %strong.total-count {{conflictsCount}}
+  conflicts have been resolved
+
+  .commit-message-container.form-group
+    .max-width-marker
+    %textarea.form-control.js-commit-message{"v-model" => "conflictsData.commitMessage"}
+      {{{conflictsData.commitMessage}}}
+
+  %button{type: "button", class: "btn btn-success js-submit-button", ":disabled" => "!readyToCommit", "@click" => "commit()"}
+    %span {{commitButtonText}}
+
+  = 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/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/_diffs.html.haml b/app/views/projects/merge_requests/show/_diffs.html.haml
index 1b0bae86ad4ee46fa06e2fe94cda246c4d2f519c..013b05628fae0750eb0f720e6e9c948cd1c662d9 100644
--- a/app/views/projects/merge_requests/show/_diffs.html.haml
+++ b/app/views/projects/merge_requests/show/_diffs.html.haml
@@ -1,6 +1,5 @@
 - if @merge_request_diff.collected?
-  = render "projects/diffs/diffs", diffs: @merge_request.diffs(diff_options),
-    project: @merge_request.project, diff_refs: @merge_request.diff_refs
+  = 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}
 - else
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..098ce19da21bc95ddfb5972870483cf7e7e687b8 100644
--- a/app/views/projects/merge_requests/show/_mr_title.html.haml
+++ b/app/views/projects/merge_requests/show/_mr_title.html.haml
@@ -14,7 +14,7 @@
   - 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" } }
+        %button.btn.btn-default.pull-left.hidden-md.hidden-lg{ type: "button", data: { toggle: "dropdown" } }
           %span.caret
           Options
         .dropdown-menu.dropdown-menu-align-right.hidden-lg
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/widget/_heading.html.haml b/app/views/projects/merge_requests/widget/_heading.html.haml
index 6ef640bb6543d1446ca42b3d71b6543b9260aedf..494695a03a50e3363efe1f8bf06b2ef01aab6493 100644
--- a/app/views/projects/merge_requests/widget/_heading.html.haml
+++ b/app/views/projects/merge_requests/widget/_heading.html.haml
@@ -42,3 +42,16 @@
     .ci_widget.ci-error{style: "display:none"}
       = 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)
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..6f5ee5f16c5a5e70921bd18759853873c4e0e9e5 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'
@@ -19,7 +25,7 @@
       = render 'projects/merge_requests/widget/open/not_allowed'
     - elsif !@merge_request.mergeable_ci_state? && @pipeline && @pipeline.failed?
       = render 'projects/merge_requests/widget/open/build_failed'
-    - elsif @merge_request.can_be_merged?
+    - elsif @merge_request.can_be_merged? || resolved_conflicts
       = render 'projects/merge_requests/widget/open/accept'
 
   - if mr_closes_issues.present?
diff --git a/app/views/projects/merge_requests/widget/_show.html.haml b/app/views/projects/merge_requests/widget/_show.html.haml
index d9efe81701f222fb81d25b86c972414b420a5198..ea618263a4a83dadc15ff23ce2ff5415623eaa22 100644
--- a/app/views/projects/merge_requests/widget/_show.html.haml
+++ b/app/views/projects/merge_requests/widget/_show.html.haml
@@ -23,7 +23,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') {
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/new.html.haml b/app/views/projects/new.html.haml
index facdfcc9447f71fc0970833eadd6e49a63cb9063..ea4898f2107fa678044c84cb38670bb2ead09170 100644
--- a/app/views/projects/new.html.haml
+++ b/app/views/projects/new.html.haml
@@ -46,28 +46,18 @@
                 %div
                   - if github_import_enabled?
                     = link_to new_import_github_path, class: 'btn import_github' do
-                      = icon 'github', text: 'GitHub'
+                      = icon('github', text: 'GitHub')
                 %div
                   - if bitbucket_import_enabled?
-                    - if bitbucket_import_configured?
-                      = link_to status_import_bitbucket_path, class: 'btn import_bitbucket', "data-no-turbolink" => "true" do
-                        %i.fa.fa-bitbucket
-                        Bitbucket
-                    - else
-                      = link_to status_import_bitbucket_path, class: 'how_to_import_link btn import_bitbucket', "data-no-turbolink" => "true" do
-                        %i.fa.fa-bitbucket
-                        Bitbucket
+                    = link_to status_import_bitbucket_path, class: "btn import_bitbucket #{'how_to_import_link' unless bitbucket_import_configured?}", "data-no-turbolink" => "true" do
+                      = icon('bitbucket', text: 'Bitbucket')
+                    - unless bitbucket_import_configured?
                       = render 'bitbucket_import_modal'
                 %div
                   - if gitlab_import_enabled?
-                    - if gitlab_import_configured?
-                      = link_to status_import_gitlab_path, class: 'btn import_gitlab' do
-                        %i.fa.fa-heart
-                        GitLab.com
-                    - else
-                      = link_to status_import_gitlab_path, class: 'how_to_import_link btn import_gitlab' do
-                        %i.fa.fa-heart
-                        GitLab.com
+                    = link_to status_import_gitlab_path, class: "btn import_gitlab #{'how_to_import_link' unless bitbucket_import_configured?}" do
+                      = icon('gitlab', text: 'GitLab.com')
+                    - unless gitlab_import_configured?
                       = render 'gitlab_import_modal'
                 %div
                   - if gitorious_import_enabled?
@@ -77,23 +67,19 @@
                 %div
                   - if google_code_import_enabled?
                     = link_to new_import_google_code_path, class: 'btn import_google_code' do
-                      %i.fa.fa-google
-                      Google Code
+                      = icon('google', text: 'Google Code')
                 %div
                   - if fogbugz_import_enabled?
                     = link_to new_import_fogbugz_path, class: 'btn import_fogbugz' do
-                      %i.fa.fa-bug
-                      Fogbugz
+                      = icon('bug', text: 'Fogbugz')
                 %div
                   - if git_import_enabled?
                     = link_to "#", class: 'btn js-toggle-button import_git' do
-                      %i.fa.fa-git
-                      %span Repo by URL
+                      = icon('git', text: 'Repo by URL')
                 %div{ class: 'import_gitlab_project' }
-                  - if gitlab_project_import_enabled?
+                  - if gitlab_project_import_enabled? && current_user.is_admin?
                     = link_to new_import_gitlab_project_path, class: 'btn btn_import_gitlab_project project-submit' do
-                      %i.fa.fa-gitlab
-                      %span GitLab export
+                      = icon('gitlab', text: 'GitLab export')
 
             .js-toggle-content.hide
               = render "shared/import_form", f: f
@@ -159,4 +145,4 @@
   $('.import_git').click(function( event ) {
     $projectImportUrl = $('#project_import_url')
     $projectImportUrl.attr('disabled', !$projectImportUrl.attr('disabled'))
-  });
\ No newline at end of file
+  });
diff --git a/app/views/projects/notes/_form.html.haml b/app/views/projects/notes/_form.html.haml
index 7c61ba750fe5e0e0fe14587e0f6c025fcfe84c41..402f5b52f5e5d68da8771dcdf735ff396c896ee9 100644
--- a/app/views/projects/notes/_form.html.haml
+++ b/app/views/projects/notes/_form.html.haml
@@ -1,4 +1,4 @@
-= 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|
+= 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 +10,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: true
+    = render 'projects/notes/hints', supports_slash_commands: true
     .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..d2ac1ce2b9a9c001581d0e1834f9fb69a0140f02 100644
--- a/app/views/projects/notes/_note.html.haml
+++ b/app/views/projects/notes/_note.html.haml
@@ -1,5 +1,6 @@
 - return unless note.author
 - return if note.cross_reference_not_visible_for?(current_user)
+- can_resolve = can?(current_user, :resolve_note, note)
 
 - note_editable = note_editable?(note)
 %li.timeline-entry{ id: dom_id(note), class: ["note", "note-row-#{note.id}", ('system-note' if note.system)], data: {author_id: note.author.id, editable: note_editable} }
@@ -16,19 +17,48 @@
             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?
+              %resolve-btn{ ":namespace-path" => "'#{note.project.namespace.path}'",
+                  ":project-path" => "'#{note.project.path}'",
+                  ":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')
+
+              - 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')
       .note-body{class: note_editable ? 'js-task-list-container' : ''}
         .note-text.md
           = preserve do
diff --git a/app/views/projects/pipelines/_info.html.haml b/app/views/projects/pipelines/_info.html.haml
index 8289aefcde755c31ecf5bdef79e988a19bae03fb..063e83a407aca98225f8d9a52f2a7cb463305ee4 100644
--- a/app/views/projects/pipelines/_info.html.haml
+++ b/app/views/projects/pipelines/_info.html.haml
@@ -9,7 +9,7 @@
     = 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)
 
   .pull-right
     = link_to namespace_project_pipeline_path(@project.namespace, @project, @pipeline), class: "ci-status ci-#{@pipeline.status}" do
diff --git a/app/views/projects/pipelines/new.html.haml b/app/views/projects/pipelines/new.html.haml
index 5f4ec2e40c85e9c510aeebe395a735f4960007cb..55202725b9ee74c808a4b96148fd5365b93a1d8b 100644
--- a/app/views/projects/pipelines/new.html.haml
+++ b/app/views/projects/pipelines/new.html.haml
@@ -9,7 +9,7 @@
   .form-group
     = f.label :ref, 'Create for', class: 'control-label'
     .col-sm-10
-      = f.text_field :ref, required: true, tabindex: 2, class: 'form-control'
+      = f.text_field :ref, required: true, tabindex: 2, class: 'form-control js-branch-name ui-autocomplete-input', autocomplete: :false, id: :ref
       .help-block Existing branch name, tag
   .form-actions
     = f.submit 'Create pipeline', class: 'btn btn-create', tabindex: 3
diff --git a/app/views/projects/pipelines_settings/_badge.html.haml b/app/views/projects/pipelines_settings/_badge.html.haml
new file mode 100644
index 0000000000000000000000000000000000000000..7b7fa56d993f8c235701918b0625c19b195931af
--- /dev/null
+++ b/app/views/projects/pipelines_settings/_badge.html.haml
@@ -0,0 +1,27 @@
+.row{ class: badge.title.gsub(' ', '-') }
+  .col-lg-3.profile-settings-sidebar
+    %h4.prepend-top-0
+      = badge.title.capitalize
+  .col-lg-9
+    .prepend-top-10
+      .panel.panel-default
+        .panel-heading
+          %b
+            = badge.title.capitalize
+            &middot;
+          = badge.to_html
+          .pull-right
+            = render 'shared/ref_switcher', destination: 'badges', align_right: true
+        .panel-body
+          .row
+            .col-md-2.text-center
+              Markdown
+            .col-md-10.code.js-syntax-highlight
+              = highlight('.md', badge.to_markdown)
+          .row
+            %hr
+          .row
+            .col-md-2.text-center
+              HTML
+            .col-md-10.code.js-syntax-highlight
+              = highlight('.html', badge.to_html)
diff --git a/app/views/projects/pipelines_settings/show.html.haml b/app/views/projects/pipelines_settings/show.html.haml
index 228bad36ebd97c099db4e603c2f9b3816812bf57..8c7222bfe3d3122f46264f4f022bf38e4f4e8e84 100644
--- a/app/views/projects/pipelines_settings/show.html.haml
+++ b/app/views/projects/pipelines_settings/show.html.haml
@@ -77,27 +77,4 @@
 %hr
 
 .row.prepend-top-default
-  .col-lg-3.profile-settings-sidebar
-    %h4.prepend-top-0
-      Builds Badge
-  .col-lg-9
-    .prepend-top-10
-      .panel.panel-default
-        .panel-heading
-          %b Builds badge &middot;
-          = @build_badge.to_html
-          .pull-right
-            = render 'shared/ref_switcher', destination: 'badges', align_right: true
-        .panel-body
-          .row
-            .col-md-2.text-center
-              Markdown
-            .col-md-10.code.js-syntax-highlight
-              = highlight('.md', @build_badge.to_markdown)
-          .row
-            %hr
-          .row
-            .col-md-2.text-center
-              HTML
-            .col-md-10.code.js-syntax-highlight
-              = highlight('.html', @build_badge.to_html)
+  = render partial: 'badge', collection: @badges
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..fa8cbf717337b1c63a79f5ffc3ead340aff05e6c 100644
--- a/app/views/projects/project_members/_new_project_member.html.haml
+++ b/app/views/projects/project_members/_new_project_member.html.haml
@@ -14,5 +14,14 @@
         Read more about role permissions
         %strong= link_to "here", help_page_path("user/permissions"), class: "vlink"
 
+  .form-group
+    = f.label :expires_at, 'Access expiration date', class: 'control-label'
+    .col-sm-10
+      .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, the user(s) will automatically lose access to this project.
+
   .form-actions
     = f.submit 'Add users to project', class: "btn btn-create"
diff --git a/app/views/projects/project_members/index.html.haml b/app/views/projects/project_members/index.html.haml
index 9031f01b496d51c94e62ee659843436b4fc572d3..9d063b3081f878a40c176954ac8ac18ce51cb892 100644
--- a/app/views/projects/project_members/index.html.haml
+++ b/app/views/projects/project_members/index.html.haml
@@ -1,6 +1,6 @@
 - page_title "Members"
 
-.project-members-page.prepend-top-default
+.project-members-page.js-project-members-page.prepend-top-default
   - if can?(current_user, :admin_project_member, @project)
     .panel.panel-default
       .panel-heading
diff --git a/app/views/projects/project_members/update.js.haml b/app/views/projects/project_members/update.js.haml
index 45f8ef890606e95ce6e022b3b00664d4b2921351..833954bc0391636933a77a979e25566a018b00db 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))}');
+  new MemberExpirationDate();
diff --git a/app/views/projects/protected_branches/_branches_list.html.haml b/app/views/projects/protected_branches/_branches_list.html.haml
index 0603a014008cf43e9bb0c8312c80e605575c53f9..04b19a8c5a7389c97f7ad9575275a0ce96cc80b2 100644
--- a/app/views/projects/protected_branches/_branches_list.html.haml
+++ b/app/views/projects/protected_branches/_branches_list.html.haml
@@ -1,26 +1,28 @@
-%h5.prepend-top-0
-  Already Protected (#{@protected_branches.size})
-- if @protected_branches.empty?
-  %p.settings-message.text-center
-    No branches are protected, protect a branch with the form above.
-- else
-  - can_admin_project = can?(current_user, :admin_project, @project)
+.panel.panel-default.protected-branches-list
+  - if @protected_branches.empty?
+    .panel-heading
+      %h3.panel-title
+        Protected branch (#{@protected_branches.size})
+    %p.settings-message.text-center
+      There are currently no protected branches, protect a branch with the form above.
+  - else
+    - can_admin_project = can?(current_user, :admin_project, @project)
 
-  %table.table.protected-branches-list
-    %colgroup
-      %col{ width: "20%" }
-      %col{ width: "30%" }
-      %col{ width: "25%" }
-      %col{ width: "25%" }
-    %thead
-      %tr
-        %th Branch
-        %th Last commit
-        %th Allowed to merge
-        %th Allowed to push
-        - if can_admin_project
-          %th
-    %tbody
-      = render partial: @protected_branches, locals: { can_admin_project: can_admin_project }
+    %table.table.table-bordered
+      %colgroup
+        %col{ width: "25%" }
+        %col{ width: "30%" }
+        %col{ width: "25%" }
+        %col{ width: "20%" }
+      %thead
+        %tr
+          %th Protected branch (#{@protected_branches.size})
+          %th Last commit
+          %th Allowed to merge
+          %th Allowed to push
+          - if can_admin_project
+            %th
+      %tbody
+        = render partial: @protected_branches, locals: { can_admin_project: can_admin_project }
 
-  = paginate @protected_branches, theme: 'gitlab'
+    = paginate @protected_branches, theme: 'gitlab'
diff --git a/app/views/projects/protected_branches/_create_protected_branch.html.haml b/app/views/projects/protected_branches/_create_protected_branch.html.haml
new file mode 100644
index 0000000000000000000000000000000000000000..e95a3b1b4c368810e70e0197d3620826df36e0c9
--- /dev/null
+++ b/app/views/projects/protected_branches/_create_protected_branch.html.haml
@@ -0,0 +1,41 @@
+= form_for [@project.namespace.becomes(Namespace), @project, @protected_branch] do |f|
+  .panel.panel-default
+    .panel-heading
+      %h3.panel-title
+        Protect a branch
+    .panel-body
+      .form-horizontal
+        = form_errors(@protected_branch)
+        .form-group
+          = f.label :name, class: 'col-md-2 text-right' do
+            Branch:
+          .col-md-10
+            = render partial: "dropdown", locals: { f: f }
+            .help-block
+              = link_to 'Wildcards', help_page_path('user/project/protected_branches', anchor: 'wildcard-protected-branches')
+              such as
+              %code *-stable
+              or
+              %code production/*
+              are supported
+        .form-group
+          %label.col-md-2.text-right{ for: 'merge_access_levels_attributes' }
+            Allowed to merge:
+          .col-md-10
+            .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
+            .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/_dropdown.html.haml b/app/views/projects/protected_branches/_dropdown.html.haml
index b803d932e676057b5607e35646233c372240daa3..a9e27df5a87a0cfff07efc36a1fcaf5dbcb7dd2a 100644
--- a/app/views/projects/protected_branches/_dropdown.html.haml
+++ b/app/views/projects/protected_branches/_dropdown.html.haml
@@ -1,17 +1,15 @@
 = f.hidden_field(:name)
 
-= dropdown_tag("Protected Branch",
-               options: { title: "Pick protected branch", toggle_class: 'js-protected-branch-select js-filter-submit',
+= dropdown_tag('Select branch or create wildcard',
+               options: { toggle_class: 'js-protected-branch-select js-filter-submit wide',
                           filter: true, dropdown_class: "dropdown-menu-selectable", placeholder: "Search protected branches",
                           footer_content: true,
                           data: { show_no: true, show_any: true, show_upcoming: true,
                                   selected: params[:protected_branch_name],
                                   project_id: @project.try(:id) } }) do
 
-  %ul.dropdown-footer-list.hidden.protected-branch-select-footer-list
+  %ul.dropdown-footer-list
     %li
       = link_to '#', title: "New Protected Branch", class: "create-new-protected-branch" do
-        Create new
-
-:javascript
-  new ProtectedBranchSelect();
+        Create wildcard
+        %code
diff --git a/app/views/projects/protected_branches/_protected_branch.html.haml b/app/views/projects/protected_branches/_protected_branch.html.haml
index 498e412235e44dca9a46eec653d37e5f5e0db97e..0628134b1bb781058db0f243019071a4b6d21fc0 100644
--- a/app/views/projects/protected_branches/_protected_branch.html.haml
+++ b/app/views/projects/protected_branches/_protected_branch.html.haml
@@ -1,5 +1,4 @@
-- url = namespace_project_protected_branch_path(@project.namespace, @project, protected_branch)
-%tr
+%tr.js-protected-branch-edit-form{ data: { url: namespace_project_protected_branch_path(@project.namespace, @project, protected_branch), branch_id: protected_branch.id } }
   %td
     = protected_branch.name
     - if @project.root_ref?(protected_branch.name)
@@ -14,16 +13,9 @@
         = time_ago_with_tooltip(commit.committed_date)
       - else
         (branch was removed from repository)
-  %td
-    = hidden_field_tag "allowed_to_merge_#{protected_branch.id}", protected_branch.merge_access_level.access_level
-    = dropdown_tag(protected_branch.merge_access_level.humanize,
-                   options: { title: "Allowed to merge", toggle_class: 'allowed-to-merge', dropdown_class: 'dropdown-menu-selectable merge',
-                   data: { field_name: "allowed_to_merge_#{protected_branch.id}", url: url, id: protected_branch.id, type: "merge_access_level" }})
-  %td
-    = hidden_field_tag "allowed_to_push_#{protected_branch.id}", protected_branch.push_access_level.access_level
-    = dropdown_tag(protected_branch.push_access_level.humanize,
-                   options: { title: "Allowed to push", toggle_class: 'allowed-to-push', dropdown_class: 'dropdown-menu-selectable push',
-                   data: { field_name: "allowed_to_push_#{protected_branch.id}", url: url, id: protected_branch.id, type: "push_access_level" }})
+
+  = render partial: 'update_protected_branch', locals: { protected_branch: protected_branch }
+
   - if can_admin_project
     %td
-      = link_to 'Unprotect', [@project.namespace.becomes(Namespace), @project, protected_branch], data: { confirm: 'Branch will be writable for developers. Are you sure?' }, method: :delete, class: "btn btn-warning btn-sm pull-right"
+      = link_to 'Unprotect', [@project.namespace.becomes(Namespace), @project, protected_branch], data: { confirm: 'Branch will be writable for developers. Are you sure?' }, method: :delete, class: 'btn btn-warning'
diff --git a/app/views/projects/protected_branches/_update_protected_branch.html.haml b/app/views/projects/protected_branches/_update_protected_branch.html.haml
new file mode 100644
index 0000000000000000000000000000000000000000..d6044aacaec851bb06cbb5f921898e0dbdaccd83
--- /dev/null
+++ b/app/views/projects/protected_branches/_update_protected_branch.html.haml
@@ -0,0 +1,10 @@
+%td
+  = hidden_field_tag "allowed_to_merge_#{protected_branch.id}", protected_branch.merge_access_levels.first.access_level
+  = dropdown_tag( (protected_branch.merge_access_levels.first.humanize || 'Select') ,
+                 options: { toggle_class: 'js-allowed-to-merge', dropdown_class: 'dropdown-menu-selectable js-allowed-to-merge-container',
+                 data: { field_name: "allowed_to_merge_#{protected_branch.id}", access_level_id: protected_branch.merge_access_levels.first.id }})
+%td
+  = hidden_field_tag "allowed_to_push_#{protected_branch.id}", protected_branch.push_access_levels.first.access_level
+  = dropdown_tag( (protected_branch.push_access_levels.first.humanize || 'Select') ,
+                 options: { toggle_class: 'js-allowed-to-push', dropdown_class: 'dropdown-menu-selectable js-allowed-to-push-container',
+                 data: { field_name: "allowed_to_push_#{protected_branch.id}", access_level_id: protected_branch.push_access_levels.first.id }})
diff --git a/app/views/projects/protected_branches/index.html.haml b/app/views/projects/protected_branches/index.html.haml
index 4efe44c72335db454cd746936acc6c543b317d11..49dcc9a6ba4ea1df381a37a8504882996329b872 100644
--- a/app/views/projects/protected_branches/index.html.haml
+++ b/app/views/projects/protected_branches/index.html.haml
@@ -14,41 +14,7 @@
         %li prevent <strong>anyone</strong> from deleting the branch
       %p.append-bottom-0 Read more about #{link_to "protected branches", help_page_path("user/project/protected_branches"), class: "underlined-link"} and #{link_to "project permissions", help_page_path("user/permissions"), class: "underlined-link"}.
   .col-lg-9
-    %h5.prepend-top-0
-      Protect a branch
     - if can? current_user, :admin_project, @project
-      = form_for [@project.namespace.becomes(Namespace), @project, @protected_branch] do |f|
-        = form_errors(@protected_branch)
+      = render 'create_protected_branch'
 
-        .form-group
-          = f.label :name, "Branch", class: "label-light"
-          = render partial: "dropdown", locals: { f: f }
-          %p.help-block
-            = link_to "Wildcards", help_page_path('user/project/protected_branches', anchor: "wildcard-protected-branches")
-            such as
-            %code *-stable
-            or
-            %code production/*
-            are supported.
-
-        .form-group
-          = hidden_field_tag 'protected_branch[merge_access_level_attributes][access_level]'
-          = label_tag "Allowed to merge: ", nil, class: "label-light append-bottom-0"
-          = dropdown_tag("<Make a selection>",
-                         options: { title: "Allowed to merge", toggle_class: 'allowed-to-merge',
-                                    dropdown_class: 'dropdown-menu-selectable',
-                                    data: { field_name: "protected_branch[merge_access_level_attributes][access_level]" }})
-
-        .form-group
-          = hidden_field_tag 'protected_branch[push_access_level_attributes][access_level]'
-          = label_tag "Allowed to push: ", nil, class: "label-light append-bottom-0"
-          = dropdown_tag("<Make a selection>",
-                         options: { title: "Allowed to push", toggle_class: 'allowed-to-push',
-                                    dropdown_class: 'dropdown-menu-selectable',
-                                    data: { field_name: "protected_branch[push_access_level_attributes][access_level]" }})
-
-
-        = f.submit "Protect", class: "btn-create btn protect-branch-btn", disabled: true
-
-    %hr
     = render "branches_list"
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/show.html.haml b/app/views/projects/show.html.haml
index dd1cf680cfadbc129e32ba20b05ed1edc153a50f..340e159c87412d2330cb728889e653798a8d30f9 100644
--- a/app/views/projects/show.html.haml
+++ b/app/views/projects/show.html.haml
@@ -43,6 +43,10 @@
       %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
@@ -60,10 +64,12 @@
         %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"
@@ -82,4 +88,4 @@
         Archived project! Repository is read-only
 
   %div{class: "project-show-#{default_project_view}"}
-    = render default_project_view
\ No newline at end of file
+    = render default_project_view
diff --git a/app/views/projects/tree/_tree_commit_column.html.haml b/app/views/projects/tree/_tree_commit_column.html.haml
index a3a4bd4f7527bb1d908fab5b98f47847807a4b11..84da16b6bb15fbfc0125c3159612deeb9de93284 100644
--- a/app/views/projects/tree/_tree_commit_column.html.haml
+++ b/app/views/projects/tree/_tree_commit_column.html.haml
@@ -1,2 +1,2 @@
 %span.str-truncated
-  = link_to_gfm commit.title, namespace_project_commit_path(@project.namespace, @project, commit.id), class: "tree-commit-link"
+  = link_to_gfm commit.full_title, namespace_project_commit_path(@project.namespace, @project, commit.id), class: "tree-commit-link"
diff --git a/app/views/projects/tree/_tree_row.html.haml b/app/views/projects/tree/_tree_row.html.haml
new file mode 100644
index 0000000000000000000000000000000000000000..0a5c6f048f72026346c143ce189915c3c63ea0dc
--- /dev/null
+++ b/app/views/projects/tree/_tree_row.html.haml
@@ -0,0 +1,6 @@
+- if tree_row.type == :tree
+  = render partial: 'projects/tree/tree_item', object: tree_row, as: 'tree_item', locals: { type: 'folder' }
+- elsif tree_row.type == :blob
+  = render partial: 'projects/tree/blob_item', object: tree_row, as: 'blob_item', locals: { type: 'file' }
+- elsif tree_row.type == :commit
+  = render partial: 'projects/tree/submodule_item', object: tree_row, as: 'submodule_item'
diff --git a/app/views/projects/wikis/_form.html.haml b/app/views/projects/wikis/_form.html.haml
index 797a1a59e9f7e5cab24e5c9b26b5dd72517b65e2..643f7c589e6545243e5448eb967d63daf2a388eb 100644
--- a/app/views/projects/wikis/_form.html.haml
+++ b/app/views/projects/wikis/_form.html.haml
@@ -18,9 +18,14 @@
       .error-alert
 
       .help-block
-        To link to a (new) page, simply type
-        %code [Link Title](page-slug)
-        \.
+        = succeed '.' do
+          To link to a (new) page, simply type
+          %code [Link Title](page-slug)
+
+        = succeed '.' do
+          More examples are in the
+          = link_to 'documentation', help_page_path("user/project/markdown", anchor: "wiki-specific-markdown")
+
   .form-group
     = f.label :commit_message, class: 'control-label'
     .col-sm-10= f.text_field :message, class: 'form-control', rows: 18
diff --git a/app/views/shared/_labels_row.html.haml b/app/views/shared/_labels_row.html.haml
index dce492352ac6189543ed450268a6a39c507177df..e324d0e5203e4a1dbe51d6438b97fc46f9cad6e3 100644
--- a/app/views/shared/_labels_row.html.haml
+++ b/app/views/shared/_labels_row.html.haml
@@ -1,9 +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.name, label_filter_path(@project, label, type: controller.controller_name),
-      class: "btn btn-transparent has-tooltip",
-      style: "background-color: #{label.color};",
-      title: escape_once(label.description),
-      data: { container: "body" }
+    = link_to_label(label, 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/_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/icons/_icon_fork.svg b/app/views/shared/icons/_icon_fork.svg
index a21f8f3a95151f554d21afc9d66d0f7df16ce570..fc970e4ce5035da88d3ecd5586edb512a5bf8b9e 100644
--- a/app/views/shared/icons/_icon_fork.svg
+++ b/app/views/shared/icons/_icon_fork.svg
@@ -1,3 +1,3 @@
-<svg xmlns="http://www.w3.org/2000/svg" width="40" height="40" viewBox="0 0 40 40">
+<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.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>
diff --git a/app/views/shared/icons/_icon_play.svg b/app/views/shared/icons/_icon_play.svg
new file mode 100644
index 0000000000000000000000000000000000000000..80a6d41dbf651c6295f1e62e5e44220e7e6d457f
--- /dev/null
+++ b/app/views/shared/icons/_icon_play.svg
@@ -0,0 +1 @@
+<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 10 11"><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/_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 0b7fa8c7d06d34a5cd7aa36683aedd4aae4d6248..4f8ea7e7cef830dd668b60b79e9d35b2a4026587 100644
--- a/app/views/shared/issuable/_filter.html.haml
+++ b/app/views/shared/issuable/_filter.html.haml
@@ -27,7 +27,17 @@
           = render "shared/issuable/label_dropdown"
 
         .pull-right
-          = render 'shared/sort_dropdown'
+          - if controller.controller_name == 'boards' && can?(current_user, :admin_list, @project)
+            .dropdown
+              %button.btn.btn-create.js-new-board-list{ type: "button", data: { toggle: "dropdown", labels: labels_filter_path, project_id: @project.try(:id) } }
+                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'
       .issues_bulk_update.hide
@@ -45,7 +55,7 @@
           .filter-item.inline
             = dropdown_tag("Milestone", options: { title: "Assign milestone", toggle_class: 'js-milestone-select js-extra-options js-filter-submit js-filter-bulk-update', filter: true, dropdown_class: "dropdown-menu-selectable dropdown-menu-milestone", placeholder: "Search milestones", data: { show_no: true, field_name: "update[milestone_id]", project_id: @project.id, milestones: namespace_project_milestones_path(@project.namespace, @project, :json), use_id: true } })
           .filter-item.inline.labels-filter
-            = render "shared/issuable/label_dropdown", classes: ['js-filter-bulk-update', 'js-multiselect'], show_create: false, show_footer: false, extra_options: false, filter_submit: false, show_footer: false, data_options: { persist_when_hide: "true", field_name: "update[label_ids][]", show_no: false, show_any: false, use_id: true }
+            = render "shared/issuable/label_dropdown", classes: ['js-filter-bulk-update', 'js-multiselect'], show_create: false, show_footer: false, extra_options: false, filter_submit: false, data_options: { persist_when_hide: "true", field_name: "update[label_ids][]", show_no: false, show_any: false, use_id: true }
           .filter-item.inline
             = dropdown_tag("Subscription", options: { toggle_class: "js-subscription-event", title: "Change subscription", dropdown_class: "dropdown-menu-selectable", data: { field_name: "update[subscription_event]" } } ) do
               %ul
diff --git a/app/views/shared/issuable/_form.html.haml b/app/views/shared/issuable/_form.html.haml
index c30bdb0ae913279c1ecfa1c027b4d111b1fcc6c5..22594b464435e43865d87befa9f64ce325163002 100644
--- a/app/views/shared/issuable/_form.html.haml
+++ b/app/views/shared/issuable/_form.html.haml
@@ -1,8 +1,30 @@
 = 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'
-  .col-sm-10
+
+  - 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' }
     = f.text_field :title, maxlength: 255, autofocus: true, autocomplete: 'off',
         class: 'form-control pad', required: true
 
@@ -23,6 +45,13 @@
           to prevent a
           %strong Work In Progress
           merge request from being merged before it's ready.
+
+    - if can_add_template?(issuable)
+      %p.help-block
+        Add
+        = link_to "description templates", help_page_path('user/project/description_templates'), tabindex: -1
+        to help your contributors communicate effectively!
+
 .form-group.detail-page-description
   = f.label :description, 'Description', class: 'control-label'
   .col-sm-10
@@ -30,8 +59,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
 
@@ -98,7 +128,7 @@
     = 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.' }
@@ -149,3 +179,5 @@
         = 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_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/_sidebar.html.haml b/app/views/shared/issuable/_sidebar.html.haml
index 8e2fcbdfab8c24090ace207c8a4e9c8c53d63415..c1b50e65af5f094ecbe34559083bc7c3aea8ecc6 100644
--- a/app/views/shared/issuable/_sidebar.html.haml
+++ b/app/views/shared/issuable/_sidebar.html.haml
@@ -109,7 +109,7 @@
 
       - if issuable.project.labels.any?
         .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
diff --git a/app/views/shared/members/_member.html.haml b/app/views/shared/members/_member.html.haml
index 5ae485f36baa08ff4c7913e4c8ba1303178960ad..5f20e4bd42af1dc32ec28128e48f311c250e97fc 100644
--- a/app/views/shared/members/_member.html.haml
+++ b/app/views/shared/members/_member.html.haml
@@ -1,4 +1,4 @@
-- show_roles = local_assigns.fetch(:show_roles, default_show_roles(member))
+- show_roles = local_assigns.fetch(:show_roles, true)
 - show_controls = local_assigns.fetch(:show_controls, true)
 - user = member.user
 
@@ -16,7 +16,7 @@
           = button_tag icon('pencil'),
                        type: 'button',
                        class: 'btn inline js-toggle-button',
-                       title: 'Edit access level'
+                       title: 'Edit'
 
           - if member.request?
             = link_to icon('check inverse'), polymorphic_path([:approve_access_request, member]),
@@ -59,6 +59,10 @@
           = 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: ''
@@ -73,8 +77,16 @@
   - 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'
+      = form_for member, remote: true, html: { class: 'form-horizontal' }  do |f|
+        .form-group
+          = label_tag "member_access_level_#{member.id}", 'Project access', class: 'control-label'
+          .col-sm-10
+            = f.select :access_level, options_for_select(member.class.access_level_roles, member.access_level), {}, class: 'form-control', id: "member_access_level_#{member.id}"
+        .form-group
+          = label_tag "member_expires_at_#{member.id}", 'Access expiration date', class: 'control-label'
+          .col-sm-10
+            .clearable-input
+              = f.text_field :expires_at, class: 'form-control js-access-expiration-date', placeholder: 'Select access expiration date', id: "member_expires_at_#{member.id}"
+              %i.clear-icon.js-clear-input
         .prepend-top-10
           = f.submit 'Save', class: 'btn btn-save btn-sm'
diff --git a/app/views/shared/projects/_project.html.haml b/app/views/shared/projects/_project.html.haml
index b8b66d08db8585e5c2a850f7ca3106af8e596ead..66c309644a7f427b805649b20823f5e87f51b306 100644
--- a/app/views/shared/projects/_project.html.haml
+++ b/app/views/shared/projects/_project.html.haml
@@ -12,19 +12,21 @@
 %li.project-row{ class: css_class }
   = cache(cache_key) do
     .controls
+      - if project.archived
+        %span.label.label-warning archived
       - if project.commit.try(:status)
         %span
           = render_commit_status(project.commit)
       - 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: false)
+        = visibility_level_icon(project.visibility_level, fw: true)
 
     .title
       = link_to project_path(project), class: dom_class(project) do
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/web_hooks/_form.html.haml b/app/views/shared/web_hooks/_form.html.haml
index 106161d6515d1710a2b5d53c2977dc41761e1705..f99fbe56277bd0b0470177bdc82ffdbaf6cea643 100644
--- a/app/views/shared/web_hooks/_form.html.haml
+++ b/app/views/shared/web_hooks/_form.html.haml
@@ -19,7 +19,7 @@
         = f.label :token, "Secret Token", class: 'label-light'
         = f.text_field :token, class: "form-control", placeholder: ''
         %p.help-block
-          Use this token to validate received payloads
+          Use this token to validate received payloads. It will be sent with the request in the X-Gitlab-Token HTTP header.
       .form-group
         = f.label :url, "Trigger", class: 'label-light'
         %ul.list-unstyled
@@ -29,42 +29,49 @@
               = f.label :push_events, class: 'list-label' do
                 %strong Push events
               %p.light
-                This url will be triggered by a push to the repository
+                This URL will be triggered by a push to the repository
           %li
             = f.check_box :tag_push_events, class: 'pull-left'
             .prepend-left-20
               = f.label :tag_push_events, class: 'list-label' do
                 %strong Tag push events
               %p.light
-                This url will be triggered when a new tag is pushed to the repository
+                This URL will be triggered when a new tag is pushed to the repository
           %li
             = f.check_box :note_events, class: 'pull-left'
             .prepend-left-20
               = f.label :note_events, class: 'list-label' do
                 %strong Comments
               %p.light
-                This url will be triggered when someone adds a comment
+                This URL will be triggered when someone adds a comment
           %li
             = f.check_box :issues_events, class: 'pull-left'
             .prepend-left-20
               = f.label :issues_events, class: 'list-label' do
                 %strong Issues events
               %p.light
-                This url will be triggered when an issue is created/updated/merged
+                This URL will be triggered when an issue is created/updated/merged
           %li
             = f.check_box :merge_requests_events, class: 'pull-left'
             .prepend-left-20
               = f.label :merge_requests_events, class: 'list-label' do
                 %strong Merge Request events
               %p.light
-                This url will be triggered when a merge request is created/updated/merged
+                This URL will be triggered when a merge request is created/updated/merged
           %li
             = f.check_box :build_events, class: 'pull-left'
             .prepend-left-20
               = f.label :build_events, class: 'list-label' do
                 %strong Build events
               %p.light
-                This url will be triggered when the build status changes
+                This URL will be triggered when the build status changes
+          %li
+            = f.check_box :pipeline_events, class: 'pull-left'
+            .prepend-left-20
+              = f.label :pipeline_events, class: 'list-label' do
+                %strong Pipeline events
+              %p.light
+                This URL will be triggered when the pipeline status changes
           %li
             = f.check_box :pipeline_events, class: 'pull-left'
             .prepend-left-20
@@ -78,7 +85,7 @@
               = f.label :wiki_page_events, class: 'list-label' do
                 %strong Wiki Page events
               %p.light
-                This url will be triggered when a wiki page is created/updated
+                This URL will be triggered when a wiki page is created/updated
       .form-group
         = f.label :enable_ssl_verification, "SSL verification", class: 'label-light checkbox'
         .checkbox
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/workers/emails_on_push_worker.rb b/app/workers/emails_on_push_worker.rb
index 0b6a01a3200829b86fdd8ca2b25a37457bafcffb..1dc7e0adef7bd41773d7e72ebeace9c91fc99424 100644
--- a/app/workers/emails_on_push_worker.rb
+++ b/app/workers/emails_on_push_worker.rb
@@ -33,25 +33,14 @@ class EmailsOnPushWorker
     reverse_compare = false
 
     if action == :push
-      merge_base_sha = project.merge_base_commit(before_sha, after_sha).try(:sha)
-      compare = Gitlab::Git::Compare.new(project.repository.raw_repository, before_sha, after_sha)
-
-      diff_refs = Gitlab::Diff::DiffRefs.new(
-        base_sha: merge_base_sha,
-        start_sha: before_sha,
-        head_sha: 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 = Gitlab::Git::Compare.new(project.repository.raw_repository, after_sha, before_sha)
-
-        diff_refs = Gitlab::Diff::DiffRefs.new(
-          base_sha: merge_base_sha,
-          start_sha: after_sha,
-          head_sha: 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/group_destroy_worker.rb b/app/workers/group_destroy_worker.rb
new file mode 100644
index 0000000000000000000000000000000000000000..5048746f09ba5590f9a8b1a10318726345b112dc
--- /dev/null
+++ b/app/workers/group_destroy_worker.rb
@@ -0,0 +1,17 @@
+class GroupDestroyWorker
+  include Sidekiq::Worker
+
+  sidekiq_options queue: :default
+
+  def perform(group_id, user_id)
+    begin
+      group = Group.with_deleted.find(group_id)
+    rescue ActiveRecord::RecordNotFound
+      return
+    end
+
+    user = User.find(user_id)
+
+    DestroyGroupService.new(group, user).execute
+  end
+end
diff --git a/app/workers/gitlab_remove_project_export_worker.rb b/app/workers/import_export_project_cleanup_worker.rb
similarity index 51%
rename from app/workers/gitlab_remove_project_export_worker.rb
rename to app/workers/import_export_project_cleanup_worker.rb
index 1d91897d52039b91b54e48e541d8914578409110..72e3a9ae734686fe50e65a541b66d9a2bc7dfecb 100644
--- a/app/workers/gitlab_remove_project_export_worker.rb
+++ b/app/workers/import_export_project_cleanup_worker.rb
@@ -1,9 +1,9 @@
-class GitlabRemoveProjectExportWorker
+class ImportExportProjectCleanupWorker
   include Sidekiq::Worker
 
   sidekiq_options queue: :default
 
   def perform
-    Project.remove_gitlab_exports!
+    ImportExportCleanUpService.new.execute
   end
 end
diff --git a/app/workers/irker_worker.rb b/app/workers/irker_worker.rb
index 605ec4f04e5e0c4d318994fbfdcbca81d61367ff..19f38358eb51171951cfde71f4b4e494cd9511ad 100644
--- a/app/workers/irker_worker.rb
+++ b/app/workers/irker_worker.rb
@@ -141,8 +141,10 @@ class IrkerWorker
   end
 
   def files_count(commit)
-    files = "#{commit.diffs.real_size} file"
-    files += 's' if commit.diffs.count > 1
+    diffs = commit.raw_diffs(deltas_only: true)
+
+    files = "#{diffs.real_size} file"
+    files += 's' if diffs.size > 1
     files
   end
 
diff --git a/app/workers/post_receive.rb b/app/workers/post_receive.rb
index 09035a7cf2daaaef53c1e531d1e8e8d9e9b25888..a9a2b7160059dcda26948dc1ce9201abfc657053 100644
--- a/app/workers/post_receive.rb
+++ b/app/workers/post_receive.rb
@@ -10,6 +10,10 @@ class PostReceive
       log("Check gitlab.yml config for correct repositories.storages values. No repository storage path matches \"#{repo_path}\"")
     end
 
+    changes = Base64.decode64(changes) unless changes.include?(' ')
+    # Use Sidekiq.logger so arguments can be correlated with execution
+    # time and thread ID's.
+    Sidekiq.logger.info "changes: #{changes.inspect}" if ENV['SIDEKIQ_LOG_ARGUMENTS']
     post_received = Gitlab::GitPostReceive.new(repo_path, identifier, changes)
 
     if post_received.project.nil?
diff --git a/app/workers/project_destroy_worker.rb b/app/workers/project_destroy_worker.rb
index b51c6a266c9ae20fbe018720cd479c646dc08fc9..3062301a9b1fcf63cb91f7969fe8fd2d105f53db 100644
--- a/app/workers/project_destroy_worker.rb
+++ b/app/workers/project_destroy_worker.rb
@@ -12,6 +12,6 @@ class ProjectDestroyWorker
 
     user = User.find(user_id)
 
-    ::Projects::DestroyService.new(project, user, params).execute
+    ::Projects::DestroyService.new(project, user, params.symbolize_keys).execute
   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..246c8b6650a07a79111703b463466544f23757f8
--- /dev/null
+++ b/app/workers/remove_expired_group_links_worker.rb
@@ -0,0 +1,7 @@
+class RemoveExpiredGroupLinksWorker
+  include Sidekiq::Worker
+
+  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..cf765af97ce39e33a0df92f9d9d69a113f33b386
--- /dev/null
+++ b/app/workers/remove_expired_members_worker.rb
@@ -0,0 +1,13 @@
+class RemoveExpiredMembersWorker
+  include Sidekiq::Worker
+
+  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/repository_fork_worker.rb b/app/workers/repository_fork_worker.rb
index d69d6037053d3da556ab1b80fde4a75a529706b9..61ed1c38ac427f20cbb3a71bdbe49e768c6687c8 100644
--- a/app/workers/repository_fork_worker.rb
+++ b/app/workers/repository_fork_worker.rb
@@ -5,6 +5,10 @@ class RepositoryForkWorker
   sidekiq_options queue: :gitlab_shell
 
   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..e6701078f71f86b972cb02d268127e5ab7a40f8e 100644
--- a/app/workers/repository_import_worker.rb
+++ b/app/workers/repository_import_worker.rb
@@ -10,6 +10,10 @@ 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)
+
     result = Projects::ImportService.new(project, current_user).execute
 
     if result[:status] == :error
diff --git a/config/application.rb b/config/application.rb
index 06ebb14a5fea258e2a6bdbfff6cb6319e2a81a69..4792f6670a817636c44692f78633ed3d2bea39bd 100644
--- a/config/application.rb
+++ b/config/application.rb
@@ -85,6 +85,11 @@ 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 << "diff_notes/diff_notes_bundle.js"
+    config.assets.precompile << "boards/boards_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"
@@ -107,7 +112,8 @@ module Gitlab
       end
     end
 
-    redis_config_hash = Gitlab::Redis.redis_store_options
+    # Use Redis caching across all environments
+    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
     config.cache_store = :redis_store, redis_config_hash
diff --git a/config/initializers/1_settings.rb b/config/initializers/1_settings.rb
index 49130f37b3186e852e1e9a695d98a13f74aa2029..7a9376def02634dbb11aa5689b9f9c2a8cb8d9f5 100644
--- a/config/initializers/1_settings.rb
+++ b/config/initializers/1_settings.rb
@@ -287,12 +287,18 @@ Settings.cron_jobs['admin_email_worker']['job_class'] = 'AdminEmailWorker'
 Settings.cron_jobs['repository_archive_cache_worker'] ||= Settingslogic.new({})
 Settings.cron_jobs['repository_archive_cache_worker']['cron'] ||= '0 * * * *'
 Settings.cron_jobs['repository_archive_cache_worker']['job_class'] = 'RepositoryArchiveCacheWorker'
-Settings.cron_jobs['gitlab_remove_project_export_worker'] ||= Settingslogic.new({})
-Settings.cron_jobs['gitlab_remove_project_export_worker']['cron'] ||= '0 * * * *'
-Settings.cron_jobs['gitlab_remove_project_export_worker']['job_class'] = 'GitlabRemoveProjectExportWorker'
+Settings.cron_jobs['import_export_project_cleanup_worker'] ||= Settingslogic.new({})
+Settings.cron_jobs['import_export_project_cleanup_worker']['cron'] ||= '0 * * * *'
+Settings.cron_jobs['import_export_project_cleanup_worker']['job_class'] = 'ImportExportProjectCleanupWorker'
 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'
 
 #
 # GitLab Shell
diff --git a/config/initializers/5_backend.rb b/config/initializers/5_backend.rb
index e026151a03270c1502fc3ce68f4abbcf518c3aec..ed88c8ee1b827d043d1cb2a815112a3280a5a982 100644
--- a/config/initializers/5_backend.rb
+++ b/config/initializers/5_backend.rb
@@ -1,6 +1,3 @@
-# GIT over HTTP
-require_dependency Rails.root.join('lib/gitlab/backend/grack_auth')
-
 # GIT over SSH
 require_dependency Rails.root.join('lib/gitlab/backend/shell')
 
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/devise.rb b/config/initializers/devise.rb
index 73977341b73ae679c5382f7c0390be6d39bd778c..a0a8f88584c3dc8450daea995dc19df183b42f31 100644
--- a/config/initializers/devise.rb
+++ b/config/initializers/devise.rb
@@ -100,6 +100,9 @@ Devise.setup do |config|
   # secure: true in order to force SSL only cookies.
   # config.cookie_options = {}
 
+  # Send a notification email when the user's password is changed
+  config.send_password_change_notification = true
+
   # ==> Configuration for :validatable
   # Range for password length. Default is 6..128.
   config.password_length = 8..128
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/metrics.rb b/config/initializers/metrics.rb
index f3cddac5b364101b3b6602b0a11da43a22abd7b7..52522e099e7de06c3bd9a3588f43332f8fdadd08 100644
--- a/config/initializers/metrics.rb
+++ b/config/initializers/metrics.rb
@@ -144,6 +144,13 @@ if Gitlab::Metrics.enabled?
     end
 
     config.instrument_methods(Rinku)
+    config.instrument_instance_methods(Repository)
+
+    config.instrument_methods(Gitlab::Highlight)
+    config.instrument_instance_methods(Gitlab::Highlight)
+
+    # This is a Rails scope so we have to instrument it manually.
+    config.instrument_method(Project, :visible_to_user)
   end
 
   GC::Profiler.enable
diff --git a/config/initializers/mime_types.rb b/config/initializers/mime_types.rb
index 3e55312020593edad806eb627e1b9e749937d7c1..f498732feca21222c6c0dc921b3482463a35a7f0 100644
--- a/config/initializers/mime_types.rb
+++ b/config/initializers/mime_types.rb
@@ -12,3 +12,10 @@ Mime::Type.register_alias "text/html",  :md
 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
+})
diff --git a/config/initializers/secret_token.rb b/config/initializers/secret_token.rb
index dae3a4a9a93f26da1b6c9289bf806639508b38c3..291fa6c0abcfd0f3361d585642c2803373aa3f34 100644
--- a/config/initializers/secret_token.rb
+++ b/config/initializers/secret_token.rb
@@ -2,49 +2,86 @@
 
 require 'securerandom'
 
-# Your secret key for verifying the integrity of signed cookies.
-# If you change this key, all old signed cookies will become invalid!
-# Make sure the secret is at least 30 characters and all random,
-# no regular words or you'll be exposed to dictionary attacks.
-
-def find_secure_token
-  token_file = Rails.root.join('.secret')
-  if ENV.key?('SECRET_KEY_BASE')
-    ENV['SECRET_KEY_BASE']
-  elsif File.exist? token_file
-    # Use the existing token.
-    File.read(token_file).chomp
-  else
-    # Generate a new token of 64 random hexadecimal characters and store it in token_file.
-    token = SecureRandom.hex(64)
-    File.write(token_file, token)
-    token
+# Transition material in .secret to the secret_key_base key in config/secrets.yml.
+# Historically, ENV['SECRET_KEY_BASE'] takes precedence over .secret, so we maintain that
+# behavior.
+#
+# It also used to be the case that the key material in ENV['SECRET_KEY_BASE'] or .secret
+# was used to encrypt OTP (two-factor authentication) data so if present, we copy that key
+# material into config/secrets.yml under otp_key_base.
+#
+# Finally, if we have successfully migrated all secrets to config/secrets.yml, delete the
+# .secret file to avoid confusion.
+#
+def create_tokens
+  secret_file = Rails.root.join('.secret')
+  file_secret_key = File.read(secret_file).chomp if File.exist?(secret_file)
+  env_secret_key = ENV['SECRET_KEY_BASE']
+
+  # Ensure environment variable always overrides secrets.yml.
+  Rails.application.secrets.secret_key_base = env_secret_key if env_secret_key.present?
+
+  defaults = {
+    secret_key_base: file_secret_key || generate_new_secure_token,
+    otp_key_base: env_secret_key || file_secret_key || generate_new_secure_token,
+    db_key_base: generate_new_secure_token
+  }
+
+  missing_secrets = set_missing_keys(defaults)
+  write_secrets_yml(missing_secrets) unless missing_secrets.empty?
+
+  begin
+    File.delete(secret_file) if file_secret_key
+  rescue => e
+    warn "Error deleting useless .secret file: #{e}"
   end
 end
 
-Rails.application.config.secret_token = find_secure_token
-Rails.application.config.secret_key_base = find_secure_token
-
-# CI
 def generate_new_secure_token
   SecureRandom.hex(64)
 end
 
-if Rails.application.secrets.db_key_base.blank?
-  warn "Missing `db_key_base` for '#{Rails.env}' environment. The secrets will be generated and stored in `config/secrets.yml`"
+def warn_missing_secret(secret)
+  warn "Missing Rails.application.secrets.#{secret} for #{Rails.env} environment. The secret will be generated and stored in config/secrets.yml."
+end
 
-  all_secrets = YAML.load_file('config/secrets.yml') if File.exist?('config/secrets.yml')
-  all_secrets ||= {}
+def set_missing_keys(defaults)
+  defaults.stringify_keys.each_with_object({}) do |(key, default), missing|
+    if Rails.application.secrets[key].blank?
+      warn_missing_secret(key)
 
-  # generate secrets
-  env_secrets = all_secrets[Rails.env.to_s] || {}
-  env_secrets['db_key_base'] ||= generate_new_secure_token
-  all_secrets[Rails.env.to_s] = env_secrets
+      missing[key] = Rails.application.secrets[key] = default
+    end
+  end
+end
+
+def write_secrets_yml(missing_secrets)
+  secrets_yml = Rails.root.join('config/secrets.yml')
+  rails_env = Rails.env.to_s
+  secrets = YAML.load_file(secrets_yml) if File.exist?(secrets_yml)
+  secrets ||= {}
+  secrets[rails_env] ||= {}
+
+  secrets[rails_env].merge!(missing_secrets) do |key, old, new|
+    # Previously, it was possible this was set to the literal contents of an Erb
+    # expression that evaluated to an empty value. We don't want to support that
+    # specifically, just ensure we don't break things further.
+    #
+    if old.present?
+      warn <<EOM
+Rails.application.secrets.#{key} was blank, but the literal value in config/secrets.yml was:
+  #{old}
 
-  # save secrets
-  File.open('config/secrets.yml', 'w', 0600) do |file|
-    file.write(YAML.dump(all_secrets))
+This probably isn't the expected value for this secret. To keep using a literal Erb string in config/secrets.yml, replace `<%` with `<%%`.
+EOM
+
+      exit 1 # rubocop:disable Rails/Exit
+    end
+
+    new
   end
 
-  Rails.application.secrets.db_key_base = env_secrets['db_key_base']
+  File.write(secrets_yml, YAML.dump(secrets), mode: 'w', perm: 0600)
 end
+
+create_tokens
diff --git a/config/initializers/session_store.rb b/config/initializers/session_store.rb
index 0d9d87bac00a3169cf681e66f42c0bcbfab07da2..70be2617cabf22de3b8b33dc6a825fd64293b2b6 100644
--- a/config/initializers/session_store.rb
+++ b/config/initializers/session_store.rb
@@ -13,9 +13,9 @@ end
 if Rails.env.test?
   Gitlab::Application.config.session_store :cookie_store, key: "_gitlab_session"
 else
-  redis_config = Gitlab::Redis.redis_store_options
+  redis_config = Gitlab::Redis.params
   redis_config[:namespace] = Gitlab::Redis::SESSION_NAMESPACE
-  
+
   Gitlab::Application.config.session_store(
     :redis_store, # Using the cookie_store would enable session replay attacks.
     servers: redis_config,
diff --git a/config/initializers/sidekiq.rb b/config/initializers/sidekiq.rb
index cf49ec2194c8e1e682d073262091aea10e7ffd9e..f7e714cd6bc7855a48e268b984a8f3a64e1c4c38 100644
--- a/config/initializers/sidekiq.rb
+++ b/config/initializers/sidekiq.rb
@@ -1,8 +1,9 @@
+# Custom Redis configuration
+redis_config_hash = Gitlab::Redis.params
+redis_config_hash[:namespace] = Gitlab::Redis::SIDEKIQ_NAMESPACE
+
 Sidekiq.configure_server do |config|
-  config.redis = {
-    url: Gitlab::Redis.url,
-    namespace: Gitlab::Redis::SIDEKIQ_NAMESPACE
-  }
+  config.redis = redis_config_hash
 
   config.server_middleware do |chain|
     chain.add Gitlab::SidekiqMiddleware::ArgumentsLogger if ENV['SIDEKIQ_LOG_ARGUMENTS']
@@ -39,8 +40,5 @@ Sidekiq.configure_server do |config|
 end
 
 Sidekiq.configure_client do |config|
-  config.redis = {
-    url: Gitlab::Redis.url,
-    namespace: Gitlab::Redis::SIDEKIQ_NAMESPACE
-  }
+  config.redis = redis_config_hash
 end
diff --git a/config/mail_room.yml b/config/mail_room.yml
index 7cab24b295e863ec0d19b95d25b198e74b6d80d8..c639f8260aa1b2785ca0e044fe370051623f9230 100644
--- a/config/mail_room.yml
+++ b/config/mail_room.yml
@@ -1,47 +1,36 @@
+# If you change this file in a Merge Request, please also create
+# a Merge Request on https://gitlab.com/gitlab-org/omnibus-gitlab/merge_requests
+#
 :mailboxes:
-<%
-require "yaml"
-require "json"
-require_relative "lib/gitlab/redis" unless defined?(Gitlab::Redis)
+  <%
+    require_relative "lib/gitlab/mail_room" unless defined?(Gitlab::MailRoom)
+    config = Gitlab::MailRoom.config
 
-rails_env = ENV["RAILS_ENV"] || ENV["RACK_ENV"] || "development"
-
-config_file = ENV["MAIL_ROOM_GITLAB_CONFIG_FILE"] || "config/gitlab.yml"
-if File.exists?(config_file)
-  all_config = YAML.load_file(config_file)[rails_env]
-
-  config = all_config["incoming_email"] || {}
-  config['enabled']    = false    if config['enabled'].nil?
-  config['port']       = 143      if config['port'].nil?
-  config['ssl']        = false    if config['ssl'].nil?
-  config['start_tls']  = false    if config['start_tls'].nil?
-  config['mailbox']    = "inbox"  if config['mailbox'].nil?
-
-  if config['enabled'] && config['address']
-    redis_url = Gitlab::Redis.new(rails_env).url
-    %>
+    if Gitlab::MailRoom.enabled?
+  %>
     -
-      :host: <%= config['host'].to_json %>
-      :port: <%= config['port'].to_json %>
-      :ssl: <%= config['ssl'].to_json %>
-      :start_tls: <%= config['start_tls'].to_json %>
-      :email: <%= config['user'].to_json %>
-      :password: <%= config['password'].to_json %>
+      :host: <%= config[:host].to_json %>
+      :port: <%= config[:port].to_json %>
+      :ssl: <%= config[:ssl].to_json %>
+      :start_tls: <%= config[:start_tls].to_json %>
+      :email: <%= config[:user].to_json %>
+      :password: <%= config[:password].to_json %>
+      :idle_timeout: 60
 
-      :name: <%= config['mailbox'].to_json %>
+      :name: <%= config[:mailbox].to_json %>
 
       :delete_after_delivery: true
 
       :delivery_method: sidekiq
       :delivery_options:
-        :redis_url: <%= redis_url.to_json %>
-        :namespace: resque:gitlab
+        :redis_url: <%= config[:redis_url].to_json %>
+        :namespace: <%= Gitlab::Redis::SIDEKIQ_NAMESPACE %>
         :queue: incoming_email
         :worker: EmailReceiverWorker
 
       :arbitration_method: redis
       :arbitration_options:
-        :redis_url: <%= redis_url.to_json %>
-        :namespace: mail_room:gitlab
+        :redis_url: <%= config[:redis_url].to_json %>
+        :namespace: <%= Gitlab::Redis::MAILROOM_NAMESPACE %>
+
   <% end %>
-<% end %>
diff --git a/config/resque.yml.example b/config/resque.yml.example
index d98f43f71b298743b7feeef51e64cde6e73802fd..0c19d8bc1d36afd58a8e6964ce0fc7293c5d0613 100644
--- a/config/resque.yml.example
+++ b/config/resque.yml.example
@@ -1,6 +1,34 @@
 # If you change this file in a Merge Request, please also create
 # a Merge Request on https://gitlab.com/gitlab-org/omnibus-gitlab/merge_requests
 #
-development: redis://localhost:6379
-test: redis://localhost:6379
-production: unix:/var/run/redis/redis.sock
+development:
+  url: redis://localhost:6379
+  # sentinels:
+  #   -
+  #     host: localhost
+  #     port: 26380 # point to sentinel, not to redis port
+  #   -
+  #     host: slave2
+  #     port: 26381 # point to sentinel, not to redis port
+test:
+  url: redis://localhost:6379
+production:
+  # Redis (single instance)
+  url: unix:/var/run/redis/redis.sock
+  ##
+  # Redis + Sentinel (for HA)
+  #
+  # Please read instructions carefully before using it as you may lose data:
+  # http://redis.io/topics/sentinel
+  #
+  # You must specify a list of a few sentinels that will handle client connection
+  # please read here for more information: https://docs.gitlab.com/ce/administration/high_availability/redis.html
+  ##
+  # url: redis://master:6379
+  # sentinels:
+  #   -
+  #     host: slave1
+  #     port: 26379 # point to sentinel, not to redis port
+  #   -
+  #     host: slave2
+  #     port: 26379 # point to sentinel, not to redis port
diff --git a/config/routes.rb b/config/routes.rb
index 371eb4bee7f6e4844148dfcfbe31dfa69ccec2ab..e93b640fbc0958a413da4af9ddfc125bfb19a0a9 100644
--- a/config/routes.rb
+++ b/config/routes.rb
@@ -84,15 +84,17 @@ Rails.application.routes.draw do
   # Health check
   get 'health_check(/:checks)' => 'health_check#index', as: :health_check
 
-  # Enable Grack support (for LFS only)
-  mount Grack::AuthSpawner, at: '/', constraints: lambda { |request| /[-\/\w\.]+\.git\/(info\/lfs|gitlab-lfs)/.match(request.path_info) }, via: [:get, :post, :put]
-
   # 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
   #
@@ -255,7 +257,11 @@ Rails.application.routes.draw do
     resource :impersonation, only: :destroy
 
     resources :abuse_reports, only: [:index, :destroy]
-    resources :spam_logs, only: [:index, :destroy]
+    resources :spam_logs, only: [:index, :destroy] do
+      member do
+        post :mark_as_ham
+      end
+    end
 
     resources :applications
 
@@ -374,6 +380,8 @@ Rails.application.routes.draw do
           patch :skip
         end
       end
+
+      resources :u2f_registrations, only: [:destroy]
     end
   end
 
@@ -471,7 +479,7 @@ Rails.application.routes.draw do
         post :unarchive
         post :housekeeping
         post :toggle_star
-        post :markdown_preview
+        post :preview_markdown
         post :export
         post :remove_export
         post :generate_new_export
@@ -482,11 +490,26 @@ Rails.application.routes.draw do
       end
 
       scope module: :projects do
-        # Git HTTP clients ('git clone' etc.)
         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
@@ -512,6 +535,11 @@ Rails.application.routes.draw do
         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',
@@ -626,13 +654,17 @@ Rails.application.routes.draw do
 
         get '/compare/:from...:to', to: 'compare#show', as: 'compare', constraints: { from: /.+/, to: /.+/ }
 
-        resources :network, only: [:show], constraints: { id: /(?:[^.]|\.(?!json$))+/, format: /json/ }
+        # 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: /(?:[^.]|\.(?!json$))+/, format: /json/ } do
-          member do
-            get :commits
-            get :ci
-            get :languages
+          resources :graphs, only: [:show], constraints: { id: Gitlab::Regex.git_reference_regex } do
+            member do
+              get :commits
+              get :ci
+              get :languages
+            end
           end
         end
 
@@ -656,7 +688,7 @@ Rails.application.routes.draw do
           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/markdown_preview', to: 'wikis#markdown_preview', constraints: WIKI_SLUG_ID, as: 'wiki_markdown_preview'
+          post '/wikis/*id/preview_markdown', to: 'wikis#preview_markdown', constraints: WIKI_SLUG_ID, as: 'wiki_preview_markdown'
         end
 
         resource :repository, only: [:create] do
@@ -702,7 +734,9 @@ Rails.application.routes.draw do
           member do
             get :commits
             get :diffs
+            get :conflicts
             get :builds
+            get :pipelines
             get :merge_check
             post :merge
             post :cancel_merge_when_build_succeeds
@@ -711,6 +745,7 @@ Rails.application.routes.draw do
             post :toggle_award_emoji
             post :remove_wip
             get :diff_for_path
+            post :resolve_conflicts
           end
 
           collection do
@@ -719,6 +754,13 @@ Rails.application.routes.draw do
             get :update_branches
             get :diff_for_path
           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 }
@@ -797,6 +839,7 @@ Rails.application.routes.draw 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
@@ -827,6 +870,22 @@ Rails.application.routes.draw do
           member do
             post :toggle_award_emoji
             delete :delete_attachment
+            post :resolve
+            delete :resolve, action: :unresolve
+          end
+        end
+
+        resource :board, only: [: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]
+            end
           end
         end
 
@@ -853,7 +912,10 @@ Rails.application.routes.draw do
         resources :badges, only: [:index] do
           collection do
             scope '*ref', constraints: { ref: Gitlab::Regex.git_reference_regex } do
-              get :build, constraints: { format: /svg/ }
+              constraints format: /svg/ do
+                get :build
+                get :coverage
+              end
             end
           end
         end
diff --git a/db/fixtures/development/14_builds.rb b/db/fixtures/development/14_builds.rb
deleted file mode 100644
index 124704cb45157574822bef247b469bbbb72c6235..0000000000000000000000000000000000000000
--- a/db/fixtures/development/14_builds.rb
+++ /dev/null
@@ -1,123 +0,0 @@
-class Gitlab::Seeder::Builds
-  STAGES = %w[build notify_build test notify_test deploy notify_deploy]
-  
-  def initialize(project)
-    @project = project
-  end
-
-  def seed!
-    pipelines.each do |pipeline|
-      begin
-        build_create!(pipeline, name: 'build:linux', stage: 'build')
-        build_create!(pipeline, name: 'build:osx', stage: 'build')
-
-        build_create!(pipeline, name: 'slack post build', stage: 'notify_build')
-
-        build_create!(pipeline, name: 'rspec:linux', stage: 'test')
-        build_create!(pipeline, name: 'rspec:windows', stage: 'test')
-        build_create!(pipeline, name: 'rspec:windows', stage: 'test')
-        build_create!(pipeline, name: 'rspec:osx', stage: 'test')
-        build_create!(pipeline, name: 'spinach:linux', stage: 'test')
-        build_create!(pipeline, name: 'spinach:osx', stage: 'test')
-        build_create!(pipeline, name: 'cucumber:linux', stage: 'test')
-        build_create!(pipeline, name: 'cucumber:osx', stage: 'test')
-
-        build_create!(pipeline, name: 'slack post test', stage: 'notify_test')
-
-        build_create!(pipeline, name: 'staging', stage: 'deploy', environment: 'staging')
-        build_create!(pipeline, name: 'production', stage: 'deploy', environment: 'production', when: 'manual')
-
-        commit_status_create!(pipeline, name: 'jenkins')
-
-        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)
-    build = Ci::Build.new(attributes)
-
-    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
-
-    build.save!
-    build.update(status: build_status)
-
-    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
-  
-  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', 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..49e6e2361b1d7a8e3a4ccddf9a4c4f5935c35a9e
--- /dev/null
+++ b/db/fixtures/development/14_pipelines.rb
@@ -0,0 +1,153 @@
+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', 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: :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 },
+    { 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.build_updated
+      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/migrate/20140407135544_fix_namespaces.rb b/db/migrate/20140407135544_fix_namespaces.rb
index 9137496669837f189ae1ccc2c04b89e09e691350..0026ce645a6617ac9b40e8e177be400d310d20d7 100644
--- a/db/migrate/20140407135544_fix_namespaces.rb
+++ b/db/migrate/20140407135544_fix_namespaces.rb
@@ -1,8 +1,14 @@
 # rubocop:disable all
 class FixNamespaces < ActiveRecord::Migration
+  DOWNTIME = false
+
   def up
-    Namespace.where('name <> path and type is null').each do |namespace|
-      namespace.update_attribute(:name, namespace.path)
+    namespaces = exec_query('SELECT id, path FROM namespaces WHERE name <> path and type is null')
+
+    namespaces.each do |row|
+      id = row['id']
+      path = row['path']
+      exec_query("UPDATE namespaces SET name = '#{path}' WHERE id = #{id}")
     end
   end
 
diff --git a/db/migrate/20160705055254_move_from_developers_can_merge_to_protected_branches_merge_access.rb b/db/migrate/20160705055254_move_from_developers_can_merge_to_protected_branches_merge_access.rb
index fa93936ced7637deae62ac332de08757dadd4bc7..1db0df92becc3e6a384772e7fea917ca684167a6 100644
--- a/db/migrate/20160705055254_move_from_developers_can_merge_to_protected_branches_merge_access.rb
+++ b/db/migrate/20160705055254_move_from_developers_can_merge_to_protected_branches_merge_access.rb
@@ -14,7 +14,7 @@ class MoveFromDevelopersCanMergeToProtectedBranchesMergeAccess < ActiveRecord::M
   def up
     execute <<-HEREDOC
       INSERT into protected_branch_merge_access_levels (protected_branch_id, access_level, created_at, updated_at)
-        SELECT id, (CASE WHEN developers_can_merge THEN 1 ELSE 0 END), now(), now()
+        SELECT id, (CASE WHEN developers_can_merge THEN 30 ELSE 40 END), now(), now()
           FROM protected_branches
     HEREDOC
   end
@@ -23,7 +23,7 @@ class MoveFromDevelopersCanMergeToProtectedBranchesMergeAccess < ActiveRecord::M
     execute <<-HEREDOC
       UPDATE protected_branches SET developers_can_merge = TRUE
         WHERE id IN (SELECT protected_branch_id FROM protected_branch_merge_access_levels
-                       WHERE access_level = 1);
+                       WHERE access_level = 30);
     HEREDOC
   end
 end
diff --git a/db/migrate/20160705055308_move_from_developers_can_push_to_protected_branches_push_access.rb b/db/migrate/20160705055308_move_from_developers_can_push_to_protected_branches_push_access.rb
index 56f6159d1d8e6ce1a63ae22187fa5fb83f6cf26c..5c3e189bb5ba038857cc18237df2189bbb70a069 100644
--- a/db/migrate/20160705055308_move_from_developers_can_push_to_protected_branches_push_access.rb
+++ b/db/migrate/20160705055308_move_from_developers_can_push_to_protected_branches_push_access.rb
@@ -14,7 +14,7 @@ class MoveFromDevelopersCanPushToProtectedBranchesPushAccess < ActiveRecord::Mig
   def up
     execute <<-HEREDOC
       INSERT into protected_branch_push_access_levels (protected_branch_id, access_level, created_at, updated_at)
-        SELECT id, (CASE WHEN developers_can_push THEN 1 ELSE 0 END), now(), now()
+        SELECT id, (CASE WHEN developers_can_push THEN 30 ELSE 40 END), now(), now()
           FROM protected_branches
     HEREDOC
   end
@@ -23,7 +23,7 @@ class MoveFromDevelopersCanPushToProtectedBranchesPushAccess < ActiveRecord::Mig
     execute <<-HEREDOC
       UPDATE protected_branches SET developers_can_push = TRUE
         WHERE id IN (SELECT protected_branch_id FROM protected_branch_push_access_levels
-                       WHERE access_level = 1);
+                       WHERE access_level = 30);
     HEREDOC
   end
 end
diff --git a/db/migrate/20160705055809_remove_developers_can_push_from_protected_branches.rb b/db/migrate/20160705055809_remove_developers_can_push_from_protected_branches.rb
index f563f660ddf301069ba103662892cdcc7bc8d8b0..52a9819c6287b97dbb8b933faf6e11df5d622012 100644
--- a/db/migrate/20160705055809_remove_developers_can_push_from_protected_branches.rb
+++ b/db/migrate/20160705055809_remove_developers_can_push_from_protected_branches.rb
@@ -14,6 +14,6 @@ class RemoveDevelopersCanPushFromProtectedBranches < ActiveRecord::Migration
   end
 
   def down
-    add_column_with_default(:protected_branches, :developers_can_push, :boolean, default: false, null: false)
+    add_column_with_default(:protected_branches, :developers_can_push, :boolean, default: false, allow_null: false)
   end
 end
diff --git a/db/migrate/20160705055813_remove_developers_can_merge_from_protected_branches.rb b/db/migrate/20160705055813_remove_developers_can_merge_from_protected_branches.rb
index aa71e06d36e846abdefbbdcd6ad73fcc1ddb2135..4a7bde7f9f33774f14e95bac9647bcd0b68b6629 100644
--- a/db/migrate/20160705055813_remove_developers_can_merge_from_protected_branches.rb
+++ b/db/migrate/20160705055813_remove_developers_can_merge_from_protected_branches.rb
@@ -14,6 +14,6 @@ class RemoveDevelopersCanMergeFromProtectedBranches < ActiveRecord::Migration
   end
 
   def down
-    add_column_with_default(:protected_branches, :developers_can_merge, :boolean, default: false, null: false)
+    add_column_with_default(:protected_branches, :developers_can_merge, :boolean, default: false, allow_null: false)
   end
 end
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/20160716115711_add_queued_at_to_ci_builds.rb b/db/migrate/20160716115711_add_queued_at_to_ci_builds.rb
new file mode 100644
index 0000000000000000000000000000000000000000..756910a1fa0cdb819d0013d38ba41f1e82dad1d2
--- /dev/null
+++ b/db/migrate/20160716115711_add_queued_at_to_ci_builds.rb
@@ -0,0 +1,9 @@
+class AddQueuedAtToCiBuilds < ActiveRecord::Migration
+  include Gitlab::Database::MigrationHelpers
+
+  DOWNTIME = false
+
+  def change
+    add_column :ci_builds, :queued_at, :timestamp
+  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/20160727163552_create_user_agent_details.rb b/db/migrate/20160727163552_create_user_agent_details.rb
new file mode 100644
index 0000000000000000000000000000000000000000..ed4ccfedc0a4a73b7765e562a851f832c4d2c939
--- /dev/null
+++ b/db/migrate/20160727163552_create_user_agent_details.rb
@@ -0,0 +1,18 @@
+class CreateUserAgentDetails < ActiveRecord::Migration
+  include Gitlab::Database::MigrationHelpers
+
+  # Set this constant to true if this migration requires downtime.
+  DOWNTIME = false
+
+  def change
+    create_table :user_agent_details 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.timestamps null: false
+    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/20160729173930_remove_project_id_from_spam_logs.rb b/db/migrate/20160729173930_remove_project_id_from_spam_logs.rb
new file mode 100644
index 0000000000000000000000000000000000000000..e28ab31d629d049a458d00c24559dd7210511235
--- /dev/null
+++ b/db/migrate/20160729173930_remove_project_id_from_spam_logs.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 RemoveProjectIdFromSpamLogs < 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 = 'Removing a column that contains data that is not used anywhere.'
+
+  # 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
+    remove_column :spam_logs, :project_id, :integer
+  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/20160801163709_add_submitted_as_ham_to_spam_logs.rb b/db/migrate/20160801163709_add_submitted_as_ham_to_spam_logs.rb
new file mode 100644
index 0000000000000000000000000000000000000000..296f1dfac7b2b2f1b95537c9c2e3c97b8fb62015
--- /dev/null
+++ b/db/migrate/20160801163709_add_submitted_as_ham_to_spam_logs.rb
@@ -0,0 +1,20 @@
+# See http://doc.gitlab.com/ce/development/migration_style_guide.html
+# for more information on how to write migrations for GitLab.
+
+class AddSubmittedAsHamToSpamLogs < 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 change
+    add_column_with_default :spam_logs, :submitted_as_ham, :boolean, default: false
+  end
+end
diff --git a/db/migrate/20160802010328_remove_builds_enable_index_on_projects.rb b/db/migrate/20160802010328_remove_builds_enable_index_on_projects.rb
new file mode 100644
index 0000000000000000000000000000000000000000..5fd51cb65f16fdaae2e2bce3ecb81457197eb3ed
--- /dev/null
+++ b/db/migrate/20160802010328_remove_builds_enable_index_on_projects.rb
@@ -0,0 +1,9 @@
+class RemoveBuildsEnableIndexOnProjects < ActiveRecord::Migration
+  include Gitlab::Database::MigrationHelpers
+
+  DOWNTIME = false
+
+  def change
+    remove_index :projects, column: :builds_enabled if index_exists?(:projects, :builds_enabled)
+  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/20160804150737_add_timestamps_to_members_again.rb b/db/migrate/20160804150737_add_timestamps_to_members_again.rb
new file mode 100644
index 0000000000000000000000000000000000000000..6691ba57fbb323a7176b33683efb7f1402e55333
--- /dev/null
+++ b/db/migrate/20160804150737_add_timestamps_to_members_again.rb
@@ -0,0 +1,21 @@
+# rubocop:disable all
+# 20141121133009_add_timestamps_to_members.rb was meant to ensure that all
+# rows in the members table had created_at and updated_at set, following an
+# error in a previous migration. This failed to set all rows in at least one
+# case: https://gitlab.com/gitlab-org/gitlab-ce/issues/20568
+#
+# Why this happened is lost in the mists of time, so repeat the SQL query
+# without speculation, just in case more than one person was affected.
+class AddTimestampsToMembersAgain < ActiveRecord::Migration
+  DOWNTIME = false
+
+  def up
+    execute "UPDATE members SET created_at = NOW() WHERE created_at IS NULL"
+    execute "UPDATE members SET updated_at = NOW() WHERE updated_at IS NULL"
+  end
+
+  def down
+    # no change
+  end
+
+end
diff --git a/db/migrate/20160805041956_add_deleted_at_to_namespaces.rb b/db/migrate/20160805041956_add_deleted_at_to_namespaces.rb
new file mode 100644
index 0000000000000000000000000000000000000000..a853de3abfbed5842ea423efbe77c077d0163028
--- /dev/null
+++ b/db/migrate/20160805041956_add_deleted_at_to_namespaces.rb
@@ -0,0 +1,12 @@
+class AddDeletedAtToNamespaces < ActiveRecord::Migration
+  include Gitlab::Database::MigrationHelpers
+
+  DOWNTIME = false
+
+  disable_ddl_transaction!
+
+  def change
+    add_column :namespaces, :deleted_at, :datetime
+    add_concurrent_index :namespaces, :deleted_at
+  end
+end
diff --git a/db/migrate/20160810102349_remove_ci_runner_trigram_indexes.rb b/db/migrate/20160810102349_remove_ci_runner_trigram_indexes.rb
new file mode 100644
index 0000000000000000000000000000000000000000..0cfb637804bb56e301245ada56042d5dfd0bad17
--- /dev/null
+++ b/db/migrate/20160810102349_remove_ci_runner_trigram_indexes.rb
@@ -0,0 +1,27 @@
+# See http://doc.gitlab.com/ce/development/migration_style_guide.html
+# for more information on how to write migrations for GitLab.
+
+class RemoveCiRunnerTrigramIndexes < ActiveRecord::Migration
+  include Gitlab::Database::MigrationHelpers
+
+  DOWNTIME = false
+
+  # Disabled for the "down" method so the indexes can be re-created concurrently.
+  disable_ddl_transaction!
+
+  def up
+    return unless Gitlab::Database.postgresql?
+
+    transaction do
+      execute 'DROP INDEX IF EXISTS index_ci_runners_on_token_trigram;'
+      execute 'DROP INDEX IF EXISTS index_ci_runners_on_description_trigram;'
+    end
+  end
+
+  def down
+    return unless Gitlab::Database.postgresql?
+
+    execute 'CREATE INDEX CONCURRENTLY index_ci_runners_on_token_trigram ON ci_runners USING gin(token gin_trgm_ops);'
+    execute 'CREATE INDEX CONCURRENTLY index_ci_runners_on_description_trigram ON ci_runners USING gin(description gin_trgm_ops);'
+  end
+end
diff --git a/db/migrate/20160810142633_remove_redundant_indexes.rb b/db/migrate/20160810142633_remove_redundant_indexes.rb
new file mode 100644
index 0000000000000000000000000000000000000000..8641c6ffa8f9fdb5ecf91068bdf3a20e24e3db23
--- /dev/null
+++ b/db/migrate/20160810142633_remove_redundant_indexes.rb
@@ -0,0 +1,112 @@
+# See http://doc.gitlab.com/ce/development/migration_style_guide.html
+# for more information on how to write migrations for GitLab.
+
+class RemoveRedundantIndexes < ActiveRecord::Migration
+  include Gitlab::Database::MigrationHelpers
+
+  DOWNTIME = false
+
+  disable_ddl_transaction!
+
+  def up
+    indexes = [
+      [:ci_taggings, 'ci_taggings_idx'],
+      [:audit_events, 'index_audit_events_on_author_id'],
+      [:audit_events, 'index_audit_events_on_type'],
+      [:ci_builds, 'index_ci_builds_on_erased_by_id'],
+      [:ci_builds, 'index_ci_builds_on_project_id_and_commit_id'],
+      [:ci_builds, 'index_ci_builds_on_type'],
+      [:ci_commits, 'index_ci_commits_on_project_id'],
+      [:ci_commits, 'index_ci_commits_on_project_id_and_committed_at'],
+      [:ci_commits, 'index_ci_commits_on_project_id_and_committed_at_and_id'],
+      [:ci_commits, 'index_ci_commits_on_project_id_and_sha'],
+      [:ci_commits, 'index_ci_commits_on_sha'],
+      [:ci_events, 'index_ci_events_on_created_at'],
+      [:ci_events, 'index_ci_events_on_is_admin'],
+      [:ci_events, 'index_ci_events_on_project_id'],
+      [:ci_jobs, 'index_ci_jobs_on_deleted_at'],
+      [:ci_jobs, 'index_ci_jobs_on_project_id'],
+      [:ci_projects, 'index_ci_projects_on_gitlab_id'],
+      [:ci_projects, 'index_ci_projects_on_shared_runners_enabled'],
+      [:ci_services, 'index_ci_services_on_project_id'],
+      [:ci_sessions, 'index_ci_sessions_on_session_id'],
+      [:ci_sessions, 'index_ci_sessions_on_updated_at'],
+      [:ci_tags, 'index_ci_tags_on_name'],
+      [:ci_triggers, 'index_ci_triggers_on_deleted_at'],
+      [:identities, 'index_identities_on_created_at_and_id'],
+      [:issues, 'index_issues_on_title'],
+      [:keys, 'index_keys_on_created_at_and_id'],
+      [:members, 'index_members_on_created_at_and_id'],
+      [:members, 'index_members_on_type'],
+      [:milestones, 'index_milestones_on_created_at_and_id'],
+      [:namespaces, 'index_namespaces_on_visibility_level'],
+      [:projects, 'index_projects_on_builds_enabled_and_shared_runners_enabled'],
+      [:services, 'index_services_on_category'],
+      [:services, 'index_services_on_created_at_and_id'],
+      [:services, 'index_services_on_default'],
+      [:snippets, 'index_snippets_on_created_at'],
+      [:snippets, 'index_snippets_on_created_at_and_id'],
+      [:todos, 'index_todos_on_state'],
+      [:web_hooks, 'index_web_hooks_on_created_at_and_id'],
+
+      # These indexes _may_ be used but they can be replaced by other existing
+      # indexes.
+
+      # There's already a composite index on (project_id, iid) which means that
+      # a separate index for _just_ project_id is not needed.
+      [:issues, 'index_issues_on_project_id'],
+
+      # These are all composite indexes for the columns (created_at, id). In all
+      # these cases there's already a standalone index for "created_at" which
+      # can be used instead.
+      #
+      # Because the "id" column of these composite indexes is never needed (due
+      # to "id" already being indexed as its a primary key) these composite
+      # indexes are useless.
+      [:issues, 'index_issues_on_created_at_and_id'],
+      [:merge_requests, 'index_merge_requests_on_created_at_and_id'],
+      [:namespaces, 'index_namespaces_on_created_at_and_id'],
+      [:notes, 'index_notes_on_created_at_and_id'],
+      [:projects, 'index_projects_on_created_at_and_id'],
+      [:users, 'index_users_on_created_at_and_id'],
+    ]
+
+    transaction do
+      indexes.each do |(table, index)|
+        remove_index(table, name: index) if index_exists_by_name?(table, index)
+      end
+    end
+
+    add_concurrent_index(:users, :created_at)
+    add_concurrent_index(:projects, :created_at)
+    add_concurrent_index(:namespaces, :created_at)
+  end
+
+  def down
+    # We're only restoring the composite indexes that could be replaced with
+    # individual ones, just in case somebody would ever want to revert.
+    transaction do
+      remove_index(:users, :created_at)
+      remove_index(:projects, :created_at)
+      remove_index(:namespaces, :created_at)
+    end
+
+    [:issues, :merge_requests, :namespaces, :notes, :projects, :users].each do |table|
+      add_concurrent_index(table, [:created_at, :id],
+                           name: "index_#{table}_on_created_at_and_id")
+    end
+  end
+
+  # Rails' index_exists? doesn't work when you only give it a table and index
+  # name. As such we have to use some extra code to check if an index exists for
+  # a given name.
+  def index_exists_by_name?(table, index)
+    indexes_for_table[table].include?(index)
+  end
+
+  def indexes_for_table
+    @indexes_for_table ||= Hash.new do |hash, table_name|
+      hash[table_name] = indexes(table_name).map(&:name)
+    end
+  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/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/schema.rb b/db/schema.rb
index 5b35a528e71b7c1ff0fa6028d94fe0391c88c211..802c928b2fc8c77ca257b8438051208acfb1dd45 100644
--- a/db/schema.rb
+++ b/db/schema.rb
@@ -11,7 +11,7 @@
 #
 # It's strongly recommended that you check this file into your version control system.
 
-ActiveRecord::Schema.define(version: 20160726093600) do
+ActiveRecord::Schema.define(version: 20160823081327) do
 
   # These are extensions that must be enabled in order to support this database
   enable_extension "plpgsql"
@@ -84,12 +84,14 @@ ActiveRecord::Schema.define(version: 20160726093600) do
     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.boolean  "user_default_external",                 default: false,       null: false
     t.string   "repository_storage",                    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"
   end
 
   create_table "audit_events", force: :cascade do |t|
@@ -102,9 +104,7 @@ ActiveRecord::Schema.define(version: 20160726093600) do
     t.datetime "updated_at"
   end
 
-  add_index "audit_events", ["author_id"], name: "index_audit_events_on_author_id", using: :btree
   add_index "audit_events", ["entity_id", "entity_type"], name: "index_audit_events_on_entity_id_and_entity_type", using: :btree
-  add_index "audit_events", ["type"], name: "index_audit_events_on_type", using: :btree
 
   create_table "award_emoji", force: :cascade do |t|
     t.string   "name"
@@ -119,6 +119,14 @@ ActiveRecord::Schema.define(version: 20160726093600) 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.datetime "starts_at"
@@ -167,11 +175,12 @@ ActiveRecord::Schema.define(version: 20160726093600) do
     t.text     "artifacts_metadata"
     t.integer  "erased_by_id"
     t.datetime "erased_at"
-    t.string   "environment"
     t.datetime "artifacts_expire_at"
+    t.string   "environment"
     t.integer  "artifacts_size"
     t.string   "when"
     t.text     "yaml_variables"
+    t.datetime "queued_at"
   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
@@ -179,13 +188,10 @@ ActiveRecord::Schema.define(version: 20160726093600) do
   add_index "ci_builds", ["commit_id", "type", "name", "ref"], name: "index_ci_builds_on_commit_id_and_type_and_name_and_ref", using: :btree
   add_index "ci_builds", ["commit_id", "type", "ref"], name: "index_ci_builds_on_commit_id_and_type_and_ref", using: :btree
   add_index "ci_builds", ["commit_id"], name: "index_ci_builds_on_commit_id", using: :btree
-  add_index "ci_builds", ["erased_by_id"], name: "index_ci_builds_on_erased_by_id", using: :btree
   add_index "ci_builds", ["gl_project_id"], name: "index_ci_builds_on_gl_project_id", using: :btree
-  add_index "ci_builds", ["project_id", "commit_id"], name: "index_ci_builds_on_project_id_and_commit_id", using: :btree
   add_index "ci_builds", ["project_id"], name: "index_ci_builds_on_project_id", using: :btree
   add_index "ci_builds", ["runner_id"], name: "index_ci_builds_on_runner_id", using: :btree
   add_index "ci_builds", ["status"], name: "index_ci_builds_on_status", using: :btree
-  add_index "ci_builds", ["type"], name: "index_ci_builds_on_type", using: :btree
 
   create_table "ci_commits", force: :cascade do |t|
     t.integer  "project_id"
@@ -209,11 +215,6 @@ ActiveRecord::Schema.define(version: 20160726093600) do
   add_index "ci_commits", ["gl_project_id", "sha"], name: "index_ci_commits_on_gl_project_id_and_sha", using: :btree
   add_index "ci_commits", ["gl_project_id", "status"], name: "index_ci_commits_on_gl_project_id_and_status", using: :btree
   add_index "ci_commits", ["gl_project_id"], name: "index_ci_commits_on_gl_project_id", using: :btree
-  add_index "ci_commits", ["project_id", "committed_at", "id"], name: "index_ci_commits_on_project_id_and_committed_at_and_id", using: :btree
-  add_index "ci_commits", ["project_id", "committed_at"], name: "index_ci_commits_on_project_id_and_committed_at", using: :btree
-  add_index "ci_commits", ["project_id", "sha"], name: "index_ci_commits_on_project_id_and_sha", using: :btree
-  add_index "ci_commits", ["project_id"], name: "index_ci_commits_on_project_id", using: :btree
-  add_index "ci_commits", ["sha"], name: "index_ci_commits_on_sha", using: :btree
   add_index "ci_commits", ["status"], name: "index_ci_commits_on_status", using: :btree
   add_index "ci_commits", ["user_id"], name: "index_ci_commits_on_user_id", using: :btree
 
@@ -226,10 +227,6 @@ ActiveRecord::Schema.define(version: 20160726093600) do
     t.datetime "updated_at"
   end
 
-  add_index "ci_events", ["created_at"], name: "index_ci_events_on_created_at", using: :btree
-  add_index "ci_events", ["is_admin"], name: "index_ci_events_on_is_admin", using: :btree
-  add_index "ci_events", ["project_id"], name: "index_ci_events_on_project_id", using: :btree
-
   create_table "ci_jobs", force: :cascade do |t|
     t.integer  "project_id",                          null: false
     t.text     "commands"
@@ -244,9 +241,6 @@ ActiveRecord::Schema.define(version: 20160726093600) do
     t.datetime "deleted_at"
   end
 
-  add_index "ci_jobs", ["deleted_at"], name: "index_ci_jobs_on_deleted_at", using: :btree
-  add_index "ci_jobs", ["project_id"], name: "index_ci_jobs_on_project_id", using: :btree
-
   create_table "ci_projects", force: :cascade do |t|
     t.string   "name"
     t.integer  "timeout",                  default: 3600,  null: false
@@ -270,9 +264,6 @@ ActiveRecord::Schema.define(version: 20160726093600) do
     t.text     "generated_yaml_config"
   end
 
-  add_index "ci_projects", ["gitlab_id"], name: "index_ci_projects_on_gitlab_id", using: :btree
-  add_index "ci_projects", ["shared_runners_enabled"], name: "index_ci_projects_on_shared_runners_enabled", using: :btree
-
   create_table "ci_runner_projects", force: :cascade do |t|
     t.integer  "runner_id",     null: false
     t.integer  "project_id"
@@ -301,10 +292,8 @@ ActiveRecord::Schema.define(version: 20160726093600) do
     t.boolean  "locked",       default: false, null: false
   end
 
-  add_index "ci_runners", ["description"], name: "index_ci_runners_on_description_trigram", using: :gin, opclasses: {"description"=>"gin_trgm_ops"}
   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
-  add_index "ci_runners", ["token"], name: "index_ci_runners_on_token_trigram", using: :gin, opclasses: {"token"=>"gin_trgm_ops"}
 
   create_table "ci_services", force: :cascade do |t|
     t.string   "type"
@@ -316,8 +305,6 @@ ActiveRecord::Schema.define(version: 20160726093600) do
     t.text     "properties"
   end
 
-  add_index "ci_services", ["project_id"], name: "index_ci_services_on_project_id", using: :btree
-
   create_table "ci_sessions", force: :cascade do |t|
     t.string   "session_id", null: false
     t.text     "data"
@@ -325,9 +312,6 @@ ActiveRecord::Schema.define(version: 20160726093600) do
     t.datetime "updated_at"
   end
 
-  add_index "ci_sessions", ["session_id"], name: "index_ci_sessions_on_session_id", using: :btree
-  add_index "ci_sessions", ["updated_at"], name: "index_ci_sessions_on_updated_at", using: :btree
-
   create_table "ci_taggings", force: :cascade do |t|
     t.integer  "tag_id"
     t.integer  "taggable_id"
@@ -338,7 +322,6 @@ ActiveRecord::Schema.define(version: 20160726093600) do
     t.datetime "created_at"
   end
 
-  add_index "ci_taggings", ["tag_id", "taggable_id", "taggable_type", "context", "tagger_id", "tagger_type"], name: "ci_taggings_idx", unique: true, using: :btree
   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|
@@ -346,8 +329,6 @@ ActiveRecord::Schema.define(version: 20160726093600) do
     t.integer "taggings_count", default: 0
   end
 
-  add_index "ci_tags", ["name"], name: "index_ci_tags_on_name", unique: true, using: :btree
-
   create_table "ci_trigger_requests", force: :cascade do |t|
     t.integer  "trigger_id", null: false
     t.text     "variables"
@@ -365,7 +346,6 @@ ActiveRecord::Schema.define(version: 20160726093600) do
     t.integer  "gl_project_id"
   end
 
-  add_index "ci_triggers", ["deleted_at"], name: "index_ci_triggers_on_deleted_at", using: :btree
   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|
@@ -471,7 +451,6 @@ ActiveRecord::Schema.define(version: 20160726093600) do
     t.datetime "updated_at"
   end
 
-  add_index "identities", ["created_at", "id"], name: "index_identities_on_created_at_and_id", using: :btree
   add_index "identities", ["user_id"], name: "index_identities_on_user_id", using: :btree
 
   create_table "issues", force: :cascade do |t|
@@ -492,21 +471,19 @@ ActiveRecord::Schema.define(version: 20160726093600) do
     t.datetime "deleted_at"
     t.date     "due_date"
     t.integer  "moved_to_id"
+    t.integer  "lock_version"
   end
 
   add_index "issues", ["assignee_id"], name: "index_issues_on_assignee_id", using: :btree
   add_index "issues", ["author_id"], name: "index_issues_on_author_id", using: :btree
   add_index "issues", ["confidential"], name: "index_issues_on_confidential", using: :btree
-  add_index "issues", ["created_at", "id"], name: "index_issues_on_created_at_and_id", using: :btree
   add_index "issues", ["created_at"], name: "index_issues_on_created_at", using: :btree
   add_index "issues", ["deleted_at"], name: "index_issues_on_deleted_at", using: :btree
   add_index "issues", ["description"], name: "index_issues_on_description_trigram", using: :gin, opclasses: {"description"=>"gin_trgm_ops"}
   add_index "issues", ["due_date"], name: "index_issues_on_due_date", using: :btree
   add_index "issues", ["milestone_id"], name: "index_issues_on_milestone_id", using: :btree
   add_index "issues", ["project_id", "iid"], name: "index_issues_on_project_id_and_iid", unique: true, using: :btree
-  add_index "issues", ["project_id"], name: "index_issues_on_project_id", using: :btree
   add_index "issues", ["state"], name: "index_issues_on_state", using: :btree
-  add_index "issues", ["title"], name: "index_issues_on_title", using: :btree
   add_index "issues", ["title"], name: "index_issues_on_title_trigram", using: :gin, opclasses: {"title"=>"gin_trgm_ops"}
 
   create_table "keys", force: :cascade do |t|
@@ -520,7 +497,6 @@ ActiveRecord::Schema.define(version: 20160726093600) do
     t.boolean  "public",      default: false, null: false
   end
 
-  add_index "keys", ["created_at", "id"], name: "index_keys_on_created_at_and_id", using: :btree
   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
 
@@ -568,6 +544,19 @@ ActiveRecord::Schema.define(version: 20160726093600) do
 
   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
@@ -582,14 +571,13 @@ ActiveRecord::Schema.define(version: 20160726093600) do
     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
-  add_index "members", ["created_at", "id"], name: "index_members_on_created_at_and_id", using: :btree
   add_index "members", ["invite_token"], name: "index_members_on_invite_token", unique: true, using: :btree
   add_index "members", ["requested_at"], name: "index_members_on_requested_at", using: :btree
   add_index "members", ["source_id", "source_type"], name: "index_members_on_source_id_and_source_type", using: :btree
-  add_index "members", ["type"], name: "index_members_on_type", using: :btree
   add_index "members", ["user_id"], name: "index_members_on_user_id", using: :btree
 
   create_table "merge_request_diffs", force: :cascade do |t|
@@ -625,18 +613,18 @@ ActiveRecord::Schema.define(version: 20160726093600) do
     t.integer  "position",                     default: 0
     t.datetime "locked_at"
     t.integer  "updated_by_id"
-    t.string   "merge_error"
+    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.integer  "lock_version"
   end
 
   add_index "merge_requests", ["assignee_id"], name: "index_merge_requests_on_assignee_id", using: :btree
   add_index "merge_requests", ["author_id"], name: "index_merge_requests_on_author_id", using: :btree
-  add_index "merge_requests", ["created_at", "id"], name: "index_merge_requests_on_created_at_and_id", using: :btree
   add_index "merge_requests", ["created_at"], name: "index_merge_requests_on_created_at", using: :btree
   add_index "merge_requests", ["deleted_at"], name: "index_merge_requests_on_deleted_at", using: :btree
   add_index "merge_requests", ["description"], name: "index_merge_requests_on_description_trigram", using: :gin, opclasses: {"description"=>"gin_trgm_ops"}
@@ -659,7 +647,6 @@ ActiveRecord::Schema.define(version: 20160726093600) do
     t.integer  "iid"
   end
 
-  add_index "milestones", ["created_at", "id"], name: "index_milestones_on_created_at_and_id", using: :btree
   add_index "milestones", ["description"], name: "index_milestones_on_description_trigram", using: :gin, opclasses: {"description"=>"gin_trgm_ops"}
   add_index "milestones", ["due_date"], name: "index_milestones_on_due_date", using: :btree
   add_index "milestones", ["project_id", "iid"], name: "index_milestones_on_project_id_and_iid", unique: true, using: :btree
@@ -679,16 +666,17 @@ ActiveRecord::Schema.define(version: 20160726093600) do
     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"
   end
 
-  add_index "namespaces", ["created_at", "id"], name: "index_namespaces_on_created_at_and_id", using: :btree
+  add_index "namespaces", ["created_at"], name: "index_namespaces_on_created_at", using: :btree
+  add_index "namespaces", ["deleted_at"], name: "index_namespaces_on_deleted_at", using: :btree
   add_index "namespaces", ["name"], name: "index_namespaces_on_name", unique: true, using: :btree
   add_index "namespaces", ["name"], name: "index_namespaces_on_name_trigram", using: :gin, opclasses: {"name"=>"gin_trgm_ops"}
   add_index "namespaces", ["owner_id"], name: "index_namespaces_on_owner_id", using: :btree
   add_index "namespaces", ["path"], name: "index_namespaces_on_path", unique: true, using: :btree
   add_index "namespaces", ["path"], name: "index_namespaces_on_path_trigram", using: :gin, opclasses: {"path"=>"gin_trgm_ops"}
   add_index "namespaces", ["type"], name: "index_namespaces_on_type", using: :btree
-  add_index "namespaces", ["visibility_level"], name: "index_namespaces_on_visibility_level", using: :btree
 
   create_table "notes", force: :cascade do |t|
     t.text     "note"
@@ -701,18 +689,22 @@ ActiveRecord::Schema.define(version: 20160726093600) do
     t.string   "line_code"
     t.string   "commit_id"
     t.integer  "noteable_id"
-    t.boolean  "system",            default: false, null: false
+    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"
   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", "id"], name: "index_notes_on_created_at_and_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
@@ -782,10 +774,10 @@ ActiveRecord::Schema.define(version: 20160726093600) do
     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.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
@@ -797,6 +789,7 @@ ActiveRecord::Schema.define(version: 20160726093600) do
     t.datetime "created_at"
     t.datetime "updated_at"
     t.integer  "group_access", default: 30, null: false
+    t.date     "expires_at"
   end
 
   create_table "project_import_data", force: :cascade do |t|
@@ -847,14 +840,12 @@ ActiveRecord::Schema.define(version: 20160726093600) do
     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  "has_external_wiki"
   end
 
-  add_index "projects", ["builds_enabled", "shared_runners_enabled"], name: "index_projects_on_builds_enabled_and_shared_runners_enabled", using: :btree
-  add_index "projects", ["builds_enabled"], name: "index_projects_on_builds_enabled", using: :btree
   add_index "projects", ["ci_id"], name: "index_projects_on_ci_id", using: :btree
-  add_index "projects", ["created_at", "id"], name: "index_projects_on_created_at_and_id", using: :btree
+  add_index "projects", ["created_at"], name: "index_projects_on_created_at", using: :btree
   add_index "projects", ["creator_id"], name: "index_projects_on_creator_id", using: :btree
   add_index "projects", ["description"], name: "index_projects_on_description_trigram", using: :gin, opclasses: {"description"=>"gin_trgm_ops"}
   add_index "projects", ["last_activity_at"], name: "index_projects_on_last_activity_at", using: :btree
@@ -938,11 +929,9 @@ ActiveRecord::Schema.define(version: 20160726093600) do
     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
   end
 
-  add_index "services", ["category"], name: "index_services_on_category", using: :btree
-  add_index "services", ["created_at", "id"], name: "index_services_on_created_at_and_id", using: :btree
-  add_index "services", ["default"], name: "index_services_on_default", using: :btree
   add_index "services", ["project_id"], name: "index_services_on_project_id", using: :btree
   add_index "services", ["template"], name: "index_services_on_template", using: :btree
 
@@ -959,8 +948,6 @@ ActiveRecord::Schema.define(version: 20160726093600) do
   end
 
   add_index "snippets", ["author_id"], name: "index_snippets_on_author_id", using: :btree
-  add_index "snippets", ["created_at", "id"], name: "index_snippets_on_created_at_and_id", using: :btree
-  add_index "snippets", ["created_at"], name: "index_snippets_on_created_at", using: :btree
   add_index "snippets", ["file_name"], name: "index_snippets_on_file_name_trigram", using: :gin, opclasses: {"file_name"=>"gin_trgm_ops"}
   add_index "snippets", ["project_id"], name: "index_snippets_on_project_id", using: :btree
   add_index "snippets", ["title"], name: "index_snippets_on_title_trigram", using: :gin, opclasses: {"title"=>"gin_trgm_ops"}
@@ -972,12 +959,12 @@ ActiveRecord::Schema.define(version: 20160726093600) do
     t.string   "source_ip"
     t.string   "user_agent"
     t.boolean  "via_api"
-    t.integer  "project_id"
     t.string   "noteable_type"
     t.string   "title"
     t.text     "description"
-    t.datetime "created_at",    null: false
-    t.datetime "updated_at",    null: false
+    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|
@@ -1029,7 +1016,6 @@ ActiveRecord::Schema.define(version: 20160726093600) do
   add_index "todos", ["commit_id"], name: "index_todos_on_commit_id", using: :btree
   add_index "todos", ["note_id"], name: "index_todos_on_note_id", using: :btree
   add_index "todos", ["project_id"], name: "index_todos_on_project_id", using: :btree
-  add_index "todos", ["state"], name: "index_todos_on_state", using: :btree
   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
 
@@ -1041,11 +1027,22 @@ ActiveRecord::Schema.define(version: 20160726093600) do
     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
+  end
+
   create_table "users", force: :cascade do |t|
     t.string   "email",                       default: "",    null: false
     t.string   "encrypted_password",          default: "",    null: false
@@ -1109,7 +1106,7 @@ ActiveRecord::Schema.define(version: 20160726093600) do
   add_index "users", ["admin"], name: "index_users_on_admin", using: :btree
   add_index "users", ["authentication_token"], name: "index_users_on_authentication_token", unique: true, using: :btree
   add_index "users", ["confirmation_token"], name: "index_users_on_confirmation_token", unique: true, using: :btree
-  add_index "users", ["created_at", "id"], name: "index_users_on_created_at_and_id", using: :btree
+  add_index "users", ["created_at"], name: "index_users_on_created_at", using: :btree
   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"}
@@ -1147,11 +1144,14 @@ ActiveRecord::Schema.define(version: 20160726093600) do
     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
   end
 
-  add_index "web_hooks", ["created_at", "id"], name: "index_web_hooks_on_created_at_and_id", using: :btree
   add_index "web_hooks", ["project_id"], name: "index_web_hooks_on_project_id", using: :btree
 
+  add_foreign_key "boards", "projects"
+  add_foreign_key "lists", "boards"
+  add_foreign_key "lists", "labels"
   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"
diff --git a/doc/README.md b/doc/README.md
index b5b377822e6ededbe34ee9c7d7d98aecb3bef884..195b7c7af62c7f915791af7c261a30ab3fe02a95 100644
--- a/doc/README.md
+++ b/doc/README.md
@@ -9,7 +9,7 @@
 - [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](markdown/markdown.md) GitLab's advanced formatting system.
+- [Markdown](user/markdown.md) GitLab's advanced formatting system.
 - [Migrating from SVN](workflow/importing/migrating_from_svn.md) Convert a SVN repository to Git and GitLab.
 - [Permissions](user/permissions.md) Learn what each role in a project (external/guest/reporter/developer/master/owner) can do.
 - [Profile Settings](profile/README.md)
@@ -18,6 +18,7 @@
 - [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.
+- [Koding](user/project/koding.md) Learn how to use Koding, the online IDE.
 
 ## Administrator documentation
 
@@ -29,6 +30,7 @@
 - [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.
+- [Koding](administration/integration/koding.md) Set up Koding to use with GitLab.
 - [Libravatar](customization/libravatar.md) Use Libravatar for user avatars.
 - [Log system](administration/logs.md) Log system.
 - [Environment Variables](administration/environment_variables.md) to configure GitLab.
@@ -54,7 +56,5 @@
 
 ## Contributor documentation
 
-- [Documentation styleguide](development/doc_styleguide.md) Use this styleguide if you are
-  contributing to documentation.
-- [Development](development/README.md) Explains the architecture and the guidelines for shell commands.
+- [Development](development/README.md) All styleguides and explanations how to contribute.
 - [Legal](legal/README.md) Contributor license agreements.
diff --git a/doc/administration/build_artifacts.md b/doc/administration/build_artifacts.md
new file mode 100644
index 0000000000000000000000000000000000000000..64353f7282b2b3b14613eb61a721b26382e961c8
--- /dev/null
+++ b/doc/administration/build_artifacts.md
@@ -0,0 +1,90 @@
+# Build artifacts administration
+
+>**Notes:**
+>- Introduced in GitLab 8.2 and GitLab Runner 0.7.0.
+>- Starting from GitLab 8.4 and GitLab Runner 1.0, the artifacts archive format
+   changed to `ZIP`.
+>- This is the administration documentation. For the user guide see
+   [user/project/builds/artifacts.md](../user/project/builds/artifacts.md).
+
+Artifacts is a list of files and directories which are attached to a build
+after it completes successfully. This feature is enabled by default in all
+GitLab installations. Keep reading if you want to know how to disable it.
+
+## Disabling build artifacts
+
+To disable artifacts site-wide, follow the steps below.
+
+---
+
+**In Omnibus installations:**
+
+1. Edit `/etc/gitlab/gitlab.rb` and add the following line:
+
+    ```ruby
+    gitlab_rails['artifacts_enabled'] = false
+    ```
+
+1. Save the file and [reconfigure GitLab][] for the changes to take effect.
+
+---
+
+**In installations from source:**
+
+1. Edit `/home/git/gitlab/config/gitlab.yml` and add or amend the following lines:
+
+    ```yaml
+    artifacts:
+      enabled: false
+    ```
+
+1. Save the file and [restart GitLab][] for the changes to take effect.
+
+## Storing build artifacts
+
+After a successful build, GitLab Runner uploads an archive containing the build
+artifacts to GitLab.
+
+To change the location where the artifacts are stored, follow the steps below.
+
+---
+
+**In Omnibus installations:**
+
+_The artifacts are stored by default in
+`/var/opt/gitlab/gitlab-rails/shared/artifacts`._
+
+1. To change the storage path for example to `/mnt/storage/artifacts`, edit
+   `/etc/gitlab/gitlab.rb` and add the following line:
+
+    ```ruby
+    gitlab_rails['artifacts_path'] = "/mnt/storage/artifacts"
+    ```
+
+1. Save the file and [reconfigure GitLab][] for the changes to take effect.
+
+---
+
+**In installations from source:**
+
+_The artifacts are stored by default in
+`/home/git/gitlab/shared/artifacts`._
+
+1. To change the storage path for example to `/mnt/storage/artifacts`, edit
+   `/home/git/gitlab/config/gitlab.yml` and add or amend the following lines:
+
+    ```yaml
+    artifacts:
+      enabled: true
+      path: /mnt/storage/artifacts
+    ```
+
+1. Save the file and [restart GitLab][] for the changes to take effect.
+
+## Set the maximum file size of the artifacts
+
+Provided the artifacts are enabled, you can change the maximum file size of the
+artifacts through the [Admin area settings](../user/admin_area/settings/continuous_integration#maximum-artifacts-size).
+
+[reconfigure gitlab]: restart_gitlab.md "How to restart GitLab"
+[restart gitlab]: restart_gitlab.md "How to restart GitLab"
diff --git a/doc/administration/container_registry.md b/doc/administration/container_registry.md
index d5d433034546fe77676447cc0ceee6af1ef10605..28c4c7c86ca4c1fe543c7b973b0205769155353f 100644
--- a/doc/administration/container_registry.md
+++ b/doc/administration/container_registry.md
@@ -1,7 +1,6 @@
 # GitLab Container Registry Administration
 
-> **Note:**
-This feature was [introduced][ce-4040] in GitLab 8.8.
+> [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.
@@ -122,6 +121,10 @@ Registry is exposed to the outside world is `4567`, here is what you need to set
 in `gitlab.rb` or `gitlab.yml` if you are using Omnibus GitLab or installed
 GitLab from source respectively.
 
+>**Note:**
+Be careful to choose a port different than the one that Registry listens to (`5000` by default),
+otherwise you will run into conflicts .
+
 ---
 
 **Omnibus GitLab installations**
diff --git a/doc/administration/custom_hooks.md b/doc/administration/custom_hooks.md
index e3306c22d3f25d06a8a2ca56a4c256ca85d544e3..0387d730489256962d1a7c3fe448cd8a15545ff9 100644
--- a/doc/administration/custom_hooks.md
+++ b/doc/administration/custom_hooks.md
@@ -44,8 +44,7 @@ as appropriate.
 
 ## Custom error messages
 
->**Note:**
-This feature was [introduced][5073] in GitLab 8.10.
+> [Introduced][5073] in GitLab 8.10.
 
 If the commit is declined or an error occurs during the Git hook check,
 the STDERR or STDOUT message of the hook will be present in GitLab's UI.
diff --git a/doc/administration/high_availability/redis.md b/doc/administration/high_availability/redis.md
index f6153216f333a9a4d0c573415659d28084e468fb..bc42433065609965a4e3c73db559544afd00bc9d 100644
--- a/doc/administration/high_availability/redis.md
+++ b/doc/administration/high_availability/redis.md
@@ -1,7 +1,12 @@
 # Configuring Redis for GitLab HA
 
-You can choose to install and manage Redis yourself, or you can use GitLab
-Omnibus packages to help.
+You can choose to install and manage Redis yourself, or you can use the one
+that comes bundled with GitLab Omnibus packages.
+
+> **Note:** Redis does not require authentication by default. See
+  [Redis Security](http://redis.io/topics/security) documentation for more
+  information. We recommend using a combination of a Redis password and tight
+  firewall rules to secure your Redis service.
 
 ## Configure your own Redis server
 
@@ -9,49 +14,293 @@ If you're hosting GitLab on a cloud provider, you can optionally use a
 managed service for Redis. For example, AWS offers a managed ElastiCache service
 that runs Redis.
 
-> **Note:** Redis does not require authentication by default. See
-  [Redis Security](http://redis.io/topics/security) documentation for more
-  information. We recommend using a combination of a Redis password and tight
-  firewall rules to secure your Redis service.
+## Configure Redis using Omnibus
 
-## Configure using Omnibus
+If you don't want to bother setting up your own Redis server, you can use the
+one bundled with Omnibus. In this case, you should disable all services except
+Redis.
 
 1. Download/install GitLab Omnibus using **steps 1 and 2** from
    [GitLab downloads](https://about.gitlab.com/downloads). Do not complete other
    steps on the download page.
 1. Create/edit `/etc/gitlab/gitlab.rb` and use the following configuration.
    Be sure to change the `external_url` to match your eventual GitLab front-end
-   URL.
+   URL:
 
     ```ruby
-      external_url 'https://gitlab.example.com'
+    external_url 'https://gitlab.example.com'
 
-      # Disable all components except Redis
-      redis['enable'] = true
-      bootstrap['enable'] = false
-      nginx['enable'] = false
-      unicorn['enable'] = false
-      sidekiq['enable'] = false
-      postgresql['enable'] = false
-      gitlab_workhorse['enable'] = false
-      mailroom['enable'] = false
+    # Disable all services except Redis
+    redis['enable'] = true
+    bootstrap['enable'] = false
+    nginx['enable'] = false
+    unicorn['enable'] = false
+    sidekiq['enable'] = false
+    postgresql['enable'] = false
+    gitlab_workhorse['enable'] = false
+    mailroom['enable'] = false
 
-      # Redis configuration
-      redis['port'] = 6379
-      redis['bind'] = '0.0.0.0'
+    # Redis configuration
+    redis['port'] = 6379
+    redis['bind'] = '0.0.0.0'
 
-      # If you wish to use Redis authentication (recommended)
-      redis['password'] = 'Redis Password'
+    # If you wish to use Redis authentication (recommended)
+    redis['password'] = 'Redis Password'
     ```
 
 1. Run `sudo gitlab-ctl reconfigure` to install and configure PostgreSQL.
 
     > **Note**: This `reconfigure` step will result in some errors.
       That's OK - don't be alarmed.
+
 1. Run `touch /etc/gitlab/skip-auto-migrations` to prevent database migrations
    from running on upgrade. Only the primary GitLab application server should
    handle migrations.
 
+## Experimental Redis Sentinel support
+
+> [Introduced][ce-1877] in GitLab 8.11.
+
+Since GitLab 8.11, you can configure a list of Redis Sentinel servers that
+will monitor a group of Redis servers to provide you with a standard failover
+support.
+
+There is currently one exception to the Sentinel support: `mail_room`, the
+component that processes incoming emails. It doesn't support Sentinel yet, but
+we hope to integrate a future release that does support it.
+
+To get a better understanding on how to correctly setup Sentinel, please read
+the [Redis Sentinel documentation](http://redis.io/topics/sentinel) first, as
+failing to configure it correctly can lead to data loss.
+
+The configuration consists of three parts:
+
+- Redis setup
+- Sentinel setup
+- GitLab setup
+
+Read carefully how to configure those components below.
+
+### Redis setup
+
+You must have at least 2 Redis servers: 1 Master, 1 or more Slaves.
+They should be configured the same way and with similar server specs, as
+in a failover situation, any Slave can be elected as the new Master by
+the Sentinel servers.
+
+In a minimal setup, the only required change for the slaves in `redis.conf`
+is the addition of a `slaveof` line pointing to the initial master.
+You can increase the security by defining a `requirepass` configuration in
+the master, and `masterauth` in slaves.
+
+---
+
+**Configuring your own Redis server**
+
+1. Add to the slaves' `redis.conf`:
+
+    ```conf
+    # IP and port of the master Redis server
+    slaveof 10.10.10.10 6379
+    ```
+
+1. Optionally, set up password authentication for increased security.
+   Add the following to master's `redis.conf`:
+
+    ```conf
+    # Optional password authentication for increased security
+    requirepass "<password>"
+    ```
+
+1. Then add this line to all the slave servers' `redis.conf`:
+
+    ```conf
+    masterauth "<password>"
+    ```
+
+1. Restart the Redis services for the changes to take effect.
+
+---
+
+**Using Redis via Omnibus**
+
+1. Edit `/etc/gitlab/gitlab.rb` of a master Redis machine (usualy a single machine):
+
+    ```ruby
+    ## Redis TCP support (will disable UNIX socket transport)
+    redis['bind'] = '0.0.0.0' # or specify an IP to bind to a single one
+    redis['port'] = 6379
+
+    ## Master redis instance
+    redis['password'] = '<huge password string here>'
+    ```
+
+1. Edit `/etc/gitlab/gitlab.rb` of a slave Redis machine (should be one or more machines):
+
+    ```ruby
+    ## Redis TCP support (will disable UNIX socket transport)
+    redis['bind'] = '0.0.0.0' # or specify an IP to bind to a single one
+    redis['port'] = 6379
+
+    ## Slave redis instance
+    redis['master_ip'] = '10.10.10.10' # IP of master Redis server
+    redis['master_port'] = 6379 # Port of master Redis server
+    redis['master_password'] = "<huge password string here>"
+    ```
+
+1. Reconfigure the GitLab for the changes to take effect: `sudo gitlab-ctl reconfigure`
+
+---
+
+Now that the Redis servers are all set up, let's configure the Sentinel
+servers.
+
+### Sentinel setup
+
+We don't provide yet an automated way to setup and run the Sentinel daemon
+from Omnibus installation method. You must follow the instructions below and
+run it by yourself.
+
+The support for Sentinel in Ruby has some [caveats](https://github.com/redis/redis-rb/issues/531).
+While you can give any name for the `master-group-name` part of the
+configuration, as in this example:
+
+```conf
+sentinel monitor <master-group-name> <ip> <port> <quorum>
+```
+
+,for it to work in Ruby, you have to use the "hostname" of the master Redis
+server, otherwise you will get an error message like:
+`Redis::CannotConnectError: No sentinels available.`. Read
+[Sentinel troubleshooting](#sentinel-troubleshooting) for more information.
+
+Here is an example configuration file (`sentinel.conf`) for a Sentinel node:
+
+```conf
+port 26379
+sentinel monitor master-redis.example.com 10.10.10.10 6379 1
+sentinel down-after-milliseconds master-redis.example.com 10000
+sentinel config-epoch master-redis.example.com 0
+sentinel leader-epoch master-redis.example.com 0
+```
+
+---
+
+The final part is to inform the main GitLab application server of the Redis
+master and the new sentinels servers.
+
+### GitLab setup
+
+You can enable or disable sentinel support at any time in new or existing
+installations. From the GitLab application perspective, all it requires is
+the correct credentials for the master Redis and for a few Sentinel nodes.
+
+It doesn't require a list of all Sentinel nodes, as in case of a failure,
+the application will need to query only one of them.
+
+>**Note:**
+The following steps should be performed in the [GitLab application server](gitlab.md).
+
+**For source based installations**
+
+1. Edit `/home/git/gitlab/config/resque.yml` following the example in
+   `/home/git/gitlab/config/resque.yml.example`, and uncomment the sentinels
+   line, changing to the correct server credentials.
+1. Restart GitLab for the changes to take effect.
+
+**For Omnibus installations**
+
+1. Edit `/etc/gitlab/gitlab.rb` and add/change the following lines:
+
+    ```ruby
+    gitlab-rails['redis_host'] = "master-redis.example.com"
+    gitlab-rails['redis_port'] = 6379
+    gitlab-rails['redis_password'] = '<huge password string here>'
+    gitlab-rails['redis_sentinels'] = [
+      {'host' => '10.10.10.1', 'port' => 26379},
+      {'host' => '10.10.10.2', 'port' => 26379},
+      {'host' => '10.10.10.3', 'port' => 26379}
+    ]
+    ```
+
+1. [Reconfigure] the GitLab for the changes to take effect.
+
+### Sentinel troubleshooting
+
+If you get an error like: `Redis::CannotConnectError: No sentinels available.`,
+there may be something wrong with your configuration files or it can be related
+to [this issue][gh-531] ([pull request][gh-534] that should make things better).
+
+It's a bit rigid the way you have to config `resque.yml` and `sentinel.conf`,
+otherwise `redis-rb` will not work properly.
+
+The hostname ('my-primary-redis') of the primary Redis server (`sentinel.conf`)
+**must** match the one configured in GitLab (`resque.yml` for source installations
+or `gitlab-rails['redis_*']` in Omnibus) and it must be valid ex:
+
+```conf
+# sentinel.conf:
+sentinel monitor my-primary-redis 10.10.10.10 6379 1
+sentinel down-after-milliseconds my-primary-redis 10000
+sentinel config-epoch my-primary-redis 0
+sentinel leader-epoch my-primary-redis 0
+```
+
+```yaml
+# resque.yaml
+production:
+  url: redis://my-primary-redis:6378
+  sentinels:
+    -
+      host: slave1
+      port: 26380 # point to sentinel, not to redis port
+    -
+      host: slave2
+      port: 26381 # point to sentinel, not to redis port
+```
+
+When in doubt, please read [Redis Sentinel documentation](http://redis.io/topics/sentinel)
+
+---
+
+To make sure your configuration is correct:
+
+1. SSH into your GitLab application server
+1. Enter the Rails console:
+
+    ```
+    # For Omnibus installations
+    sudo gitlab-rails console
+
+    # For source installations
+    sudo -u git rails console RAILS_ENV=production
+    ```
+
+1. Run in the console:
+
+    ```ruby
+    redis = Redis.new(Gitlab::Redis.params)
+    redis.info
+    ```
+
+    Keep this screen open and try to simulate a failover below.
+
+1. To simulate a failover on master Redis, SSH into the Redis server and run:
+
+    ```bash
+    # port must match your master redis port
+     redis-cli -h localhost -p 6379 DEBUG sleep 60
+    ```
+
+1. Then back in the Rails console from the first step, run:
+
+    ```
+    redis.info
+    ```
+
+    You should see a different port after a few seconds delay
+    (the failover/reconnect time).
+
 ---
 
 Read more on high-availability configuration:
@@ -60,3 +309,9 @@ Read more on high-availability configuration:
 1. [Configure NFS](nfs.md)
 1. [Configure the GitLab application servers](gitlab.md)
 1. [Configure the load balancers](load_balancer.md)
+
+[ce-1877]: https://gitlab.com/gitlab-org/gitlab-ce/merge_requests/1877
+[restart]: ../restart_gitlab.md#installations-from-source
+[reconfigure]: ../restart_gitlab.md#omnibus-gitlab-reconfigure
+[gh-531]: https://github.com/redis/redis-rb/issues/531
+[gh-534]: https://github.com/redis/redis-rb/issues/534
diff --git a/doc/administration/housekeeping.md b/doc/administration/housekeeping.md
index a5fa7d358a2ab55094461f08b2599e185549c82d..34b4f1faa94adfc9e3cb1b4b75cc148590d0113c 100644
--- a/doc/administration/housekeeping.md
+++ b/doc/administration/housekeeping.md
@@ -1,6 +1,6 @@
 # Housekeeping
 
-_**Note:** This feature was [introduced][ce-2371] in GitLab 8.4_
+> [Introduced][ce-2371] in GitLab 8.4.
 
 ---
 
diff --git a/doc/administration/integration/koding.md b/doc/administration/integration/koding.md
new file mode 100644
index 0000000000000000000000000000000000000000..a2c358af095254b5a31c49c93cb2903fbc7f6050
--- /dev/null
+++ b/doc/administration/integration/koding.md
@@ -0,0 +1,242 @@
+# 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 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/raketasks/project_import_export.md b/doc/administration/raketasks/project_import_export.md
index c212059b9d5a511dd2db3157b7a25835d1e82089..39b1883375e9055d54dc99dfd7446c1bc73126dc 100644
--- a/doc/administration/raketasks/project_import_export.md
+++ b/doc/administration/raketasks/project_import_export.md
@@ -1,13 +1,14 @@
 # Project import/export
 
 >**Note:**
-  - This feature was [introduced][ce-3050] in GitLab 8.9
-  - Importing will not be possible if the import instance version is lower
-    than that of the exporter.
-  - For existing installations, the project import option has to be enabled in
-    application settings (`/admin/application_settings`) under 'Import sources'.
-  - The exports are stored in a temporary [shared directory][tmp] and are deleted
-    every 24 hours by a specific worker.
+>
+>  - [Introduced][ce-3050] in GitLab 8.9.
+>  - Importing will not be possible if the import instance version is lower
+>    than that of the exporter.
+>  - For existing installations, the project import option has to be enabled in
+>    application settings (`/admin/application_settings`) under 'Import sources'.
+>  - The exports are stored in a temporary [shared directory][tmp] and are deleted
+>    every 24 hours by a specific worker.
 
 The GitLab Import/Export version can be checked by using:
 
diff --git a/doc/administration/repository_checks.md b/doc/administration/repository_checks.md
index 4172b604cec81ebcc3098f152c8371c03ec98611..bc2b1f20ed30de3beaddcf8ccbd8c2c432c4b23e 100644
--- a/doc/administration/repository_checks.md
+++ b/doc/administration/repository_checks.md
@@ -1,8 +1,7 @@
 # Repository checks
 
->**Note:**
-This feature was [introduced][ce-3232] in GitLab 8.7. It is OFF by
-default because it still causes too many false alarms.
+> [Introduced][ce-3232] in GitLab 8.7. It is OFF by default because it still
+causes too many false alarms.
 
 Git has a built-in mechanism, [git fsck][git-fsck], to verify the
 integrity of all data committed to a repository. GitLab administrators
diff --git a/doc/api/README.md b/doc/api/README.md
index d1e6c54c521f55cb99bcc646a90160870dd9f138..3e79cce0120115522ce604b45867a95d13aeb2a1 100644
--- a/doc/api/README.md
+++ b/doc/api/README.md
@@ -16,6 +16,8 @@ following locations:
 - [Commits](commits.md)
 - [Deploy Keys](deploy_keys.md)
 - [Groups](groups.md)
+- [Group Access Requests](access_requests.md)
+- [Group Members](members.md)
 - [Issues](issues.md)
 - [Keys](keys.md)
 - [Labels](labels.md)
@@ -24,7 +26,10 @@ following locations:
 - [Open source license templates](licenses.md)
 - [Namespaces](namespaces.md)
 - [Notes](notes.md) (comments)
+- [Pipelines](pipelines.md)
 - [Projects](projects.md) including setting Webhooks
+- [Project Access Requests](access_requests.md)
+- [Project Members](members.md)
 - [Project Snippets](project_snippets.md)
 - [Repositories](repositories.md)
 - [Repository Files](repository_files.md)
@@ -74,14 +79,14 @@ You can use an OAuth 2 token to authenticate with the API by passing it either i
 Example of using the OAuth2 token in the header:
 
 ```shell
-curl -H "Authorization: Bearer OAUTH-TOKEN" https://gitlab.example.com/api/v3/projects
+curl --header "Authorization: Bearer OAUTH-TOKEN" https://gitlab.example.com/api/v3/projects
 ```
 
 Read more about [GitLab as an OAuth2 client](oauth2.md).
 
 ### Personal Access Tokens
 
-> **Note:** This feature was [introduced][ce-3749] in GitLab 8.8
+> [Introduced][ce-3749] in GitLab 8.8.
 
 You can create as many personal access tokens as you like from your GitLab
 profile (`/profile/personal_access_tokens`); perhaps one for each application
@@ -154,7 +159,7 @@ be returned with status code `403`:
 
 ```json
 {
-  "message": "403 Forbidden: Must be admin to use sudo"
+  "message": "403 Forbidden - Must be admin to use sudo"
 }
 ```
 
@@ -204,7 +209,7 @@ resources you can pass the following parameters:
 In the example below, we list 50 [namespaces](namespaces.md) per page.
 
 ```bash
-curl -X PUT -H "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" "https://gitlab.example.com/api/v3/namespaces?per_page=50
+curl --request PUT --header "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" "https://gitlab.example.com/api/v3/namespaces?per_page=50
 ```
 
 ### Pagination Link header
@@ -218,7 +223,7 @@ and we request the second page (`page=2`) of [comments](notes.md) of the issue
 with ID `8` which belongs to the project with ID `8`:
 
 ```bash
-curl -I -H "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" https://gitlab.example.com/api/v3/projects/8/issues/8/notes?per_page=3&page=2
+curl --head --header "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" https://gitlab.example.com/api/v3/projects/8/issues/8/notes?per_page=3&page=2
 ```
 
 The response will then be:
diff --git a/doc/api/access_requests.md b/doc/api/access_requests.md
new file mode 100644
index 0000000000000000000000000000000000000000..ea308b54d62d6d79a996b4f254fb23b02c3b6eec
--- /dev/null
+++ b/doc/api/access_requests.md
@@ -0,0 +1,147 @@
+# Group and project access requests
+
+ >**Note:** This feature was introduced in GitLab 8.11
+
+ **Valid access levels**
+
+ The access levels are defined in the `Gitlab::Access` module. Currently, these levels are recognized:
+
+```
+10 => Guest access
+20 => Reporter access
+30 => Developer access
+40 => Master access
+50 => Owner access # Only valid for groups
+```
+
+## List access requests for a group or project
+
+Gets a list of access requests viewable by the authenticated user.
+
+Returns `200` if the request succeeds.
+
+```
+GET /groups/:id/access_requests
+GET /projects/:id/access_requests
+```
+
+| Attribute | Type | Required | Description |
+| --------- | ---- | -------- | ----------- |
+| `id`      | integer/string | yes | The group/project ID or path |
+
+```bash
+curl --header "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" https://gitlab.example.com/api/v3/groups/:id/access_requests
+curl --header "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" https://gitlab.example.com/api/v3/projects/:id/access_requests
+```
+
+Example response:
+
+```json
+[
+ {
+   "id": 1,
+   "username": "raymond_smith",
+   "name": "Raymond Smith",
+   "state": "active",
+   "created_at": "2012-10-22T14:13:35Z",
+   "requested_at": "2012-10-22T14:13:35Z"
+ },
+ {
+   "id": 2,
+   "username": "john_doe",
+   "name": "John Doe",
+   "state": "active",
+   "created_at": "2012-10-22T14:13:35Z",
+   "requested_at": "2012-10-22T14:13:35Z"
+ }
+]
+```
+
+## Request access to a group or project
+
+Requests access for the authenticated user to a group or project.
+
+Returns `201` if the request succeeds.
+
+```
+POST /groups/:id/access_requests
+POST /projects/:id/access_requests
+```
+
+| Attribute | Type | Required | Description |
+| --------- | ---- | -------- | ----------- |
+| `id`      | integer/string | yes | The group/project ID or path |
+
+```bash
+curl --request POST --header "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" https://gitlab.example.com/api/v3/groups/:id/access_requests
+curl --request POST --header "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" https://gitlab.example.com/api/v3/projects/:id/access_requests
+```
+
+Example response:
+
+```json
+{
+  "id": 1,
+  "username": "raymond_smith",
+  "name": "Raymond Smith",
+  "state": "active",
+  "created_at": "2012-10-22T14:13:35Z",
+  "requested_at": "2012-10-22T14:13:35Z"
+}
+```
+
+## Approve an access request
+
+Approves an access request for the given user.
+
+Returns `201` if the request succeeds.
+
+```
+PUT /groups/:id/access_requests/:user_id/approve
+PUT /projects/:id/access_requests/:user_id/approve
+```
+
+| Attribute | Type | Required | Description |
+| --------- | ---- | -------- | ----------- |
+| `id`      | integer/string | yes | The group/project ID or path |
+| `user_id` | integer | yes   | The user ID of the access requester |
+| `access_level` | integer | no | A valid access level (defaults: `30`, developer access level) |
+
+```bash
+curl --request PUT --header "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" https://gitlab.example.com/api/v3/groups/:id/access_requests/:user_id/approve?access_level=20
+curl --request PUT --header "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" https://gitlab.example.com/api/v3/projects/:id/access_requests/:user_id/approve?access_level=20
+```
+
+Example response:
+
+```json
+{
+  "id": 1,
+  "username": "raymond_smith",
+  "name": "Raymond Smith",
+  "state": "active",
+  "created_at": "2012-10-22T14:13:35Z",
+  "access_level": 20
+}
+```
+
+## Deny an access request
+
+Denies an access request for the given user.
+
+Returns `200` if the request succeeds.
+
+```
+DELETE /groups/:id/access_requests/:user_id
+DELETE /projects/:id/access_requests/:user_id
+```
+
+| Attribute | Type | Required | Description |
+| --------- | ---- | -------- | ----------- |
+| `id`      | integer/string | yes | The group/project ID or path |
+| `user_id` | integer | yes   | The user ID of the access requester |
+
+```bash
+curl --request DELETE --header "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" https://gitlab.example.com/api/v3/groups/:id/access_requests/:user_id
+curl --request DELETE --header "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" https://gitlab.example.com/api/v3/projects/:id/access_requests/:user_id
+```
diff --git a/doc/api/award_emoji.md b/doc/api/award_emoji.md
index 796b3680a755879aa26d5bd88765cfc38c0f0fc3..72ec99b7c56f3f411cd58cace0130ba6c6c09a8f 100644
--- a/doc/api/award_emoji.md
+++ b/doc/api/award_emoji.md
@@ -1,6 +1,6 @@
 # Award Emoji
 
- >**Note:** This feature was introduced in GitLab 8.9
+> [Introduced][ce-4575] in GitLab 8.9.
 
 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
@@ -25,7 +25,7 @@ Parameters:
 | `awardable_id` | integer | yes | The ID of an awardable |
 
 ```bash
-curl -H "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" http://gitlab.example.com/api/v3/projects/1/issues/80/award_emoji
+curl --header "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" http://gitlab.example.com/api/v3/projects/1/issues/80/award_emoji
 ```
 
 Example Response:
@@ -85,7 +85,7 @@ Parameters:
 | `award_id` | integer | yes | The ID of the award emoji |
 
 ```bash
-curl -H "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" http://gitlab.example.com/api/v3/projects/1/issues/80/award_emoji/1
+curl --header "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" http://gitlab.example.com/api/v3/projects/1/issues/80/award_emoji/1
 ```
 
 Example Response:
@@ -127,7 +127,7 @@ Parameters:
 | `name` | string | yes | The name of the emoji, without colons |
 
 ```bash
-curl -X POST -H "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" http://gitlab.example.com/api/v3/projects/1/issues/80/award_emoji?name=blowfish
+curl --request POST --header "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" http://gitlab.example.com/api/v3/projects/1/issues/80/award_emoji?name=blowfish
 ```
 
 Example Response:
@@ -170,7 +170,7 @@ Parameters:
 | `award_id` | integer | yes | The ID of a award_emoji |
 
 ```bash
-curl -X DELETE -H "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" http://gitlab.example.com/api/v3/projects/1/issues/80/award_emoji/344
+curl --request DELETE --header "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" http://gitlab.example.com/api/v3/projects/1/issues/80/award_emoji/344
 ```
 
 Example Response:
@@ -217,7 +217,7 @@ Parameters:
 
 
 ```bash
-curl -H "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" http://gitlab.example.com/api/v3/projects/1/issues/80/notes/1/award_emoji
+curl --header "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" http://gitlab.example.com/api/v3/projects/1/issues/80/notes/1/award_emoji
 ```
 
 Example Response:
@@ -259,7 +259,7 @@ Parameters:
 | `award_id` | integer | yes | The ID of the award emoji |
 
 ```bash
-curl -H "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" http://gitlab.example.com/api/v3/projects/1/issues/80/notes/1/award_emoji/2
+curl --header "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" http://gitlab.example.com/api/v3/projects/1/issues/80/notes/1/award_emoji/2
 ```
 
 Example Response:
@@ -299,7 +299,7 @@ Parameters:
 | `name` | string | yes | The name of the emoji, without colons |
 
 ```bash
-curl -X POST -H "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" http://gitlab.example.com/api/v3/projects/1/issues/80/notes/1/award_emoji?name=rocket
+curl --request POST --header "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" http://gitlab.example.com/api/v3/projects/1/issues/80/notes/1/award_emoji?name=rocket
 ```
 
 Example Response:
@@ -342,7 +342,7 @@ Parameters:
 | `award_id` | integer | yes | The ID of a award_emoji |
 
 ```bash
-curl -X DELETE -H "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" http://gitlab.example.com/api/v3/projects/1/issues/80/award_emoji/345
+curl --request DELETE --header "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" http://gitlab.example.com/api/v3/projects/1/issues/80/award_emoji/345
 ```
 
 Example Response:
@@ -365,3 +365,5 @@ Example Response:
   "awardable_type": "Note"
 }
 ```
+
+[ce-4575]: https://gitlab.com/gitlab-org/gitlab-ce/merge_requests/4575
diff --git a/doc/api/branches.md b/doc/api/branches.md
index dbe8306c66f42b392419a0d74451ce4af8558a2b..0b5f7778fc764e294114f282aac7f9c41d642227 100644
--- a/doc/api/branches.md
+++ b/doc/api/branches.md
@@ -13,7 +13,7 @@ GET /projects/:id/repository/branches
 | `id` | integer | yes | The ID of a project |
 
 ```bash
-curl -H "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" https://gitlab.example.com/api/v3/projects/5/repository/branches
+curl --header "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" https://gitlab.example.com/api/v3/projects/5/repository/branches
 ```
 
 Example response:
@@ -57,7 +57,7 @@ GET /projects/:id/repository/branches/:branch
 | `branch` | string | yes | The name of the branch |
 
 ```bash
-curl -H "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" https://gitlab.example.com/api/v3/projects/5/repository/branches/master
+curl --header "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" https://gitlab.example.com/api/v3/projects/5/repository/branches/master
 ```
 
 Example response:
@@ -95,7 +95,7 @@ PUT /projects/:id/repository/branches/:branch/protect
 ```
 
 ```bash
-curl -X PUT -H "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" https://gitlab.example.com/api/v3/projects/5/repository/branches/master/protect?developers_can_push=true&developers_can_merge=true
+curl --request PUT --header "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" https://gitlab.example.com/api/v3/projects/5/repository/branches/master/protect?developers_can_push=true&developers_can_merge=true
 ```
 
 | Attribute | Type | Required | Description |
@@ -140,7 +140,7 @@ PUT /projects/:id/repository/branches/:branch/unprotect
 ```
 
 ```bash
-curl -X PUT -H "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" https://gitlab.example.com/api/v3/projects/5/repository/branches/master/unprotect
+curl --request PUT --header "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" https://gitlab.example.com/api/v3/projects/5/repository/branches/master/unprotect
 ```
 
 | Attribute | Type | Required | Description |
@@ -185,7 +185,7 @@ POST /projects/:id/repository/branches
 | `ref`         | string  | yes | The branch name or commit SHA to create branch from |
 
 ```bash
-curl -X POST -H "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" "https://gitlab.example.com/api/v3/projects/5/repository/branches?branch_name=newbranch&ref=master"
+curl --request POST --header "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" "https://gitlab.example.com/api/v3/projects/5/repository/branches?branch_name=newbranch&ref=master"
 ```
 
 Example response:
@@ -230,7 +230,7 @@ It returns `200` if it succeeds, `404` if the branch to be deleted does not exis
 or `400` for other reasons. In case of an error, an explaining message is provided.
 
 ```bash
-curl -X DELETE -H "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" "https://gitlab.example.com/api/v3/projects/5/repository/branches/newbranch"
+curl --request DELETE --header "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" "https://gitlab.example.com/api/v3/projects/5/repository/branches/newbranch"
 ```
 
 Example response:
diff --git a/doc/api/build_triggers.md b/doc/api/build_triggers.md
index 0881a7d7a90408b433c10c20dfcc34788bf3c3c2..1b7a18401384dd1ae5ff23c156fb57e32c4c384b 100644
--- a/doc/api/build_triggers.md
+++ b/doc/api/build_triggers.md
@@ -15,7 +15,7 @@ GET /projects/:id/triggers
 | `id`      | integer | yes      | The ID of a project |
 
 ```
-curl -H "PRIVATE_TOKEN: 9koXpg98eAheJpvBs5tK" "https://gitlab.example.com/api/v3/projects/1/triggers"
+curl --header "PRIVATE_TOKEN: 9koXpg98eAheJpvBs5tK" "https://gitlab.example.com/api/v3/projects/1/triggers"
 ```
 
 ```json
@@ -51,7 +51,7 @@ GET /projects/:id/triggers/:token
 | `token`   | string  | yes      | The `token` of a trigger |
 
 ```
-curl -H "PRIVATE_TOKEN: 9koXpg98eAheJpvBs5tK" "https://gitlab.example.com/api/v3/projects/1/triggers/7b9148c158980bbd9bcea92c17522d"
+curl --header "PRIVATE_TOKEN: 9koXpg98eAheJpvBs5tK" "https://gitlab.example.com/api/v3/projects/1/triggers/7b9148c158980bbd9bcea92c17522d"
 ```
 
 ```json
@@ -77,7 +77,7 @@ POST /projects/:id/triggers
 | `id`      | integer | yes      | The ID of a project      |
 
 ```
-curl -X POST -H "PRIVATE_TOKEN: 9koXpg98eAheJpvBs5tK" "https://gitlab.example.com/api/v3/projects/1/triggers"
+curl --request POST --header "PRIVATE_TOKEN: 9koXpg98eAheJpvBs5tK" "https://gitlab.example.com/api/v3/projects/1/triggers"
 ```
 
 ```json
@@ -104,7 +104,7 @@ DELETE /projects/:id/triggers/:token
 | `token`   | string  | yes      | The `token` of a trigger |
 
 ```
-curl -X DELETE -H "PRIVATE_TOKEN: 9koXpg98eAheJpvBs5tK" "https://gitlab.example.com/api/v3/projects/1/triggers/7b9148c158980bbd9bcea92c17522d"
+curl --request DELETE --header "PRIVATE_TOKEN: 9koXpg98eAheJpvBs5tK" "https://gitlab.example.com/api/v3/projects/1/triggers/7b9148c158980bbd9bcea92c17522d"
 ```
 
 ```json
diff --git a/doc/api/build_variables.md b/doc/api/build_variables.md
index b96f1bdac8ab0971a5b21573c00555738b6cd85c..a21751a49eabd2c0c2c393d9bb1d40ae502167ae 100644
--- a/doc/api/build_variables.md
+++ b/doc/api/build_variables.md
@@ -13,7 +13,7 @@ GET /projects/:id/variables
 | `id`      | integer | yes      | The ID of a project |
 
 ```
-curl -H "PRIVATE_TOKEN: 9koXpg98eAheJpvBs5tK" "https://gitlab.example.com/api/v3/projects/1/variables"
+curl --header "PRIVATE_TOKEN: 9koXpg98eAheJpvBs5tK" "https://gitlab.example.com/api/v3/projects/1/variables"
 ```
 
 ```json
@@ -43,7 +43,7 @@ GET /projects/:id/variables/:key
 | `key`     | string  | yes      | The `key` of a variable |
 
 ```
-curl -H "PRIVATE_TOKEN: 9koXpg98eAheJpvBs5tK" "https://gitlab.example.com/api/v3/projects/1/variables/TEST_VARIABLE_1"
+curl --header "PRIVATE_TOKEN: 9koXpg98eAheJpvBs5tK" "https://gitlab.example.com/api/v3/projects/1/variables/TEST_VARIABLE_1"
 ```
 
 ```json
@@ -68,7 +68,7 @@ POST /projects/:id/variables
 | `value`   | string  | yes      | The `value` of a variable |
 
 ```
-curl -X POST -H "PRIVATE_TOKEN: 9koXpg98eAheJpvBs5tK" "https://gitlab.example.com/api/v3/projects/1/variables" -F "key=NEW_VARIABLE" -F "value=new value"
+curl --request POST --header "PRIVATE_TOKEN: 9koXpg98eAheJpvBs5tK" "https://gitlab.example.com/api/v3/projects/1/variables" --form "key=NEW_VARIABLE" --form "value=new value"
 ```
 
 ```json
@@ -93,7 +93,7 @@ PUT /projects/:id/variables/:key
 | `value`   | string  | yes      | The `value` of a variable |
 
 ```
-curl -X PUT -H "PRIVATE_TOKEN: 9koXpg98eAheJpvBs5tK" "https://gitlab.example.com/api/v3/projects/1/variables/NEW_VARIABLE" -F "value=updated value"
+curl --request PUT --header "PRIVATE_TOKEN: 9koXpg98eAheJpvBs5tK" "https://gitlab.example.com/api/v3/projects/1/variables/NEW_VARIABLE" --form "value=updated value"
 ```
 
 ```json
@@ -117,7 +117,7 @@ DELETE /projects/:id/variables/:key
 | `key`     | string  | yes      | The `key` of a variable |
 
 ```
-curl -X DELETE -H "PRIVATE_TOKEN: 9koXpg98eAheJpvBs5tK" "https://gitlab.example.com/api/v3/projects/1/variables/VARIABLE_1"
+curl --request DELETE --header "PRIVATE_TOKEN: 9koXpg98eAheJpvBs5tK" "https://gitlab.example.com/api/v3/projects/1/variables/VARIABLE_1"
 ```
 
 ```json
diff --git a/doc/api/builds.md b/doc/api/builds.md
index 24d90e22a9b67615f7e1a795842277c384217eda..dce666445d0911f441cffbac74b8e28ba9ce08ea 100644
--- a/doc/api/builds.md
+++ b/doc/api/builds.md
@@ -14,7 +14,7 @@ GET /projects/:id/builds
 | `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 |
 
 ```
-curl -H "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"
 ```
 
 Example of response
@@ -123,7 +123,7 @@ GET /projects/:id/repository/commits/:sha/builds
 | `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 |
 
 ```
-curl -H "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"
 ```
 
 Example of response
@@ -209,7 +209,7 @@ GET /projects/:id/builds/:build_id
 | `build_id` | integer | yes      | The ID of a build   |
 
 ```
-curl -H "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" "https://gitlab.example.com/api/v3/projects/1/builds/8"
+curl --header "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" "https://gitlab.example.com/api/v3/projects/1/builds/8"
 ```
 
 Example of response
@@ -271,7 +271,7 @@ GET /projects/:id/builds/:build_id/artifacts
 | `build_id` | integer | yes      | The ID of a build   |
 
 ```
-curl -H "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" "https://gitlab.example.com/api/v3/projects/1/builds/8/artifacts"
+curl --header "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" "https://gitlab.example.com/api/v3/projects/1/builds/8/artifacts"
 ```
 
 Response:
@@ -305,7 +305,7 @@ Parameters
 Example request:
 
 ```
-curl -H "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" "https://gitlab.example.com/api/v3/projects/1/builds/artifacts/master/download?job=test"
+curl --header "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" "https://gitlab.example.com/api/v3/projects/1/builds/artifacts/master/download?job=test"
 ```
 
 Example response:
@@ -331,7 +331,7 @@ GET /projects/:id/builds/:build_id/trace
 | build_id   | integer | yes      | The ID of a build   |
 
 ```
-curl -H "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" "https://gitlab.example.com/api/v3/projects/1/builds/8/trace"
+curl --header "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" "https://gitlab.example.com/api/v3/projects/1/builds/8/trace"
 ```
 
 Response:
@@ -355,7 +355,7 @@ POST /projects/:id/builds/:build_id/cancel
 | `build_id` | integer | yes      | The ID of a build   |
 
 ```
-curl -X POST -H "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" "https://gitlab.example.com/api/v3/projects/1/builds/1/cancel"
+curl --request POST --header "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" "https://gitlab.example.com/api/v3/projects/1/builds/1/cancel"
 ```
 
 Example of response
@@ -401,7 +401,7 @@ POST /projects/:id/builds/:build_id/retry
 | `build_id` | integer | yes      | The ID of a build   |
 
 ```
-curl -X POST -H "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" "https://gitlab.example.com/api/v3/projects/1/builds/1/retry"
+curl --request POST --header "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" "https://gitlab.example.com/api/v3/projects/1/builds/1/retry"
 ```
 
 Example of response
@@ -451,7 +451,7 @@ Parameters
 Example of request
 
 ```
-curl -X POST -H "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" "https://gitlab.example.com/api/v3/projects/1/builds/1/erase"
+curl --request POST --header "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" "https://gitlab.example.com/api/v3/projects/1/builds/1/erase"
 ```
 
 Example of response
@@ -501,7 +501,7 @@ Parameters
 Example request:
 
 ```
-curl -X POST -H "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" "https://gitlab.example.com/api/v3/projects/1/builds/1/artifacts/keep"
+curl --request POST --header "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" "https://gitlab.example.com/api/v3/projects/1/builds/1/artifacts/keep"
 ```
 
 Example response:
@@ -532,3 +532,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 d779463fd8cb047f17669399fece03f1249cc43f..2a71b087f193a091d5354c604b7cd6d0f5d1f692 100644
--- a/doc/api/ci/builds.md
+++ b/doc/api/ci/builds.md
@@ -35,7 +35,7 @@ POST /ci/api/v1/builds/register
 
 
 ```
-curl -X POST "https://gitlab.example.com/ci/api/v1/builds/register" -F "token=t0k3n"
+curl --request POST "https://gitlab.example.com/ci/api/v1/builds/register" --form "token=t0k3n"
 ```
 
 ### Update details of an existing build
@@ -52,7 +52,7 @@ PUT /ci/api/v1/builds/:id
 | `trace`   | string  | no       | The trace of a build |
 
 ```
-curl -X PUT "https://gitlab.example.com/ci/api/v1/builds/1234" -F "token=t0k3n" -F "state=running" -F "trace=Running git clone...\n"
+curl --request PUT "https://gitlab.example.com/ci/api/v1/builds/1234" --form "token=t0k3n" --form "state=running" --form "trace=Running git clone...\n"
 ```
 
 ### Incremental build trace update
@@ -87,7 +87,7 @@ Headers:
 | `Content-Range` | string  | yes      | Bytes range of trace that is sent |
 
 ```
-curl -X PATCH "https://gitlab.example.com/ci/api/v1/builds/1234/trace.txt" -H "BUILD-TOKEN=build_t0k3n" -H "Content-Range=0-21" -d "Running git clone...\n"
+curl --request PATCH "https://gitlab.example.com/ci/api/v1/builds/1234/trace.txt" --header "BUILD-TOKEN=build_t0k3n" --header "Content-Range=0-21" --data "Running git clone...\n"
 ```
 
 
@@ -104,7 +104,7 @@ POST /ci/api/v1/builds/:id/artifacts
 | `file`    | mixed   | yes      | Artifacts file                |
 
 ```
-curl -X POST "https://gitlab.example.com/ci/api/v1/builds/1234/artifacts" -F "token=build_t0k3n" -F "file=@/path/to/file"
+curl --request POST "https://gitlab.example.com/ci/api/v1/builds/1234/artifacts" --form "token=build_t0k3n" --form "file=@/path/to/file"
 ```
 
 ### Download the artifacts file from build
@@ -119,7 +119,7 @@ GET /ci/api/v1/builds/:id/artifacts
 | `token`   | string  | yes      | The build authorization token |
 
 ```
-curl "https://gitlab.example.com/ci/api/v1/builds/1234/artifacts" -F "token=build_t0k3n"
+curl "https://gitlab.example.com/ci/api/v1/builds/1234/artifacts" --form "token=build_t0k3n"
 ```
 
 ### Remove the artifacts file from build
@@ -134,5 +134,5 @@ DELETE /ci/api/v1/builds/:id/artifacts
 | `token`   | string  | yes      | The build authorization token |
 
 ```
-curl -X DELETE "https://gitlab.example.com/ci/api/v1/builds/1234/artifacts" -F "token=build_t0k3n"
+curl --request DELETE "https://gitlab.example.com/ci/api/v1/builds/1234/artifacts" --form "token=build_t0k3n"
 ```
diff --git a/doc/api/ci/runners.md b/doc/api/ci/runners.md
index 96b3c42f773a69b8f1d4e4f9bdd6802a9ed9ca60..ecec53fde0371879031fd6b71a608f100f431c13 100644
--- a/doc/api/ci/runners.md
+++ b/doc/api/ci/runners.md
@@ -35,7 +35,7 @@ POST /ci/api/v1/runners/register
 Example request:
 
 ```sh
-curl -X POST "https://gitlab.example.com/ci/api/v1/runners/register" -F "token=t0k3n"
+curl --request POST "https://gitlab.example.com/ci/api/v1/runners/register" --form "token=t0k3n"
 ```
 
 ## Delete a Runner
@@ -53,5 +53,5 @@ DELETE /ci/api/v1/runners/delete
 Example request:
 
 ```sh
-curl -X DELETE "https://gitlab.example.com/ci/api/v1/runners/delete" -F "token=t0k3n"
+curl --request DELETE "https://gitlab.example.com/ci/api/v1/runners/delete" --form "token=t0k3n"
 ```
diff --git a/doc/api/commits.md b/doc/api/commits.md
index 2960c2ae428db0dd2514de264147697d930a52bb..5c98c5d7565b5820a72af26bc05751b39152f900 100644
--- a/doc/api/commits.md
+++ b/doc/api/commits.md
@@ -16,7 +16,7 @@ GET /projects/:id/repository/commits
 | `until` | string | no | Only commits before or in this date will be returned in ISO 8601 format YYYY-MM-DDTHH:MM:SSZ |
 
 ```bash
-curl -H "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" "https://gitlab.example.com/api/v3/projects/5/repository/commits"
+curl --header "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" "https://gitlab.example.com/api/v3/projects/5/repository/commits"
 ```
 
 Example response:
@@ -62,7 +62,7 @@ Parameters:
 | `sha` | string | yes | The commit hash or name of a repository branch or tag |
 
 ```bash
-curl -H "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" "https://gitlab.example.com/api/v3/projects/5/repository/commits/master
+curl --header "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" "https://gitlab.example.com/api/v3/projects/5/repository/commits/master
 ```
 
 Example response:
@@ -106,7 +106,7 @@ Parameters:
 | `sha` | string | yes | The commit hash or name of a repository branch or tag |
 
 ```bash
-curl -H "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" "https://gitlab.example.com/api/v3/projects/5/repository/commits/master/diff"
+curl --header "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" "https://gitlab.example.com/api/v3/projects/5/repository/commits/master/diff"
 ```
 
 Example response:
@@ -142,7 +142,7 @@ Parameters:
 | `sha` | string | yes | The commit hash or name of a repository branch or tag |
 
 ```bash
-curl -H "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" "https://gitlab.example.com/api/v3/projects/5/repository/commits/master/comments"
+curl --header "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" "https://gitlab.example.com/api/v3/projects/5/repository/commits/master/comments"
 ```
 
 Example response:
@@ -195,7 +195,7 @@ POST /projects/:id/repository/commits/:sha/comments
 | `line_type` | string  | no  | The line type. Takes `new` or `old` as arguments |
 
 ```bash
-curl -X POST -H "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" -F "note=Nice picture man\!" -F "path=dudeism.md" -F "line=11" -F "line_type=new" https://gitlab.example.com/api/v3/projects/17/repository/commits/18f3e63d05582537db6d183d9d557be09e1f90c8/comments
+curl --request POST --header "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" --form "note=Nice picture man\!" --form "path=dudeism.md" --form "line=11" --form "line_type=new" https://gitlab.example.com/api/v3/projects/17/repository/commits/18f3e63d05582537db6d183d9d557be09e1f90c8/comments
 ```
 
 Example response:
@@ -240,7 +240,7 @@ GET /projects/:id/repository/commits/:sha/statuses
 | `all`     | boolean | no  | Return all statuses, not only the latest ones
 
 ```bash
-curl -H "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" "https://gitlab.example.com/api/v3/projects/17/repository/commits/18f3e63d05582537db6d183d9d557be09e1f90c8/statuses
+curl --header "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" "https://gitlab.example.com/api/v3/projects/17/repository/commits/18f3e63d05582537db6d183d9d557be09e1f90c8/statuses
 ```
 
 Example response:
@@ -315,7 +315,7 @@ POST /projects/:id/statuses/:sha
 | `description` | string  | no  | The short description of the status
 
 ```bash
-curl -X POST -H "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" "https://gitlab.example.com/api/v3/projects/17/statuses/18f3e63d05582537db6d183d9d557be09e1f90c8?state=success"
+curl --request POST --header "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" "https://gitlab.example.com/api/v3/projects/17/statuses/18f3e63d05582537db6d183d9d557be09e1f90c8?state=success"
 ```
 
 Example response:
diff --git a/doc/api/deploy_key_multiple_projects.md b/doc/api/deploy_key_multiple_projects.md
index 9280f0d68b62ec3b128ae52f27f71ad012f28797..73cb4b7ea8c277ef2cc5e7e95c8cdf423fff52b2 100644
--- a/doc/api/deploy_key_multiple_projects.md
+++ b/doc/api/deploy_key_multiple_projects.md
@@ -7,23 +7,23 @@ First, find the ID of the projects you're interested in, by either listing all
 projects:
 
 ```
-curl -H 'PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK' https://gitlab.example.com/api/v3/projects
+curl --header 'PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK' https://gitlab.example.com/api/v3/projects
 ```
 
 Or finding the ID of a group and then listing all projects in that group:
 
 ```
-curl -H 'PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK' https://gitlab.example.com/api/v3/groups
+curl --header 'PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK' https://gitlab.example.com/api/v3/groups
 
 # For group 1234:
-curl -H 'PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK' https://gitlab.example.com/api/v3/groups/1234
+curl --header 'PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK' https://gitlab.example.com/api/v3/groups/1234
 ```
 
 With those IDs, add the same deploy key to all:
 
 ```
 for project_id in 321 456 987; do
-    curl -X POST -H "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" -H "Content-Type: application/json" \
+    curl --request POST --header "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" --header "Content-Type: application/json" \
     --data '{"title": "my key", "key": "ssh-rsa AAAA..."}' https://gitlab.example.com/api/v3/projects/${project_id}/deploy_keys
 done
 ```
diff --git a/doc/api/deploy_keys.md b/doc/api/deploy_keys.md
index 4e620ccc81a5967e760fdf8000d96510f17bb2d6..ca44afbf355bde14d960c2fe9f746c509ef7d7fb 100644
--- a/doc/api/deploy_keys.md
+++ b/doc/api/deploy_keys.md
@@ -9,7 +9,7 @@ GET /deploy_keys
 ```
 
 ```bash
-curl -H "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" "https://gitlab.example.com/api/v3/deploy_keys"
+curl --header "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" "https://gitlab.example.com/api/v3/deploy_keys"
 ```
 
 Example response:
@@ -44,7 +44,7 @@ GET /projects/:id/deploy_keys
 | `id` | integer | yes | The ID of the project |
 
 ```bash
-curl -H "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" "https://gitlab.example.com/api/v3/projects/5/deploy_keys"
+curl --header "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" "https://gitlab.example.com/api/v3/projects/5/deploy_keys"
 ```
 
 Example response:
@@ -82,7 +82,7 @@ Parameters:
 | `key_id`  | integer | yes | The ID of the deploy key |
 
 ```bash
-curl -H "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" "https://gitlab.example.com/api/v3/projects/5/deploy_keys/11"
+curl --header "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" "https://gitlab.example.com/api/v3/projects/5/deploy_keys/11"
 ```
 
 Example response:
@@ -114,7 +114,7 @@ POST /projects/:id/deploy_keys
 | `key`   | string  | yes | New deploy key |
 
 ```bash
-curl -X POST -H "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" -H "Content-Type: application/json" --data '{"title": "My deploy key", "key": "ssh-rsa AAAA..."}' "https://gitlab.example.com/api/v3/projects/5/deploy_keys/"
+curl --request POST --header "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" --header "Content-Type: application/json" --data '{"title": "My deploy key", "key": "ssh-rsa AAAA..."}' "https://gitlab.example.com/api/v3/projects/5/deploy_keys/"
 ```
 
 Example response:
@@ -142,7 +142,7 @@ DELETE /projects/:id/deploy_keys/:key_id
 | `key_id`  | integer | yes | The ID of the deploy key |
 
 ```bash
-curl -X DELETE -H "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" "https://gitlab.example.com/api/v3/projects/5/deploy_keys/13"
+curl --request DELETE --header "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" "https://gitlab.example.com/api/v3/projects/5/deploy_keys/13"
 ```
 
 Example response:
@@ -159,3 +159,51 @@ Example response:
    "id" : 13
 }
 ```
+
+## Enable a deploy key
+
+Enables a deploy key for a project so this can be used. Returns the enabled key, with a status code 201 when successful.
+
+```bash
+curl --request POST --header "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" https://gitlab.example.com/api/v3/projects/5/deploy_keys/13/enable
+```
+
+| Attribute | Type | Required | Description |
+| --------- | ---- | -------- | ----------- |
+| `id`      | integer | yes | The ID of the project |
+| `key_id`  | integer | yes | The ID of the deploy key |
+
+Example response:
+
+```json
+{
+   "key" : "ssh-rsa AAAA...",
+   "id" : 12,
+   "title" : "My deploy key",
+   "created_at" : "2015-08-29T12:44:31.550Z"
+}
+```
+
+## Disable a deploy key
+
+Disable a deploy key for a project. Returns the disabled key.
+
+```bash
+curl --request DELETE --header "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" https://gitlab.example.com/api/v3/projects/5/deploy_keys/13/disable
+```
+
+| Attribute | Type | Required | Description |
+| --------- | ---- | -------- | ----------- |
+| `id`      | integer | yes | The ID of the project |
+| `key_id`  | integer | yes | The ID of the deploy key |
+
+Example response:
+
+```json
+{
+   "key" : "ssh-rsa AAAA...",
+   "id" : 12,
+   "title" : "My deploy key",
+   "created_at" : "2015-08-29T12:44:31.550Z"
+}
+```
diff --git a/doc/api/deployments.md b/doc/api/deployments.md
new file mode 100644
index 0000000000000000000000000000000000000000..417962de82d42e1d7a0c99523692d1f98d12c222
--- /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/u/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/u/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/u/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/u/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/u/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/u/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/enviroments.md b/doc/api/enviroments.md
index 1e12ded448c09b5c281ede3d84b82bfb0b684310..87a5fa67124996dcf9e80d93800ecb6d581b7353 100644
--- a/doc/api/enviroments.md
+++ b/doc/api/enviroments.md
@@ -13,7 +13,7 @@ GET /projects/:id/environments
 | `id`      | integer | yes      | The ID of the project |
 
 ```bash
-curl -H "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" https://gitlab.example.com/api/v3/projects/1/environments
+curl --header "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" https://gitlab.example.com/api/v3/projects/1/environments
 ```
 
 Example response:
@@ -45,7 +45,7 @@ POST /projects/:id/environment
 | `external_url` | string  | no     | Place to link to for this environment |
 
 ```bash
-curl --data "name=deploy&external_url=https://deploy.example.gitlab.com" -H "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" "https://gitlab.example.com/api/v3/projects/1/environments"
+curl --data "name=deploy&external_url=https://deploy.example.gitlab.com" --header "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" "https://gitlab.example.com/api/v3/projects/1/environments"
 ```
 
 Example response:
@@ -76,7 +76,7 @@ PUT /projects/:id/environments/:environments_id
 | `external_url`  | string  | no                                | The new external_url             |
 
 ```bash
-curl -X PUT --data "name=staging&external_url=https://staging.example.gitlab.com" -H "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" "https://gitlab.example.com/api/v3/projects/1/environment/1"
+curl --request PUT --data "name=staging&external_url=https://staging.example.gitlab.com" --header "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" "https://gitlab.example.com/api/v3/projects/1/environment/1"
 ```
 
 Example response:
@@ -103,7 +103,7 @@ DELETE /projects/:id/environments/:environment_id
 | `environment_id` | integer | yes | The ID of the environment |
 
 ```bash
-curl -X DELETE -H "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" "https://gitlab.example.com/api/v3/projects/1/environment/1"
+curl --request DELETE --header "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" "https://gitlab.example.com/api/v3/projects/1/environment/1"
 ```
 
 Example response:
diff --git a/doc/api/groups.md b/doc/api/groups.md
index 87480bebfc43acf719286a8f24f10d0fb5ce2c29..a898387eaa2a6887ee7deedd3468462af8445b69 100644
--- a/doc/api/groups.md
+++ b/doc/api/groups.md
@@ -1,514 +1,434 @@
-# Groups
-
-## List groups
-
-Get a list of groups. (As user: my groups, as admin: all groups)
-
-```
-GET /groups
-```
-
-```json
-[
-  {
-    "id": 1,
-    "name": "Foobar Group",
-    "path": "foo-bar",
-    "description": "An interesting group"
-  }
-]
-```
-
-You can search for groups by name or path, see below.
-
-
-## List a group's projects
-
-Get a list of projects in this group.
-
-```
-GET /groups/:id/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
-- `ci_enabled_first` - Return projects ordered by ci_enabled flag. Projects with enabled GitLab CI go first
-
-```json
-[
-  {
-    "id": 9,
-    "description": "foo",
-    "default_branch": "master",
-    "tag_list": [],
-    "public": false,
-    "archived": false,
-    "visibility_level": 10,
-    "ssh_url_to_repo": "git@gitlab.example.com/html5-boilerplate.git",
-    "http_url_to_repo": "http://gitlab.example.com/h5bp/html5-boilerplate.git",
-    "web_url": "http://gitlab.example.com/h5bp/html5-boilerplate",
-    "name": "Html5 Boilerplate",
-    "name_with_namespace": "Experimental / 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": true,
-    "created_at": "2016-04-05T21:40:50.169Z",
-    "last_activity_at": "2016-04-06T16:52:08.432Z",
-    "shared_runners_enabled": true,
-    "creator_id": 1,
-    "namespace": {
-      "id": 5,
-      "name": "Experimental",
-      "path": "h5bp",
-      "owner_id": null,
-      "created_at": "2016-04-05T21:40:49.152Z",
-      "updated_at": "2016-04-07T08:07:48.466Z",
-      "description": "foo",
-      "avatar": {
-        "url": null
-      },
-      "share_with_group_lock": false,
-      "visibility_level": 10
-    },
-    "avatar_url": null,
-    "star_count": 1,
-    "forks_count": 0,
-    "open_issues_count": 3,
-    "public_builds": true,
-    "shared_with_groups": []
-  }
-]
-```
-
-## Details of a group
-
-Get all details of a group.
-
-```
-GET /groups/:id
-```
-
-Parameters:
-
-| Attribute | Type | Required | Description |
-| --------- | ---- | -------- | ----------- |
-| `id` | integer/string | yes | The ID or path of a group |
-
-```bash
-curl -H "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" https://gitlab.example.com/api/v3/groups/4
-```
-
-Example response:
-
-```json
-{
-  "id": 4,
-  "name": "Twitter",
-  "path": "twitter",
-  "description": "Aliquid qui quis dignissimos distinctio ut commodi voluptas est.",
-  "visibility_level": 20,
-  "avatar_url": null,
-  "web_url": "https://gitlab.example.com/groups/twitter",
-  "projects": [
-    {
-      "id": 7,
-      "description": "Voluptas veniam qui et beatae voluptas doloremque explicabo facilis.",
-      "default_branch": "master",
-      "tag_list": [],
-      "public": true,
-      "archived": false,
-      "visibility_level": 20,
-      "ssh_url_to_repo": "git@gitlab.example.com:twitter/typeahead-js.git",
-      "http_url_to_repo": "https://gitlab.example.com/twitter/typeahead-js.git",
-      "web_url": "https://gitlab.example.com/twitter/typeahead-js",
-      "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,
-      "container_registry_enabled": true,
-      "created_at": "2016-06-17T07:47:25.578Z",
-      "last_activity_at": "2016-06-17T07:47:25.881Z",
-      "shared_runners_enabled": true,
-      "creator_id": 1,
-      "namespace": {
-        "id": 4,
-        "name": "Twitter",
-        "path": "twitter",
-        "owner_id": null,
-        "created_at": "2016-06-17T07:47:24.216Z",
-        "updated_at": "2016-06-17T07:47:24.216Z",
-        "description": "Aliquid qui quis dignissimos distinctio ut commodi voluptas est.",
-        "avatar": {
-          "url": null
-        },
-        "share_with_group_lock": false,
-        "visibility_level": 20
-      },
-      "avatar_url": null,
-      "star_count": 0,
-      "forks_count": 0,
-      "open_issues_count": 3,
-      "public_builds": true,
-      "shared_with_groups": []
-    },
-    {
-      "id": 6,
-      "description": "Aspernatur omnis repudiandae qui voluptatibus eaque.",
-      "default_branch": "master",
-      "tag_list": [],
-      "public": false,
-      "archived": false,
-      "visibility_level": 10,
-      "ssh_url_to_repo": "git@gitlab.example.com:twitter/flight.git",
-      "http_url_to_repo": "https://gitlab.example.com/twitter/flight.git",
-      "web_url": "https://gitlab.example.com/twitter/flight",
-      "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,
-      "container_registry_enabled": true,
-      "created_at": "2016-06-17T07:47:24.661Z",
-      "last_activity_at": "2016-06-17T07:47:24.838Z",
-      "shared_runners_enabled": true,
-      "creator_id": 1,
-      "namespace": {
-        "id": 4,
-        "name": "Twitter",
-        "path": "twitter",
-        "owner_id": null,
-        "created_at": "2016-06-17T07:47:24.216Z",
-        "updated_at": "2016-06-17T07:47:24.216Z",
-        "description": "Aliquid qui quis dignissimos distinctio ut commodi voluptas est.",
-        "avatar": {
-          "url": null
-        },
-        "share_with_group_lock": false,
-        "visibility_level": 20
-      },
-      "avatar_url": null,
-      "star_count": 0,
-      "forks_count": 0,
-      "open_issues_count": 8,
-      "public_builds": true,
-      "shared_with_groups": []
-    }
-  ],
-  "shared_projects": [
-    {
-      "id": 8,
-      "description": "Velit eveniet provident fugiat saepe eligendi autem.",
-      "default_branch": "master",
-      "tag_list": [],
-      "public": false,
-      "archived": false,
-      "visibility_level": 0,
-      "ssh_url_to_repo": "git@gitlab.example.com:h5bp/html5-boilerplate.git",
-      "http_url_to_repo": "https://gitlab.example.com/h5bp/html5-boilerplate.git",
-      "web_url": "https://gitlab.example.com/h5bp/html5-boilerplate",
-      "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,
-      "container_registry_enabled": true,
-      "created_at": "2016-06-17T07:47:27.089Z",
-      "last_activity_at": "2016-06-17T07:47:27.310Z",
-      "shared_runners_enabled": true,
-      "creator_id": 1,
-      "namespace": {
-        "id": 5,
-        "name": "H5bp",
-        "path": "h5bp",
-        "owner_id": null,
-        "created_at": "2016-06-17T07:47:26.621Z",
-        "updated_at": "2016-06-17T07:47:26.621Z",
-        "description": "Id consequatur rem vel qui doloremque saepe.",
-        "avatar": {
-          "url": null
-        },
-        "share_with_group_lock": false,
-        "visibility_level": 20
-      },
-      "avatar_url": null,
-      "star_count": 0,
-      "forks_count": 0,
-      "open_issues_count": 4,
-      "public_builds": true,
-      "shared_with_groups": [
-        {
-          "group_id": 4,
-          "group_name": "Twitter",
-          "group_access_level": 30
-        },
-        {
-          "group_id": 3,
-          "group_name": "Gitlab Org",
-          "group_access_level": 10
-        }
-      ]
-    }
-  ]
-}
-```
-
-## New group
-
-Creates a new project group. Available only for users who can create groups.
-
-```
-POST /groups
-```
-
-Parameters:
-
-- `name` (required) - The name of the group
-- `path` (required) - The path of the group
-- `description` (optional) - The group's description
-- `visibility_level` (optional) - The group's visibility. 0 for private, 10 for internal, 20 for public.
-
-## Transfer project to group
-
-Transfer a project to the Group namespace. Available only for admin
-
-```
-POST  /groups/:id/projects/:project_id
-```
-
-Parameters:
-
-- `id` (required) - The ID or path of a group
-- `project_id` (required) - The ID of a project
-
-## Update group
-
-Updates the project group. Only available to group owners and administrators.
-
-```
-PUT /groups/:id
-```
-
-| Attribute | Type | Required | Description |
-| --------- | ---- | -------- | ----------- |
-| `id` | integer | yes | The ID of the group |
-| `name` | string | no | The name of the group |
-| `path` | string | no | The path of the group |
-| `description` | string | no | The description of the group |
-| `visibility_level` | integer | no | The visibility level of the group. 0 for private, 10 for internal, 20 for public. |
-
-```bash
-curl -X PUT -H "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" "https://gitlab.example.com/api/v3/groups/5?name=Experimental"
-
-```
-
-Example response:
-
-```json
-{
-  "id": 5,
-  "name": "Experimental",
-  "path": "h5bp",
-  "description": "foo",
-  "visibility_level": 10,
-  "avatar_url": null,
-  "web_url": "http://gitlab.example.com/groups/h5bp",
-  "projects": [
-    {
-      "id": 9,
-      "description": "foo",
-      "default_branch": "master",
-      "tag_list": [],
-      "public": false,
-      "archived": false,
-      "visibility_level": 10,
-      "ssh_url_to_repo": "git@gitlab.example.com/html5-boilerplate.git",
-      "http_url_to_repo": "http://gitlab.example.com/h5bp/html5-boilerplate.git",
-      "web_url": "http://gitlab.example.com/h5bp/html5-boilerplate",
-      "name": "Html5 Boilerplate",
-      "name_with_namespace": "Experimental / 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": true,
-      "created_at": "2016-04-05T21:40:50.169Z",
-      "last_activity_at": "2016-04-06T16:52:08.432Z",
-      "shared_runners_enabled": true,
-      "creator_id": 1,
-      "namespace": {
-        "id": 5,
-        "name": "Experimental",
-        "path": "h5bp",
-        "owner_id": null,
-        "created_at": "2016-04-05T21:40:49.152Z",
-        "updated_at": "2016-04-07T08:07:48.466Z",
-        "description": "foo",
-        "avatar": {
-          "url": null
-        },
-        "share_with_group_lock": false,
-        "visibility_level": 10
-      },
-      "avatar_url": null,
-      "star_count": 1,
-      "forks_count": 0,
-      "open_issues_count": 3,
-      "public_builds": true,
-      "shared_with_groups": []
-    }
-  ]
-}
-```
-
-## Remove group
-
-Removes group with all projects inside.
-
-```
-DELETE /groups/:id
-```
-
-Parameters:
-
-- `id` (required) - The ID or path of a user group
-
-## Search for group
-
-Get all groups that match your string in their name or path.
-
-```
-GET /groups?search=foobar
-```
-
-```json
-[
-  {
-    "id": 1,
-    "name": "Foobar Group",
-    "path": "foo-bar",
-    "description": "An interesting group"
-  }
-]
-```
-
-## Group members
-
-**Group access levels**
-
-The group access levels are defined in the `Gitlab::Access` module. Currently, these levels are recognized:
-
-```
-GUEST     = 10
-REPORTER  = 20
-DEVELOPER = 30
-MASTER    = 40
-OWNER     = 50
-```
-
-### List group members
-
-Get a list of group members viewable by the authenticated user.
-
-```
-GET /groups/:id/members
-```
-
-```json
-[
-  {
-    "id": 1,
-    "username": "raymond_smith",
-    "name": "Raymond Smith",
-    "state": "active",
-    "created_at": "2012-10-22T14:13:35Z",
-    "access_level": 30
-  },
-  {
-    "id": 2,
-    "username": "john_doe",
-    "name": "John Doe",
-    "state": "active",
-    "created_at": "2012-10-22T14:13:35Z",
-    "access_level": 30
-  }
-]
-```
-
-### Add group member
-
-Adds a user to the list of group members.
-
-```
-POST /groups/:id/members
-```
-
-Parameters:
-
-- `id` (required) - The ID or path of a group
-- `user_id` (required) - The ID of a user to add
-- `access_level` (required) - Project access level
-
-### Edit group team member
-
-Updates a group team member to a specified access level.
-
-```
-PUT /groups/:id/members/:user_id
-```
-
-Parameters:
-
-- `id` (required) - The ID of a group
-- `user_id` (required) - The ID of a group member
-- `access_level` (required) - Project access level
-
-### Remove user team member
-
-Removes user from user team.
-
-```
-DELETE /groups/:id/members/:user_id
-```
-
-Parameters:
-
-- `id` (required) - The ID or path of a user group
-- `user_id` (required) - The ID of a group member
-
-## Namespaces in groups
-
-By default, groups only get 20 namespaces at a time because the API results are paginated.
-
-To get more (up to 100), pass the following as an argument to the API call:
-```
-/groups?per_page=100
-```
-
-And to switch pages add:
-```
-/groups?per_page=100&page=2
-```
+# Groups
+
+## List groups
+
+Get a list of groups. (As user: my groups, as admin: all groups)
+
+```
+GET /groups
+```
+
+```json
+[
+  {
+    "id": 1,
+    "name": "Foobar Group",
+    "path": "foo-bar",
+    "description": "An interesting group"
+  }
+]
+```
+
+You can search for groups by name or path, see below.
+
+
+## List a group's projects
+
+Get a list of projects in this group.
+
+```
+GET /groups/:id/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
+- `ci_enabled_first` - Return projects ordered by ci_enabled flag. Projects with enabled GitLab CI go first
+
+```json
+[
+  {
+    "id": 9,
+    "description": "foo",
+    "default_branch": "master",
+    "tag_list": [],
+    "public": false,
+    "archived": false,
+    "visibility_level": 10,
+    "ssh_url_to_repo": "git@gitlab.example.com/html5-boilerplate.git",
+    "http_url_to_repo": "http://gitlab.example.com/h5bp/html5-boilerplate.git",
+    "web_url": "http://gitlab.example.com/h5bp/html5-boilerplate",
+    "name": "Html5 Boilerplate",
+    "name_with_namespace": "Experimental / 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": true,
+    "created_at": "2016-04-05T21:40:50.169Z",
+    "last_activity_at": "2016-04-06T16:52:08.432Z",
+    "shared_runners_enabled": true,
+    "creator_id": 1,
+    "namespace": {
+      "id": 5,
+      "name": "Experimental",
+      "path": "h5bp",
+      "owner_id": null,
+      "created_at": "2016-04-05T21:40:49.152Z",
+      "updated_at": "2016-04-07T08:07:48.466Z",
+      "description": "foo",
+      "avatar": {
+        "url": null
+      },
+      "share_with_group_lock": false,
+      "visibility_level": 10
+    },
+    "avatar_url": null,
+    "star_count": 1,
+    "forks_count": 0,
+    "open_issues_count": 3,
+    "public_builds": true,
+    "shared_with_groups": []
+  }
+]
+```
+
+## Details of a group
+
+Get all details of a group.
+
+```
+GET /groups/:id
+```
+
+Parameters:
+
+| Attribute | Type | Required | Description |
+| --------- | ---- | -------- | ----------- |
+| `id` | integer/string | yes | The ID or path of a group |
+
+```bash
+curl --header "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" https://gitlab.example.com/api/v3/groups/4
+```
+
+Example response:
+
+```json
+{
+  "id": 4,
+  "name": "Twitter",
+  "path": "twitter",
+  "description": "Aliquid qui quis dignissimos distinctio ut commodi voluptas est.",
+  "visibility_level": 20,
+  "avatar_url": null,
+  "web_url": "https://gitlab.example.com/groups/twitter",
+  "projects": [
+    {
+      "id": 7,
+      "description": "Voluptas veniam qui et beatae voluptas doloremque explicabo facilis.",
+      "default_branch": "master",
+      "tag_list": [],
+      "public": true,
+      "archived": false,
+      "visibility_level": 20,
+      "ssh_url_to_repo": "git@gitlab.example.com:twitter/typeahead-js.git",
+      "http_url_to_repo": "https://gitlab.example.com/twitter/typeahead-js.git",
+      "web_url": "https://gitlab.example.com/twitter/typeahead-js",
+      "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,
+      "container_registry_enabled": true,
+      "created_at": "2016-06-17T07:47:25.578Z",
+      "last_activity_at": "2016-06-17T07:47:25.881Z",
+      "shared_runners_enabled": true,
+      "creator_id": 1,
+      "namespace": {
+        "id": 4,
+        "name": "Twitter",
+        "path": "twitter",
+        "owner_id": null,
+        "created_at": "2016-06-17T07:47:24.216Z",
+        "updated_at": "2016-06-17T07:47:24.216Z",
+        "description": "Aliquid qui quis dignissimos distinctio ut commodi voluptas est.",
+        "avatar": {
+          "url": null
+        },
+        "share_with_group_lock": false,
+        "visibility_level": 20
+      },
+      "avatar_url": null,
+      "star_count": 0,
+      "forks_count": 0,
+      "open_issues_count": 3,
+      "public_builds": true,
+      "shared_with_groups": []
+    },
+    {
+      "id": 6,
+      "description": "Aspernatur omnis repudiandae qui voluptatibus eaque.",
+      "default_branch": "master",
+      "tag_list": [],
+      "public": false,
+      "archived": false,
+      "visibility_level": 10,
+      "ssh_url_to_repo": "git@gitlab.example.com:twitter/flight.git",
+      "http_url_to_repo": "https://gitlab.example.com/twitter/flight.git",
+      "web_url": "https://gitlab.example.com/twitter/flight",
+      "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,
+      "container_registry_enabled": true,
+      "created_at": "2016-06-17T07:47:24.661Z",
+      "last_activity_at": "2016-06-17T07:47:24.838Z",
+      "shared_runners_enabled": true,
+      "creator_id": 1,
+      "namespace": {
+        "id": 4,
+        "name": "Twitter",
+        "path": "twitter",
+        "owner_id": null,
+        "created_at": "2016-06-17T07:47:24.216Z",
+        "updated_at": "2016-06-17T07:47:24.216Z",
+        "description": "Aliquid qui quis dignissimos distinctio ut commodi voluptas est.",
+        "avatar": {
+          "url": null
+        },
+        "share_with_group_lock": false,
+        "visibility_level": 20
+      },
+      "avatar_url": null,
+      "star_count": 0,
+      "forks_count": 0,
+      "open_issues_count": 8,
+      "public_builds": true,
+      "shared_with_groups": []
+    }
+  ],
+  "shared_projects": [
+    {
+      "id": 8,
+      "description": "Velit eveniet provident fugiat saepe eligendi autem.",
+      "default_branch": "master",
+      "tag_list": [],
+      "public": false,
+      "archived": false,
+      "visibility_level": 0,
+      "ssh_url_to_repo": "git@gitlab.example.com:h5bp/html5-boilerplate.git",
+      "http_url_to_repo": "https://gitlab.example.com/h5bp/html5-boilerplate.git",
+      "web_url": "https://gitlab.example.com/h5bp/html5-boilerplate",
+      "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,
+      "container_registry_enabled": true,
+      "created_at": "2016-06-17T07:47:27.089Z",
+      "last_activity_at": "2016-06-17T07:47:27.310Z",
+      "shared_runners_enabled": true,
+      "creator_id": 1,
+      "namespace": {
+        "id": 5,
+        "name": "H5bp",
+        "path": "h5bp",
+        "owner_id": null,
+        "created_at": "2016-06-17T07:47:26.621Z",
+        "updated_at": "2016-06-17T07:47:26.621Z",
+        "description": "Id consequatur rem vel qui doloremque saepe.",
+        "avatar": {
+          "url": null
+        },
+        "share_with_group_lock": false,
+        "visibility_level": 20
+      },
+      "avatar_url": null,
+      "star_count": 0,
+      "forks_count": 0,
+      "open_issues_count": 4,
+      "public_builds": true,
+      "shared_with_groups": [
+        {
+          "group_id": 4,
+          "group_name": "Twitter",
+          "group_access_level": 30
+        },
+        {
+          "group_id": 3,
+          "group_name": "Gitlab Org",
+          "group_access_level": 10
+        }
+      ]
+    }
+  ]
+}
+```
+
+## New group
+
+Creates a new project group. Available only for users who can create groups.
+
+```
+POST /groups
+```
+
+Parameters:
+
+- `name` (required) - The name of the group
+- `path` (required) - The path of the group
+- `description` (optional) - The group's description
+- `visibility_level` (optional) - The group's visibility. 0 for private, 10 for internal, 20 for public.
+
+## Transfer project to group
+
+Transfer a project to the Group namespace. Available only for admin
+
+```
+POST  /groups/:id/projects/:project_id
+```
+
+Parameters:
+
+- `id` (required) - The ID or path of a group
+- `project_id` (required) - The ID of a project
+
+## Update group
+
+Updates the project group. Only available to group owners and administrators.
+
+```
+PUT /groups/:id
+```
+
+| Attribute | Type | Required | Description |
+| --------- | ---- | -------- | ----------- |
+| `id` | integer | yes | The ID of the group |
+| `name` | string | no | The name of the group |
+| `path` | string | no | The path of the group |
+| `description` | string | no | The description of the group |
+| `visibility_level` | integer | no | The visibility level of the group. 0 for private, 10 for internal, 20 for public. |
+
+```bash
+curl --request PUT --header "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" "https://gitlab.example.com/api/v3/groups/5?name=Experimental"
+
+```
+
+Example response:
+
+```json
+{
+  "id": 5,
+  "name": "Experimental",
+  "path": "h5bp",
+  "description": "foo",
+  "visibility_level": 10,
+  "avatar_url": null,
+  "web_url": "http://gitlab.example.com/groups/h5bp",
+  "projects": [
+    {
+      "id": 9,
+      "description": "foo",
+      "default_branch": "master",
+      "tag_list": [],
+      "public": false,
+      "archived": false,
+      "visibility_level": 10,
+      "ssh_url_to_repo": "git@gitlab.example.com/html5-boilerplate.git",
+      "http_url_to_repo": "http://gitlab.example.com/h5bp/html5-boilerplate.git",
+      "web_url": "http://gitlab.example.com/h5bp/html5-boilerplate",
+      "name": "Html5 Boilerplate",
+      "name_with_namespace": "Experimental / 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": true,
+      "created_at": "2016-04-05T21:40:50.169Z",
+      "last_activity_at": "2016-04-06T16:52:08.432Z",
+      "shared_runners_enabled": true,
+      "creator_id": 1,
+      "namespace": {
+        "id": 5,
+        "name": "Experimental",
+        "path": "h5bp",
+        "owner_id": null,
+        "created_at": "2016-04-05T21:40:49.152Z",
+        "updated_at": "2016-04-07T08:07:48.466Z",
+        "description": "foo",
+        "avatar": {
+          "url": null
+        },
+        "share_with_group_lock": false,
+        "visibility_level": 10
+      },
+      "avatar_url": null,
+      "star_count": 1,
+      "forks_count": 0,
+      "open_issues_count": 3,
+      "public_builds": true,
+      "shared_with_groups": []
+    }
+  ]
+}
+```
+
+## Remove group
+
+Removes group with all projects inside.
+
+```
+DELETE /groups/:id
+```
+
+Parameters:
+
+- `id` (required) - The ID or path of a user group
+
+## Search for group
+
+Get all groups that match your string in their name or path.
+
+```
+GET /groups?search=foobar
+```
+
+```json
+[
+  {
+    "id": 1,
+    "name": "Foobar Group",
+    "path": "foo-bar",
+    "description": "An interesting group"
+  }
+]
+```
+
+## Group members
+
+Please consult the [Group Members](members.md) documentation.
+
+## Namespaces in groups
+
+By default, groups only get 20 namespaces at a time because the API results are paginated.
+
+To get more (up to 100), pass the following as an argument to the API call:
+```
+/groups?per_page=100
+```
+
+And to switch pages add:
+```
+/groups?per_page=100&page=2
+```
diff --git a/doc/api/issues.md b/doc/api/issues.md
index 419fb8f85d8ace0393e8f123e7c1ce77735c34d5..a665645ad0ef618be6f51dc8d6b218891958693a 100644
--- a/doc/api/issues.md
+++ b/doc/api/issues.md
@@ -33,7 +33,7 @@ GET /issues?labels=foo,bar&state=opened
 | `sort`    | string  | no    | Return requests sorted in `asc` or `desc` order. Default is `desc`  |
 
 ```bash
-curl -H "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" https://gitlab.example.com/api/v3/issues
+curl --header "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" https://gitlab.example.com/api/v3/issues
 ```
 
 Example response:
@@ -110,7 +110,7 @@ GET /groups/:id/issues?milestone=1.0.0&state=opened
 
 
 ```bash
-curl -H "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" https://gitlab.example.com/api/v3/groups/4/issues
+curl --header "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" https://gitlab.example.com/api/v3/groups/4/issues
 ```
 
 Example response:
@@ -189,7 +189,7 @@ GET /projects/:id/issues?iid=42
 
 
 ```bash
-curl -H "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" https://gitlab.example.com/api/v3/projects/4/issues
+curl --header "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" https://gitlab.example.com/api/v3/projects/4/issues
 ```
 
 Example response:
@@ -254,7 +254,7 @@ GET /projects/:id/issues/:issue_id
 | `issue_id`| integer | yes   | The ID of a project's issue |
 
 ```bash
-curl -H "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" https://gitlab.example.com/api/v3/projects/4/issues/41
+curl --header "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" https://gitlab.example.com/api/v3/projects/4/issues/41
 ```
 
 Example response:
@@ -327,7 +327,7 @@ POST /projects/:id/issues
 | `due_date`      | string  | no   | Date time string in the format YEAR-MONTH-DAY, e.g. `2016-03-11` |
 
 ```bash
-curl -X POST -H "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" https://gitlab.example.com/api/v3/projects/4/issues?title=Issues%20with%20auth&labels=bug
+curl --request POST --header "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" https://gitlab.example.com/api/v3/projects/4/issues?title=Issues%20with%20auth&labels=bug
 ```
 
 Example response:
@@ -388,7 +388,7 @@ PUT /projects/:id/issues/:issue_id
 | `due_date`      | string  | no   | Date time string in the format YEAR-MONTH-DAY, e.g. `2016-03-11` |
 
 ```bash
-curl -X PUT -H "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" https://gitlab.example.com/api/v3/projects/4/issues/85?state_event=close
+curl --request PUT --header "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" https://gitlab.example.com/api/v3/projects/4/issues/85?state_event=close
 ```
 
 Example response:
@@ -438,7 +438,7 @@ DELETE /projects/:id/issues/:issue_id
 | `issue_id`      | integer | yes | The ID of a project's issue |
 
 ```bash
-curl -X DELETE -H "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" https://gitlab.example.com/api/v3/projects/4/issues/85
+curl --request DELETE --header "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" https://gitlab.example.com/api/v3/projects/4/issues/85
 ```
 
 ## Move an issue
@@ -463,7 +463,7 @@ POST /projects/:id/issues/:issue_id/move
 | `to_project_id` | integer | yes | The ID of the new project |
 
 ```bash
-curl -X POST -H "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" https://gitlab.example.com/api/v3/projects/4/issues/85/move
+curl --request POST --header "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" https://gitlab.example.com/api/v3/projects/4/issues/85/move
 ```
 
 Example response:
@@ -518,7 +518,7 @@ POST /projects/:id/issues/:issue_id/subscription
 | `issue_id` | integer | yes | The ID of a project's issue |
 
 ```bash
-curl -X POST -H "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" https://gitlab.example.com/api/v3/projects/5/issues/93/subscription
+curl --request POST --header "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" https://gitlab.example.com/api/v3/projects/5/issues/93/subscription
 ```
 
 Example response:
@@ -573,7 +573,7 @@ DELETE /projects/:id/issues/:issue_id/subscription
 | `issue_id` | integer | yes | The ID of a project's issue |
 
 ```bash
-curl -X DELETE -H "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" https://gitlab.example.com/api/v3/projects/5/issues/93/subscription
+curl --request DELETE --header "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" https://gitlab.example.com/api/v3/projects/5/issues/93/subscription
 ```
 
 Example response:
@@ -628,7 +628,7 @@ POST /projects/:id/issues/:issue_id/todo
 | `issue_id` | integer | yes | The ID of a project's issue |
 
 ```bash
-curl -X POST -H "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" https://gitlab.example.com/api/v3/projects/5/issues/93/todo
+curl --request POST --header "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" https://gitlab.example.com/api/v3/projects/5/issues/93/todo
 ```
 
 Example response:
diff --git a/doc/api/labels.md b/doc/api/labels.md
index a181c0f57a276486d6bf15c2967a1dfa5d5eb4ad..3653ccf304acf21220a37eeccd484bd4fddb40d8 100644
--- a/doc/api/labels.md
+++ b/doc/api/labels.md
@@ -13,7 +13,7 @@ GET /projects/:id/labels
 | `id`      | integer | yes      | The ID of the project |
 
 ```bash
-curl -H "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" https://gitlab.example.com/api/v3/projects/1/labels
+curl --header "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" https://gitlab.example.com/api/v3/projects/1/labels
 ```
 
 Example response:
@@ -82,7 +82,7 @@ POST /projects/:id/labels
 | `description` | string  | no       | The description of the label |
 
 ```bash
-curl --data "name=feature&color=#5843AD" -H "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" "https://gitlab.example.com/api/v3/projects/1/labels"
+curl --data "name=feature&color=#5843AD" --header "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" "https://gitlab.example.com/api/v3/projects/1/labels"
 ```
 
 Example response:
@@ -113,7 +113,7 @@ DELETE /projects/:id/labels
 | `name`    | string  | yes      | The name of the label |
 
 ```bash
-curl -X DELETE -H "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" "https://gitlab.example.com/api/v3/projects/1/labels?name=bug"
+curl --request DELETE --header "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" "https://gitlab.example.com/api/v3/projects/1/labels?name=bug"
 ```
 
 Example response:
@@ -153,7 +153,7 @@ PUT /projects/:id/labels
 | `description`   | string  | no                                | The new description of the label |
 
 ```bash
-curl -X PUT --data "name=documentation&new_name=docs&color=#8E44AD&description=Documentation" -H "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" "https://gitlab.example.com/api/v3/projects/1/labels"
+curl --request PUT --data "name=documentation&new_name=docs&color=#8E44AD&description=Documentation" --header "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" "https://gitlab.example.com/api/v3/projects/1/labels"
 ```
 
 Example response:
@@ -184,7 +184,7 @@ POST /projects/:id/labels/:label_id/subscription
 | `label_id` | integer or string | yes      | The ID or title of a project's label |
 
 ```bash
-curl -X POST -H "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" https://gitlab.example.com/api/v3/projects/5/labels/1/subscription
+curl --request POST --header "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" https://gitlab.example.com/api/v3/projects/5/labels/1/subscription
 ```
 
 Example response:
@@ -219,7 +219,7 @@ DELETE /projects/:id/labels/:label_id/subscription
 | `label_id` | integer or string | yes      | The ID or title of a project's label |
 
 ```bash
-curl -X DELETE -H "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" https://gitlab.example.com/api/v3/projects/5/labels/1/subscription
+curl --request DELETE --header "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" https://gitlab.example.com/api/v3/projects/5/labels/1/subscription
 ```
 
 Example response:
diff --git a/doc/api/licenses.md b/doc/api/licenses.md
index 855b0eab56fe265fdad31585f8ed8e4dade28bd0..ed26d1fb7fbf777c843948148a57abc6fa732d37 100644
--- a/doc/api/licenses.md
+++ b/doc/api/licenses.md
@@ -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 -H "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/licenses/mit?project=My+Cool+Project
 ```
 
 Example response:
diff --git a/doc/api/members.md b/doc/api/members.md
new file mode 100644
index 0000000000000000000000000000000000000000..fd6d728dad24b083b08a7461860cbed8419ef7b2
--- /dev/null
+++ b/doc/api/members.md
@@ -0,0 +1,185 @@
+# Group and project members
+
+**Valid access levels**
+
+The access levels are defined in the `Gitlab::Access` module. Currently, these levels are recognized:
+
+```
+10 => Guest access
+20 => Reporter access
+30 => Developer access
+40 => Master access
+50 => Owner access # Only valid for groups
+```
+
+## List all members of a group or project
+
+Gets a list of group or project members viewable by the authenticated user.
+
+Returns `200` if the request succeeds.
+
+```
+GET /groups/:id/members
+GET /projects/:id/members
+```
+
+| Attribute | Type | Required | Description |
+| --------- | ---- | -------- | ----------- |
+| `id`      | integer/string | yes | The group/project ID or path |
+| `query`   | string | no     | A query string to search for members |
+
+```bash
+curl --header "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" https://gitlab.example.com/api/v3/groups/:id/members
+curl --header "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" https://gitlab.example.com/api/v3/projects/:id/members
+```
+
+Example response:
+
+```json
+[
+  {
+    "id": 1,
+    "username": "raymond_smith",
+    "name": "Raymond Smith",
+    "state": "active",
+    "created_at": "2012-10-22T14:13:35Z",
+    "access_level": 30
+  },
+  {
+    "id": 2,
+    "username": "john_doe",
+    "name": "John Doe",
+    "state": "active",
+    "created_at": "2012-10-22T14:13:35Z",
+    "access_level": 30
+  }
+]
+```
+
+## Get a member of a group or project
+
+Gets a member of a group or project.
+
+Returns `200` if the request succeeds.
+
+```
+GET /groups/:id/members/:user_id
+GET /projects/:id/members/:user_id
+```
+
+| Attribute | Type | Required | Description |
+| --------- | ---- | -------- | ----------- |
+| `id`      | integer/string | yes | The group/project ID or path |
+| `user_id` | integer | yes   | The user ID of the member |
+
+```bash
+curl --header "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" https://gitlab.example.com/api/v3/groups/:id/members/:user_id
+curl --header "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" https://gitlab.example.com/api/v3/projects/:id/members/:user_id
+```
+
+Example response:
+
+```json
+{
+  "id": 1,
+  "username": "raymond_smith",
+  "name": "Raymond Smith",
+  "state": "active",
+  "created_at": "2012-10-22T14:13:35Z",
+  "access_level": 30,
+  "expires_at": null
+}
+```
+
+## Add a member to a group or project
+
+Adds a member to a group or project.
+
+Returns `201` if the request succeeds.
+
+```
+POST /groups/:id/members
+POST /projects/:id/members
+```
+
+| Attribute | Type | Required | Description |
+| --------- | ---- | -------- | ----------- |
+| `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
+```
+
+Example response:
+
+```json
+{
+  "id": 1,
+  "username": "raymond_smith",
+  "name": "Raymond Smith",
+  "state": "active",
+  "created_at": "2012-10-22T14:13:35Z",
+  "access_level": 30
+}
+```
+
+## Edit a member of a group or project
+
+Updates a member of a group or project.
+
+Returns `200` if the request succeeds.
+
+```
+PUT /groups/:id/members/:user_id
+PUT /projects/:id/members/:user_id
+```
+
+| Attribute | Type | Required | Description |
+| --------- | ---- | -------- | ----------- |
+| `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
+curl --request PUT --header "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" https://gitlab.example.com/api/v3/projects/:id/members/:user_id?access_level=40
+```
+
+Example response:
+
+```json
+{
+  "id": 1,
+  "username": "raymond_smith",
+  "name": "Raymond Smith",
+  "state": "active",
+  "created_at": "2012-10-22T14:13:35Z",
+  "access_level": 40
+}
+```
+
+## Remove a member from a group or project
+
+Removes a user from a group or project.
+
+Returns `200` if the request succeeds.
+
+```
+DELETE /groups/:id/members/:user_id
+DELETE /projects/:id/members/:user_id
+```
+
+| Attribute | Type | Required | Description |
+| --------- | ---- | -------- | ----------- |
+| `id`      | integer/string | yes | The group/project ID or path |
+| `user_id` | integer | yes   | The user ID of the member |
+
+```bash
+curl --request DELETE --header "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" https://gitlab.example.com/api/v3/groups/:id/members/:user_id
+curl --request DELETE --header "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" https://gitlab.example.com/api/v3/projects/:id/members/:user_id
+```
diff --git a/doc/api/merge_requests.md b/doc/api/merge_requests.md
index e00882e6d5db225c9f44d5768cb14226639554e9..3e88a7589365942c256c13bbbcaf4276ea41a25e 100644
--- a/doc/api/merge_requests.md
+++ b/doc/api/merge_requests.md
@@ -418,7 +418,7 @@ DELETE /projects/:id/merge_requests/:merge_request_id
 | `merge_request_id` | integer | yes | The ID of a project's merge request |
 
 ```bash
-curl -X DELETE -H "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" https://gitlab.example.com/api/v3/projects/4/merge_request/85
+curl --request DELETE --header "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" https://gitlab.example.com/api/v3/projects/4/merge_request/85
 ```
 
 ## Accept MR
@@ -587,7 +587,7 @@ GET /projects/:id/merge_requests/:merge_request_id/closes_issues
 | `merge_request_id` | integer | yes   | The ID of the merge request |
 
 ```bash
-curl -H "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" https://gitlab.example.com/api/v3/projects/76/merge_requests/1/closes_issues
+curl --header "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" https://gitlab.example.com/api/v3/projects/76/merge_requests/1/closes_issues
 ```
 
 Example response when the GitLab issue tracker is used:
@@ -665,7 +665,7 @@ POST /projects/:id/merge_requests/:merge_request_id/subscription
 | `merge_request_id` | integer | yes   | The ID of the merge request |
 
 ```bash
-curl -X POST -H "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" https://gitlab.example.com/api/v3/projects/5/merge_requests/17/subscription
+curl --request POST --header "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" https://gitlab.example.com/api/v3/projects/5/merge_requests/17/subscription
 ```
 
 Example response:
@@ -739,7 +739,7 @@ DELETE /projects/:id/merge_requests/:merge_request_id/subscription
 | `merge_request_id` | integer | yes   | The ID of the merge request |
 
 ```bash
-curl -X DELETE -H "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" https://gitlab.example.com/api/v3/projects/5/merge_requests/17/subscription
+curl --request DELETE --header "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" https://gitlab.example.com/api/v3/projects/5/merge_requests/17/subscription
 ```
 
 Example response:
@@ -812,7 +812,7 @@ POST /projects/:id/merge_requests/:merge_request_id/todo
 | `merge_request_id` | integer | yes   | The ID of the merge request |
 
 ```bash
-curl -X POST -H "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" https://gitlab.example.com/api/v3/projects/5/merge_requests/27/todo
+curl --request POST --header "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" https://gitlab.example.com/api/v3/projects/5/merge_requests/27/todo
 ```
 
 Example response:
diff --git a/doc/api/milestones.md b/doc/api/milestones.md
index e4202025f8022a2cad6dc6e1d701aab5a44dcdf4..ae7d22a4be554dc0dffd5feab4bde8466efd2f6e 100644
--- a/doc/api/milestones.md
+++ b/doc/api/milestones.md
@@ -20,7 +20,7 @@ Parameters:
 | `state` | string | optional | Return  only `active` or `closed` milestones` |
 
 ```bash
-curl -H "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" https://gitlab.example.com/api/v3/projects/5/milestones
+curl --header "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" https://gitlab.example.com/api/v3/projects/5/milestones
 ```
 
 Example Response:
diff --git a/doc/api/namespaces.md b/doc/api/namespaces.md
index 42d9ce3d3915d866ece9f536d38cdb9ab55e36a8..88cd407d792b3e3ecfd334e698a173e8f8d4dbfe 100644
--- a/doc/api/namespaces.md
+++ b/doc/api/namespaces.md
@@ -19,7 +19,7 @@ GET /namespaces
 Example request:
 
 ```bash
-curl -H "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" https://gitlab.example.com/api/v3/namespaces
+curl --header "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" https://gitlab.example.com/api/v3/namespaces
 ```
 
 Example response:
@@ -54,7 +54,7 @@ GET /namespaces?search=foobar
 Example request:
 
 ```bash
-curl -H "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" https://gitlab.example.com/api/v3/namespaces?search=twitter
+curl --header "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" https://gitlab.example.com/api/v3/namespaces?search=twitter
 ```
 
 Example response:
diff --git a/doc/api/notes.md b/doc/api/notes.md
index 7aa1c2155bfe8d5c2c2b5f30ffd0002ac71115a4..85d140d06acfe5818232df1624c7f2802238bd71 100644
--- a/doc/api/notes.md
+++ b/doc/api/notes.md
@@ -124,7 +124,7 @@ Parameters:
 | `note_id` | integer | yes | The ID of a note |
 
 ```bash
-curl -X DELETE -H "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" https://gitlab.example.com/api/v3/projects/5/issues/11/notes/636
+curl --request DELETE --header "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" https://gitlab.example.com/api/v3/projects/5/issues/11/notes/636
 ```
 
 Example Response:
@@ -248,7 +248,7 @@ Parameters:
 | `note_id` | integer | yes | The ID of a note |
 
 ```bash
-curl -X DELETE -H "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" https://gitlab.example.com/api/v3/projects/5/snippets/52/notes/1659
+curl --request DELETE --header "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" https://gitlab.example.com/api/v3/projects/5/snippets/52/notes/1659
 ```
 
 Example Response:
@@ -376,7 +376,7 @@ Parameters:
 | `note_id` | integer | yes | The ID of a note |
 
 ```bash
-curl -X DELETE -H "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" https://gitlab.example.com/api/v3/projects/5/merge_requests/7/notes/1602
+curl --request DELETE --header "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" https://gitlab.example.com/api/v3/projects/5/merge_requests/7/notes/1602
 ```
 
 Example Response:
diff --git a/doc/api/oauth2.md b/doc/api/oauth2.md
index 7ce89adc98b763ccaeac2e5484959f4b45054f8d..0b0fc39ec7e22c37bd9762353da7a2f733f63143 100644
--- a/doc/api/oauth2.md
+++ b/doc/api/oauth2.md
@@ -1,38 +1,59 @@
 # GitLab as an OAuth2 client
 
-This document is about using other OAuth authentication service providers to sign into 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).
+This document covers using the OAuth2 protocol to access GitLab.
 
-OAuth2 is a protocol that enables us to authenticate a user without requiring them to give their password. 
+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).
 
-Before using the OAuth2 you should create an application in user's account. Each application gets a unique App ID and App Secret parameters. You should not share these.
+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)
 
 ## Web Application Flow
 
-This flow is using for authentication from third-party web sites and is probably used the most. 
-It basically consists of an exchange of an authorization token for an access token. For more detailed info, check out the [RFC spec here](http://tools.ietf.org/html/rfc6749#section-4.1)
+This is the most common type of flow and is used by server-side clients that wish to access GitLab on a user's behalf.
+
+>**Note:**
+This flow **should not** be used for client-side clients as you would leak your `client_secret`. Client-side clients should use the Implicit Grant (which is currently unsupported).
 
-This flow consists from 3 steps.
+For more detailed information, check out the [RFC spec](http://tools.ietf.org/html/rfc6749#section-4.1)
+
+In the following sections you will be introduced to the three steps needed for this flow.
 
 ### 1. Registering the client
 
-Create an application in user's account profile.
+First, you should create an application (`/profile/applications`) in your user's account.
+Each application gets a unique App ID and App Secret parameters. 
+
+>**Note:**
+**You should not share/leak your App ID or App Secret.**
 
 ### 2. Requesting authorization
 
-To request the authorization token, you should visit the `/oauth/authorize` endpoint. You can do that by visiting manually the URL:
+To request the authorization code, you should redirect the user to the `/oauth/authorize` endpoint:
+
+```
+https://gitlab.example.com/oauth/authorize?client_id=APP_ID&redirect_uri=REDIRECT_URI&response_type=code&state=your_unique_state_hash
+```
+
+This will ask the user to approve the applications access to their account and then redirect back to the `REDIRECT_URI` you provided.
+
+The redirect will include the GET `code` parameter, for example:
 
 ```
-http://localhost:3000/oauth/authorize?client_id=APP_ID&redirect_uri=REDIRECT_URI&response_type=code
+http://myapp.com/oauth/redirect?code=1234567890&state=your_unique_state_hash
 ```
 
-Where REDIRECT_URI is the URL in your app where users will be sent after authorization. 	
+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! 
 
 ### 3. Requesting the access token
 
-To request the access token, you should use the returned code and exchange it for an access token. To do that you can use any HTTP client. In this case, I used rest-client:
+Once you have the authorization code you can request an `access_token` using the code, to do that you can use any HTTP client. In the following example, we are using Ruby's `rest-client`:
 
 ```
 parameters = 'client_id=APP_ID&client_secret=APP_SECRET&code=RETURNED_CODE&grant_type=authorization_code&redirect_uri=REDIRECT_URI'
@@ -46,6 +67,8 @@ RestClient.post 'http://localhost:3000/oauth/token', parameters
  "refresh_token": "8257e65c97202ed1726cf9571600918f3bffb2544b26e00a61df9897668c33a1"
 }
 ```
+>**Note:**
+The `redirect_uri` must match the `redirect_uri` used in the original authorization request.
 
 You can now make requests to the API with the access token returned.
 
@@ -60,14 +83,14 @@ GET https://localhost:3000/api/v3/user?access_token=OAUTH-TOKEN
 Or you can put the token to the Authorization header:
 
 ```
-curl -H "Authorization: Bearer OAUTH-TOKEN" https://localhost:3000/api/v3/user
+curl --header "Authorization: Bearer OAUTH-TOKEN" https://localhost:3000/api/v3/user
 ```
 
 ## Resource Owner Password Credentials
 
 ## 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.
 
 ---
@@ -77,6 +100,9 @@ The credentials should only be used when there is a high degree of trust between
 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).
 
+>**Important:**
+Never store the users credentials and only use this grant type when your client is deployed to a trusted environment, in 99% of cases [personal access tokens] are a better choice.
+
 Even though this grant type requires direct client access to the resource owner credentials, the resource owner credentials are used
 for a single request and are exchanged for an access token.  This grant type can eliminate the need for the client to store the
 resource owner credentials for future use, by exchanging the credentials with a long-lived access token or refresh token.
diff --git a/doc/api/pipelines.md b/doc/api/pipelines.md
new file mode 100644
index 0000000000000000000000000000000000000000..847408a7f617dec115b5f08de5534f6b43427032
--- /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/u/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/u/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/u/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/u/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/u/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/projects.md b/doc/api/projects.md
index 0ba0bffb4ac38f6b692326a5d8c561631bfaa774..37d97b2db44691b66a02b54b2aa090c7bd9e92ab 100644
--- a/doc/api/projects.md
+++ b/doc/api/projects.md
@@ -529,7 +529,7 @@ POST /projects/:id/star
 | `id`      | integer | yes | The ID of the project |
 
 ```bash
-curl -X POST -H "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" "https://gitlab.example.com/api/v3/projects/5/star"
+curl --request POST --header "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" "https://gitlab.example.com/api/v3/projects/5/star"
 ```
 
 Example response:
@@ -595,7 +595,7 @@ DELETE /projects/:id/star
 | `id`      | integer | yes | The ID of the project |
 
 ```bash
-curl -X DELETE -H "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" "https://gitlab.example.com/api/v3/projects/5/star"
+curl --request DELETE --header "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" "https://gitlab.example.com/api/v3/projects/5/star"
 ```
 
 Example response:
@@ -665,7 +665,7 @@ POST /projects/:id/archive
 | `id`      | integer | yes | The ID of the project |
 
 ```bash
-curl -X POST -H "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" "https://gitlab.example.com/api/v3/projects/archive"
+curl --request POST --header "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" "https://gitlab.example.com/api/v3/projects/archive"
 ```
 
 Example response:
@@ -751,7 +751,7 @@ POST /projects/:id/unarchive
 | `id`      | integer | yes | The ID of the project |
 
 ```bash
-curl -X POST -H "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" "https://gitlab.example.com/api/v3/projects/unarchive"
+curl --request POST --header "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" "https://gitlab.example.com/api/v3/projects/unarchive"
 ```
 
 Example response:
@@ -858,95 +858,9 @@ Parameters:
 In Markdown contexts, the link is automatically expanded when the format in `markdown` is used.
 
 
-## Team members
+## Project members
 
-### List project team members
-
-Get a list of a project's team members.
-
-```
-GET /projects/:id/members
-```
-
-Parameters:
-
-- `id` (required) - The ID or NAMESPACE/PROJECT_NAME of a project
-- `query` (optional) - Query string to search for members
-
-### Get project team member
-
-Gets a project team member.
-
-```
-GET /projects/:id/members/:user_id
-```
-
-Parameters:
-
-- `id` (required) - The ID or NAMESPACE/PROJECT_NAME of a project
-- `user_id` (required) - The ID of a user
-
-```json
-{
-  "id": 1,
-  "username": "john_smith",
-  "email": "john@example.com",
-  "name": "John Smith",
-  "state": "active",
-  "created_at": "2012-05-23T08:00:58Z",
-  "access_level": 40
-}
-```
-
-### Add project team member
-
-Adds a user to a project team. This is an idempotent method and can be called multiple times
-with the same parameters. Adding team membership to a user that is already a member does not
-affect the existing membership.
-
-```
-POST /projects/:id/members
-```
-
-Parameters:
-
-- `id` (required) - The ID or NAMESPACE/PROJECT_NAME of a project
-- `user_id` (required) - The ID of a user to add
-- `access_level` (required) - Project access level
-
-### Edit project team member
-
-Updates a project team member to a specified access level.
-
-```
-PUT /projects/:id/members/:user_id
-```
-
-Parameters:
-
-- `id` (required) - The ID or NAMESPACE/PROJECT_NAME of a project
-- `user_id` (required) - The ID of a team member
-- `access_level` (required) - Project access level
-
-### Remove project team member
-
-Removes a user from a project team.
-
-```
-DELETE /projects/:id/members/:user_id
-```
-
-Parameters:
-
-- `id` (required) - The ID or NAMESPACE/PROJECT_NAME of a project
-- `user_id` (required) - The ID of a team member
-
-This method removes the project member if the user has the proper access rights to do so.
-It returns a status code 403 if the member does not have the proper rights to perform this action.
-In all other cases this method is idempotent and revoking team membership for a user who is not
-currently a team member is considered success.
-Please note that the returned JSON currently differs slightly. Thus you should not
-rely on the returned JSON structure.
+Please consult the [Project Members](members.md) documentation.
 
 ### Share project with group
 
diff --git a/doc/api/repository_files.md b/doc/api/repository_files.md
index 623063f357b5cdde9aab9237b3507475190cc25f..fc3af5544de021765f451124ae26faf8593f309d 100644
--- a/doc/api/repository_files.md
+++ b/doc/api/repository_files.md
@@ -12,6 +12,10 @@ Allows you to receive information about file in repository like name, size, cont
 GET /projects/:id/repository/files
 ```
 
+```bash
+curl --request GET --header 'PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK' 'https://gitlab.example.com/api/v3/projects/13083/repository/files?file_path=app/models/key.rb&ref=master'
+```
+
 Example response:
 
 ```json
@@ -39,6 +43,10 @@ Parameters:
 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'
+```
+
 Example response:
 
 ```json
@@ -62,6 +70,10 @@ Parameters:
 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'
+```
+
 Example response:
 
 ```json
@@ -94,6 +106,10 @@ Currently gitlab-shell has a boolean return code, preventing GitLab from specify
 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'
+```
+
 Example response:
 
 ```json
diff --git a/doc/api/runners.md b/doc/api/runners.md
index ddfa298f79d00870c67dc81e8f4691bcf34ac3f2..28610762dca875a5708030e2977b8bf1ae913b01 100644
--- a/doc/api/runners.md
+++ b/doc/api/runners.md
@@ -18,7 +18,7 @@ GET /runners?scope=active
 | `scope`   | string  | no       | The scope of specific runners to show, one of: `active`, `paused`, `online`; showing all runners if none provided |
 
 ```
-curl -H "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" "https://gitlab.example.com/api/v3/runners"
+curl --header "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" "https://gitlab.example.com/api/v3/runners"
 ```
 
 Example response:
@@ -57,7 +57,7 @@ GET /runners/all?scope=online
 | `scope`   | string  | no       | The scope of runners to show, one of: `specific`, `shared`, `active`, `paused`, `online`; showing all runners if none provided |
 
 ```
-curl -H "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" "https://gitlab.example.com/api/v3/runners/all"
+curl --header "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" "https://gitlab.example.com/api/v3/runners/all"
 ```
 
 Example response:
@@ -108,7 +108,7 @@ GET /runners/:id
 | `id`      | integer | yes      | The ID of a runner  |
 
 ```
-curl -H "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" "https://gitlab.example.com/api/v3/runners/6"
+curl --header "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" "https://gitlab.example.com/api/v3/runners/6"
 ```
 
 Example response:
@@ -158,7 +158,7 @@ PUT /runners/:id
 | `tag_list`    | array   | no       | The list of tags for a runner; put array of tags, that should be finally assigned to a runner |
 
 ```
-curl -X PUT -H "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" "https://gitlab.example.com/api/v3/runners/6" -F "description=test-1-20150125-test" -F "tag_list=ruby,mysql,tag1,tag2"
+curl --request PUT --header "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" "https://gitlab.example.com/api/v3/runners/6" --form "description=test-1-20150125-test" --form "tag_list=ruby,mysql,tag1,tag2"
 ```
 
 Example response:
@@ -207,7 +207,7 @@ DELETE /runners/:id
 | `id`      | integer | yes      | The ID of a runner  |
 
 ```
-curl -X DELETE -H "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" "https://gitlab.example.com/api/v3/runners/6"
+curl --request DELETE --header "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" "https://gitlab.example.com/api/v3/runners/6"
 ```
 
 Example response:
@@ -237,7 +237,7 @@ GET /projects/:id/runners
 | `id`      | integer | yes      | The ID of a project |
 
 ```
-curl -H "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" "https://gitlab.example.com/api/v3/projects/9/runners"
+curl --header "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" "https://gitlab.example.com/api/v3/projects/9/runners"
 ```
 
 Example response:
@@ -275,7 +275,7 @@ POST /projects/:id/runners
 | `runner_id` | integer | yes      | The ID of a runner  |
 
 ```
-curl -X POST -H "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" "https://gitlab.example.com/api/v3/projects/9/runners" -F "runner_id=9"
+curl --request POST --header "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" "https://gitlab.example.com/api/v3/projects/9/runners" --form "runner_id=9"
 ```
 
 Example response:
@@ -306,7 +306,7 @@ DELETE /projects/:id/runners/:runner_id
 | `runner_id` | integer | yes      | The ID of a runner  |
 
 ```
-curl -X DELETE -H "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" "https://gitlab.example.com/api/v3/projects/9/runners/9"
+curl --request DELETE --header "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" "https://gitlab.example.com/api/v3/projects/9/runners/9"
 ```
 
 Example response:
diff --git a/doc/api/services.md b/doc/api/services.md
index f821a614047192490922ddf339449282d635245e..579fdc0c8c971066bf91bc3f7eef7003aac45755 100644
--- a/doc/api/services.md
+++ b/doc/api/services.md
@@ -355,7 +355,7 @@ PUT /projects/:id/services/gemnasium
 
 Parameters:
 
-- `api_key` (**required**) - Your personal API KEY on gemnasium.com 
+- `api_key` (**required**) - Your personal API KEY on gemnasium.com
 - `token` (**required**) - The project's slug on gemnasium.com
 
 ### Delete Gemnasium service
@@ -503,6 +503,7 @@ PUT /projects/:id/services/pivotaltracker
 Parameters:
 
 - `token` (**required**)
+- `restrict_to_branch` (optional) - Comma-separated list of branches which will be automatically inspected. Leave blank to include all branches.
 
 ### Delete PivotalTracker service
 
@@ -661,4 +662,3 @@ Get JetBrains TeamCity CI service settings for a project.
 ```
 GET /projects/:id/services/teamcity
 ```
-
diff --git a/doc/api/session.md b/doc/api/session.md
index 066a055702df222478f0996aeee6105ef81e6932..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.
 
 ---
@@ -21,7 +21,7 @@ POST /session
 | `password` | string  | yes     | The password of the user |
 
 ```bash
-curl -X POST "https://gitlab.example.com/api/v3/session?login=john_smith&password=strongpassw0rd"
+curl --request POST "https://gitlab.example.com/api/v3/session?login=john_smith&password=strongpassw0rd"
 ```
 
 Example response:
diff --git a/doc/api/settings.md b/doc/api/settings.md
index ea39b32561c38048a89d6857e2c6ac654b7d68d7..a76dad0ebd47b9d2d1b1f3d15167d3c1f9321f96 100644
--- a/doc/api/settings.md
+++ b/doc/api/settings.md
@@ -13,7 +13,7 @@ GET /application/settings
 ```
 
 ```bash
-curl -H "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" https://gitlab.example.com/api/v3/application/settings
+curl --header "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" https://gitlab.example.com/api/v3/application/settings
 ```
 
 Example response:
@@ -75,7 +75,7 @@ PUT /application/settings
 | `enabled_git_access_protocol` | string | no | Enabled protocols for Git access. Allowed values are: `ssh`, `http`, and `nil` to allow both protocols.
 
 ```bash
-curl -X PUT -H "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" https://gitlab.example.com/api/v3/application/settings?signup_enabled=false&default_project_visibility=1
+curl --request PUT --header "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" https://gitlab.example.com/api/v3/application/settings?signup_enabled=false&default_project_visibility=1
 ```
 
 Example response:
diff --git a/doc/api/sidekiq_metrics.md b/doc/api/sidekiq_metrics.md
index ebd131c94ca960d4bfca513265e2b71466f503a9..1ae732d40d6cf955f5dedd1db8374f25fc1ef41a 100644
--- a/doc/api/sidekiq_metrics.md
+++ b/doc/api/sidekiq_metrics.md
@@ -15,7 +15,7 @@ GET /sidekiq/queue_metrics
 ```
 
 ```bash
-curl -H "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" https://gitlab.example.com/api/v3/sidekiq/queue_metrics
+curl --header "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" https://gitlab.example.com/api/v3/sidekiq/queue_metrics
 ```
 
 Example response:
@@ -40,7 +40,7 @@ GET /sidekiq/process_metrics
 ```
 
 ```bash
-curl -H "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" https://gitlab.example.com/api/v3/sidekiq/process_metrics
+curl --header "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" https://gitlab.example.com/api/v3/sidekiq/process_metrics
 ```
 
 Example response:
@@ -82,7 +82,7 @@ GET /sidekiq/job_stats
 ```
 
 ```bash
-curl -H "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" https://gitlab.example.com/api/v3/sidekiq/job_stats
+curl --header "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" https://gitlab.example.com/api/v3/sidekiq/job_stats
 ```
 
 Example response:
@@ -106,7 +106,7 @@ GET /sidekiq/compound_metrics
 ```
 
 ```bash
-curl -H "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" https://gitlab.example.com/api/v3/sidekiq/compound_metrics
+curl --header "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" https://gitlab.example.com/api/v3/sidekiq/compound_metrics
 ```
 
 Example response:
diff --git a/doc/api/system_hooks.md b/doc/api/system_hooks.md
index dc036d7e27fce417c1cc27dab18f620e146cfee6..1802fae14feb8c87782b4eaac7a2c5c6607c3d99 100644
--- a/doc/api/system_hooks.md
+++ b/doc/api/system_hooks.md
@@ -20,7 +20,7 @@ GET /hooks
 Example request:
 
 ```bash
-curl -H "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" https://gitlab.example.com/api/v3/hooks
+curl --header "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" https://gitlab.example.com/api/v3/hooks
 ```
 
 Example response:
@@ -52,7 +52,7 @@ POST /hooks
 Example request:
 
 ```bash
-curl -X POST -H "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" "https://gitlab.example.com/api/v3/hooks?url=https://gitlab.example.com/hook"
+curl --request POST --header "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" "https://gitlab.example.com/api/v3/hooks?url=https://gitlab.example.com/hook"
 ```
 
 Example response:
@@ -80,7 +80,7 @@ GET /hooks/:id
 Example request:
 
 ```bash
-curl -H "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" https://gitlab.example.com/api/v3/hooks/2
+curl --header "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" https://gitlab.example.com/api/v3/hooks/2
 ```
 
 Example response:
@@ -117,7 +117,7 @@ DELETE /hooks/:id
 Example request:
 
 ```bash
-curl -X DELETE -H "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" https://gitlab.example.com/api/v3/hooks/2
+curl --request DELETE --header "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" https://gitlab.example.com/api/v3/hooks/2
 ```
 
 Example response:
diff --git a/doc/api/tags.md b/doc/api/tags.md
index ac9fac92f4cc24b30e7e3001269d10e90fb0487c..5405911745653f6d0ebe3edfe8231158f87d3dad 100644
--- a/doc/api/tags.md
+++ b/doc/api/tags.md
@@ -56,7 +56,7 @@ Parameters:
 | `tag_name` | string | yes | The name of the tag |
 
 ```bash
-curl -H "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" https://gitlab.example.com/api/v3/projects/5/repository/tags/v1.0.0
+curl --header "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" https://gitlab.example.com/api/v3/projects/5/repository/tags/v1.0.0
 ```
 
 Example Response:
diff --git a/doc/api/todos.md b/doc/api/todos.md
index 937c71de386db95859085bd95d42fbfdefdf3c3b..0cd644dfd2fe2bbd4e9e84cab7b990fa2e1fecda 100644
--- a/doc/api/todos.md
+++ b/doc/api/todos.md
@@ -1,6 +1,6 @@
 # Todos
 
-**Note:** This feature was [introduced][ce-3188] in GitLab 8.10
+> [Introduced][ce-3188] in GitLab 8.10.
 
 ## Get a list of todos
 
@@ -22,7 +22,7 @@ Parameters:
 | `type` | string | no | The type of a todo. Can be either `Issue` or `MergeRequest` |
 
 ```bash
-curl -H "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" https://gitlab.example.com/api/v3/todos
+curl --header "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" https://gitlab.example.com/api/v3/todos
 ```
 
 Example Response:
@@ -194,7 +194,7 @@ Parameters:
 | `id` | integer | yes | The ID of a todo |
 
 ```bash
-curl -X DELETE -H "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" https://gitlab.example.com/api/v3/todos/130
+curl --request DELETE --header "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" https://gitlab.example.com/api/v3/todos/130
 ```
 
 Example Response:
@@ -284,7 +284,7 @@ DELETE /todos
 ```
 
 ```bash
-curl -X DELETE -H "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" https://gitlab.example.com/api/v3/todos
+curl --request DELETE --header "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" https://gitlab.example.com/api/v3/todos
 ```
 
 Example Response:
diff --git a/doc/ci/README.md b/doc/ci/README.md
index 0833027f91d7789a8bbb5dc916dddea622f0c90a..10ce4ac8940024e6f0da941de49e5e7118152ac3 100644
--- a/doc/ci/README.md
+++ b/doc/ci/README.md
@@ -14,7 +14,7 @@
 - [Use variables in your `.gitlab-ci.yml`](variables/README.md)
 - [Use SSH keys in your build environment](ssh_keys/README.md)
 - [Trigger builds through the API](triggers/README.md)
-- [Build artifacts](build_artifacts/README.md)
+- [Build artifacts](../user/project/builds/artifacts.md)
 - [User permissions](../user/permissions.md#gitlab-ci)
 - [API](../api/ci/README.md)
 - [CI services (linked docker containers)](services/README.md)
diff --git a/doc/ci/build_artifacts/README.md b/doc/ci/build_artifacts/README.md
index 9553bb11e9d759b60f3017ba5a48874cf31e5029..05605f10fb4ec8a2079dedfb8adf9813c3d3cda4 100644
--- a/doc/ci/build_artifacts/README.md
+++ b/doc/ci/build_artifacts/README.md
@@ -1,175 +1,4 @@
-# Introduction to build artifacts
+This document was moved to:
 
-Artifacts is a list of files and directories which are attached to a build
-after it completes successfully.  This feature is enabled by default in all GitLab installations.
-
-_If you are searching for ways to use artifacts, jump to
-[Defining artifacts in `.gitlab-ci.yml`](#defining-artifacts-in-gitlab-ciyml)._
-
-Since GitLab 8.2 and [GitLab Runner] 0.7.0, build artifacts that are created by
-GitLab Runner are uploaded to GitLab and are downloadable as a single archive
-(`tar.gz`) using the GitLab UI.
-
-Starting from GitLab 8.4 and GitLab Runner 1.0, the artifacts archive format
-changed to `ZIP`, and it is now possible to browse its contents, with the added
-ability of downloading the files separately.
-
-**Note:**
-The artifacts browser will be available only for new artifacts that are sent
-to GitLab using GitLab Runner version 1.0 and up. It will not be possible to
-browse old artifacts already uploaded to GitLab.
-
-## Disabling build artifacts
-
-To disable artifacts site-wide, follow the steps below.
-
----
-
-**In Omnibus installations:**
-
-1. Edit `/etc/gitlab/gitlab.rb` and add the following line:
-
-    ```ruby
-    gitlab_rails['artifacts_enabled'] = false
-    ```
-
-1. Save the file and [reconfigure GitLab][] for the changes to take effect.
-
----
-
-**In installations from source:**
-
-1. Edit `/home/git/gitlab/config/gitlab.yml` and add or amend the following lines:
-
-    ```yaml
-    artifacts:
-      enabled: false
-    ```
-
-1. Save the file and [restart GitLab][] for the changes to take effect.
-
-## Defining artifacts in `.gitlab-ci.yml`
-
-A simple example of using the artifacts definition in `.gitlab-ci.yml` would be
-the following:
-
-```yaml
-pdf:
-  script: xelatex mycv.tex
-  artifacts:
-    paths:
-    - mycv.pdf
-```
-
-A job named `pdf` calls the `xelatex` command in order to build a pdf file from
-the latex source file `mycv.tex`. We then define the `artifacts` paths which in
-turn are defined with the `paths` keyword. All paths to files and directories
-are relative to the repository that was cloned during the build.
-
-For more examples on artifacts, follow the
-[separate artifacts yaml documentation](../yaml/README.md#artifacts).
-
-## Storing build artifacts
-
-After a successful build, GitLab Runner uploads an archive containing the build
-artifacts to GitLab.
-
-To change the location where the artifacts are stored, follow the steps below.
-
----
-
-**In Omnibus installations:**
-
-_The artifacts are stored by default in
-`/var/opt/gitlab/gitlab-rails/shared/artifacts`._
-
-1. To change the storage path for example to `/mnt/storage/artifacts`, edit
-   `/etc/gitlab/gitlab.rb` and add the following line:
-
-    ```ruby
-    gitlab_rails['artifacts_path'] = "/mnt/storage/artifacts"
-    ```
-
-1. Save the file and [reconfigure GitLab][] for the changes to take effect.
-
----
-
-**In installations from source:**
-
-_The artifacts are stored by default in
-`/home/git/gitlab/shared/artifacts`._
-
-1. To change the storage path for example to `/mnt/storage/artifacts`, edit
-   `/home/git/gitlab/config/gitlab.yml` and add or amend the following lines:
-
-    ```yaml
-    artifacts:
-      enabled: true
-      path: /mnt/storage/artifacts
-    ```
-
-1. Save the file and [restart GitLab][] for the changes to take effect.
-
-## Browsing build artifacts
-
-When GitLab receives an artifacts archive, an archive metadata file is also
-generated. This metadata file describes all the entries that are located in the
-artifacts archive itself. The metadata file is in a binary format, with
-additional GZIP compression.
-
-GitLab does not extract the artifacts archive in order to save space, memory
-and disk I/O. It instead inspects the metadata file which contains all the
-relevant information. This is especially important when there is a lot of
-artifacts, or an archive is a very large file.
-
----
-
-After a successful build, if you visit the build's specific page, you can see
-that there are two buttons.
-
-One is for downloading the artifacts archive and the other for browsing its
-contents.
-
-![Build artifacts browser button](img/build_artifacts_browser_button.png)
-
----
-
-The archive browser shows the name and the actual file size of each file in the
-archive. If your artifacts contained directories, then you are also able to
-browse inside them.
-
-Below you can see an image of three different file formats, as well as two
-directories.
-
-![Build artifacts browser](img/build_artifacts_browser.png)
-
----
-
-## Downloading build artifacts
-
-If you need to download the whole archive, there are buttons in various places
-inside GitLab that make that possible.
-
-1. While on the builds page, you can see the download icon for each build's
-   artifacts archive in the right corner
-
-1. While inside a specific build, you are presented with a download button
-   along with the one that browses the archive
-
-1. And finally, when browsing an archive you can see the download button at
-   the top right corner
-
----
-
-Note that GitLab does not extract the entire artifacts archive to send just a
-single file to the user.
-
-When clicking on a specific file, [GitLab Workhorse] extracts it from the
-archive and the download begins.
-
-This implementation saves space, memory and disk I/O.
-
-[gitlab runner]: https://gitlab.com/gitlab-org/gitlab-ci-multi-runner "GitLab Runner repository"
-[reconfigure gitlab]: ../../administration/restart_gitlab.md "How to restart GitLab documentation"
-[restart gitlab]: ../../administration/restart_gitlab.md "How to restart GitLab documentation"
-[gitlab workhorse]: https://gitlab.com/gitlab-org/gitlab-workhorse "GitLab Workhorse repository"
+- [user/project/builds/artifacts.md](../../user/project/builds/artifacts.md) - user guide
+- [administration/build_artifacts.md](../../administration/build_artifacts.md) - administrator guide
diff --git a/doc/ci/build_artifacts/img/build_artifacts_browser.png b/doc/ci/build_artifacts/img/build_artifacts_browser.png
deleted file mode 100644
index 59cf2b8746b1b3cd98ad5d0fd02ab69472e3a059..0000000000000000000000000000000000000000
Binary files a/doc/ci/build_artifacts/img/build_artifacts_browser.png and /dev/null differ
diff --git a/doc/ci/build_artifacts/img/build_artifacts_browser_button.png b/doc/ci/build_artifacts/img/build_artifacts_browser_button.png
deleted file mode 100644
index 7801c2e6fa6d23639b1d92a24653741368c7fb56..0000000000000000000000000000000000000000
Binary files a/doc/ci/build_artifacts/img/build_artifacts_browser_button.png and /dev/null differ
diff --git a/doc/ci/examples/php.md b/doc/ci/examples/php.md
index bfafcc44d66ef9e55822d97a71a991cbce97fe2f..175e9d79904e19d08f89c10babd9d0d515f1e1d0 100644
--- a/doc/ci/examples/php.md
+++ b/doc/ci/examples/php.md
@@ -49,7 +49,7 @@ apt-get update -yqq
 apt-get install git -yqq
 
 # Install phpunit, the tool that we will use for testing
-curl -Lo /usr/local/bin/phpunit https://phar.phpunit.de/phpunit.phar
+curl --location --output /usr/local/bin/phpunit https://phar.phpunit.de/phpunit.phar
 chmod +x /usr/local/bin/phpunit
 
 # Install mysql driver
@@ -235,7 +235,7 @@ cache:
 
 before_script:
 # Install composer dependencies
-- curl -sS https://getcomposer.org/installer | php
+- curl --silent --show-error https://getcomposer.org/installer | php
 - php composer.phar install
 
 ...
diff --git a/doc/ci/pipelines.md b/doc/ci/pipelines.md
index 48a9f99475954b5774d135ace728adbe26489d14..20cd88c8d20c957d123b9e5184069092012d4fff 100644
--- a/doc/ci/pipelines.md
+++ b/doc/ci/pipelines.md
@@ -32,6 +32,43 @@ project.
 
 Clicking on a pipeline will show the builds that were run for that pipeline.
 
+## 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)
+```
+
+The latest successful pipeline will be used to read the test coverage value.
+
 [builds]: #builds
 [jobs]: yaml/README.md#jobs
 [stages]: yaml/README.md#stages
diff --git a/doc/ci/quick_start/README.md b/doc/ci/quick_start/README.md
index 7fa1a478f344d8d12a1108f03639e9f3a0ac9b7e..c835ebc2d449a2eca852061f935d3976814c1ff4 100644
--- a/doc/ci/quick_start/README.md
+++ b/doc/ci/quick_start/README.md
@@ -218,22 +218,14 @@ project's settings.
 For more information read the
 [Builds emails service documentation](../../project_services/builds_emails.md).
 
-## Builds badge
-
-You can access a builds badge image using following link:
-
-```
-http://example.gitlab.com/namespace/project/badges/branch/build.svg
-```
-
-Awesome! You started using CI in GitLab!
-
 ## Examples
 
 Visit the [examples README][examples] to see a list of examples using GitLab
 CI with various languages.
 
-[runner-install]: https://gitlab.com/gitlab-org/gitlab-ci-multi-runner/tree/master#installation
+Awesome! You started using CI in GitLab!
+
+[runner-install]: https://gitlab.com/gitlab-org/gitlab-ci-multi-runner/tree/master#install-gitlab-runner
 [blog-ci]: https://about.gitlab.com/2015/05/06/why-were-replacing-gitlab-ci-jobs-with-gitlab-ci-dot-yml/
 [examples]: ../examples/README.md
 [ci]: https://about.gitlab.com/gitlab-ci/
diff --git a/doc/ci/triggers/README.md b/doc/ci/triggers/README.md
index 5c316510d0e7c4d99e77478bca2d176562e86530..6c6767fea0b14ec04031e1edb0414226aa58ca9c 100644
--- a/doc/ci/triggers/README.md
+++ b/doc/ci/triggers/README.md
@@ -1,6 +1,6 @@
 # Triggering Builds through the API
 
-_**Note:** This feature was [introduced][ci-229] in GitLab CE 7.14_
+> [Introduced][ci-229] in GitLab CE 7.14.
 
 Triggers can be used to force a rebuild of a specific branch, tag or commit,
 with an API call.
@@ -77,9 +77,9 @@ See the [Examples](#examples) section below for more details.
 Using cURL you can trigger a rebuild with minimal effort, for example:
 
 ```bash
-curl -X POST \
-     -F token=TOKEN \
-     -F ref=master \
+curl --request POST \
+     --form token=TOKEN \
+     --form ref=master \
      https://gitlab.example.com/api/v3/projects/9/trigger/builds
 ```
 
@@ -88,7 +88,7 @@ In this case, the project with ID `9` will get rebuilt on `master` branch.
 Alternatively, you can pass the `token` and `ref` arguments in the query string:
 
 ```bash
-curl -X POST \
+curl --request POST \
     "https://gitlab.example.com/api/v3/projects/9/trigger/builds?token=TOKEN&ref=master"
 ```
 
@@ -103,7 +103,7 @@ need to add in project's A `.gitlab-ci.yml`:
 build_docs:
   stage: deploy
   script:
-  - "curl -X POST -F token=TOKEN -F ref=master https://gitlab.example.com/api/v3/projects/9/trigger/builds"
+  - "curl --request POST --form token=TOKEN --form ref=master https://gitlab.example.com/api/v3/projects/9/trigger/builds"
   only:
   - tags
 ```
@@ -158,10 +158,10 @@ You can then trigger a rebuild while you pass the `UPLOAD_TO_S3` variable
 and the script of the `upload_package` job will run:
 
 ```bash
-curl -X POST \
-  -F token=TOKEN \
-  -F ref=master \
-  -F "variables[UPLOAD_TO_S3]=true" \
+curl --request POST \
+  --form token=TOKEN \
+  --form ref=master \
+  --form "variables[UPLOAD_TO_S3]=true" \
   https://gitlab.example.com/api/v3/projects/9/trigger/builds
 ```
 
@@ -172,7 +172,7 @@ in conjunction with cron. The example below triggers a build on the `master`
 branch of project with ID `9` every night at `00:30`:
 
 ```bash
-30 0 * * * curl -X POST -F token=TOKEN -F ref=master https://gitlab.example.com/api/v3/projects/9/trigger/builds
+30 0 * * * curl --request POST --form token=TOKEN --form ref=master https://gitlab.example.com/api/v3/projects/9/trigger/builds
 ```
 
 [ci-229]: https://gitlab.com/gitlab-org/gitlab-ci/merge_requests/229
diff --git a/doc/ci/yaml/README.md b/doc/ci/yaml/README.md
index 01d7108854398bbaf676d2026a587770216d22ea..e7850aa2c9d372d12df27e5bd50a15633f5499a0 100644
--- a/doc/ci/yaml/README.md
+++ b/doc/ci/yaml/README.md
@@ -353,7 +353,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 |
diff --git a/doc/container_registry/README.md b/doc/container_registry/README.md
index 55077197ff9606a79519139ef45bedbc42480d70..047a0b08406941bcea0002b90f9fd39437c5ea59 100644
--- a/doc/container_registry/README.md
+++ b/doc/container_registry/README.md
@@ -1,8 +1,7 @@
 # GitLab Container Registry
 
-> **Note:**
-This feature was [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.
+> [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
@@ -90,6 +89,10 @@ 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
diff --git a/doc/container_registry/img/mitmproxy-docker.png b/doc/container_registry/img/mitmproxy-docker.png
new file mode 100644
index 0000000000000000000000000000000000000000..4e3e37b413d8c8c8d15ee85c6d7057dfa5397e11
Binary files /dev/null and b/doc/container_registry/img/mitmproxy-docker.png differ
diff --git a/doc/container_registry/troubleshooting.md b/doc/container_registry/troubleshooting.md
new file mode 100644
index 0000000000000000000000000000000000000000..14c4a7d9a63ee99aafc3754dbb9c83378a76c86f
--- /dev/null
+++ b/doc/container_registry/troubleshooting.md
@@ -0,0 +1,141 @@
+# 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.
diff --git a/doc/development/README.md b/doc/development/README.md
index c5d5af438644796847424aa7a5e279f84f61bf6d..57f37da6f809307e262e39dc16da2d6c9a55247b 100644
--- a/doc/development/README.md
+++ b/doc/development/README.md
@@ -1,18 +1,41 @@
 # Development
 
+## Outside of docs
+
+- [CONTRIBUTING.md](https://gitlab.com/gitlab-org/gitlab-ce/blob/master/CONTRIBUTING.md) main contributing guide
+- [PROCESS.md](https://gitlab.com/gitlab-org/gitlab-ce/blob/master/PROCESS.md) contributing process
+- [GitLab Development Kit (GDK)](https://gitlab.com/gitlab-org/gitlab-development-kit/blob/master/doc/howto/README.md) to install a development version
+
+## Styleguides
+
+- [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
+
+## Process
+
+- [Code review guidelines](code_review.md) for reviewing code and having code reviewed.
+
+## Backend howtos
+
 - [Architecture](architecture.md) of GitLab
 - [CI setup](ci_setup.md) for testing GitLab
-- [Code review guidelines](code_review.md) for reviewing code and having code
-  reviewed.
 - [Gotchas](gotchas.md) to avoid
 - [How to dump production data to staging](db_dump.md)
 - [Instrumentation](instrumentation.md)
-- [Licensing](licensing.md) for ensuring license compliance
-- [Migration Style Guide](migration_style_guide.md) for creating safe migrations
 - [Performance guidelines](performance.md)
 - [Rake tasks](rake_tasks.md) for development
 - [Shell commands](shell_commands.md) in the GitLab codebase
 - [Sidekiq debugging](sidekiq_debugging.md)
-- [SQL guidelines](sql.md) for SQL guidelines
-- [Testing standards and style guidelines](testing.md)
-- [UI guide](ui_guide.md) for building GitLab with existing css styles and elements
+
+## Databases
+
+- [What requires downtime?](what_requires_downtime.md)
+- [Adding database indexes](adding_database_indexes.md)
+
+## Compliance
+
+- [Licensing](licensing.md) for ensuring license compliance
diff --git a/doc/development/adding_database_indexes.md b/doc/development/adding_database_indexes.md
new file mode 100644
index 0000000000000000000000000000000000000000..ea6f14da3b93d42dbfde0f2f52aaecb36ae1aed6
--- /dev/null
+++ b/doc/development/adding_database_indexes.md
@@ -0,0 +1,123 @@
+# Adding Database Indexes
+
+Indexes can be used to speed up database queries, but when should you add a new
+index? Traditionally the answer to this question has been to add an index for
+every column used for filtering or joining data. For example, consider the
+following query:
+
+```sql
+SELECT *
+FROM projects
+WHERE user_id = 2;
+```
+
+Here we are filtering by the `user_id` column and as such a developer may decide
+to index this column.
+
+While in certain cases indexing columns using the above approach may make sense
+it can actually have a negative impact. Whenever you write data to a table any
+existing indexes need to be updated. The more indexes there are the slower this
+can potentially become. Indexes can also take up quite some disk space depending
+on the amount of data indexed and the index type. For example, PostgreSQL offers
+"GIN" indexes which can be used to index certain data types that can not be
+indexed by regular btree indexes. These indexes however generally take up more
+data and are slower to update compared to btree indexes.
+
+Because of all this one should not blindly add a new index for every column used
+to filter data by. Instead one should ask themselves the following questions:
+
+1. Can I write my query in such a way that it re-uses as many existing indexes
+   as possible?
+2. Is the data going to be large enough that using an index will actually be
+   faster than just iterating over the rows in the table?
+3. Is the overhead of maintaining the index worth the reduction in query
+   timings?
+
+We'll explore every question in detail below.
+
+## Re-using Queries
+
+The first step is to make sure your query re-uses as many existing indexes as
+possible. For example, consider the following query:
+
+```sql
+SELECT *
+FROM todos
+WHERE user_id = 123
+AND state = 'open';
+```
+
+Now imagine we already have an index on the `user_id` column but not on the
+`state` column. One may think this query will perform badly due to `state` being
+unindexed. In reality the query may perform just fine given the index on
+`user_id` can filter out enough rows.
+
+The best way to determine if indexes are re-used is to run your query using
+`EXPLAIN ANALYZE`. Depending on any extra tables that may be joined and
+other columns being used for filtering you may find an extra index is not going
+to make much (if any) difference. On the other hand you may determine that the
+index _may_ make a difference.
+
+In short:
+
+1. Try to write your query in such a way that it re-uses as many existing
+   indexes as possible.
+2. Run the query using `EXPLAIN ANALYZE` and study the output to find the most
+   ideal query.
+
+## Data Size
+
+A database may decide not to use an index despite it existing in case a regular
+sequence scan (= simply iterating over all existing rows) is faster. This is
+especially the case for small tables.
+
+If a table is expected to grow in size and you expect your query has to filter
+out a lot of rows you may want to consider adding an index. If the table size is
+very small (e.g. only a handful of rows) or any existing indexes filter out
+enough rows you may _not_ want to add a new index.
+
+## Maintenance Overhead
+
+Indexes have to be updated on every table write. In case of PostgreSQL _all_
+existing indexes will be updated whenever data is written to a table. As a
+result of this having many indexes on the same table will slow down writes.
+
+Because of this one should ask themselves: is the reduction in query performance
+worth the overhead of maintaining an extra index?
+
+If adding an index reduces SELECT timings by 5 milliseconds but increases
+INSERT/UPDATE/DELETE timings by 10 milliseconds then the index may not be worth
+it. On the other hand, if SELECT timings are reduced but INSERT/UPDATE/DELETE
+timings are not affected you may want to add the index after all.
+
+## Finding Unused Indexes
+
+To see which indexes are unused you can run the following query:
+
+```sql
+SELECT relname as table_name, indexrelname as index_name, idx_scan, idx_tup_read, idx_tup_fetch, pg_size_pretty(pg_relation_size(indexrelname::regclass))
+FROM pg_stat_all_indexes
+WHERE schemaname = 'public'
+AND idx_scan = 0
+AND idx_tup_read = 0
+AND idx_tup_fetch = 0
+ORDER BY pg_relation_size(indexrelname::regclass) desc;
+```
+
+This query outputs a list containing all indexes that are never used and sorts
+them by indexes sizes in descending order.  This query can be useful to
+determine if any previously indexes are useful after all. More information on
+the meaning of the various columns can be found at
+<https://www.postgresql.org/docs/current/static/monitoring-stats.html>.
+
+Because the output of this query relies on the actual usage of your database it
+may be affected by factors such as (but not limited to):
+
+* Certain queries never being executed, thus not being able to use certain
+  indexes.
+* Certain tables having little data, resulting in PostgreSQL using sequence
+  scans instead of index scans.
+
+In other words, this data is only reliable for a frequently used database with
+plenty of data and with as many GitLab features enabled (and being used) as
+possible.
diff --git a/doc/development/doc_styleguide.md b/doc/development/doc_styleguide.md
index 3a3597bccaa01924157a618499df1cbe751b33a6..927a18724135411c679050b6a36f7cba28004b12 100644
--- a/doc/development/doc_styleguide.md
+++ b/doc/development/doc_styleguide.md
@@ -3,12 +3,64 @@
 This styleguide recommends best practices to improve documentation and to keep
 it organized and easy to find.
 
-## Naming
+## Location and naming of documents
 
-- When creating a new document and it has more than one word in its name,
-  make sure to use underscores instead of spaces or dashes (`-`). For example,
-  a proper naming would be `import_projects_from_github.md`. The same rule
-  applies to images.
+>**Note:**
+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.
+
+Having a structured document layout, we will be able to have meaningful URLs
+like `docs.gitlab.com/user/project/merge_requests.html`. With this pattern,
+you can immediately tell that you are navigating a user related documentation
+and is about the project and its merge requests.
+
+The table below shows what kind of documentation goes where.
+
+| Directory | What belongs here |
+| --------- | -------------- |
+| `doc/user/` | User related documentation. Anything that can be done within the GitLab UI goes here including `/admin`. |
+| `doc/administration/`  | Documentation that requires the user to have access to the server where GitLab is installed. The admin settings that can be accessed via GitLab's interface go under `doc/user/admin_area/`. |
+| `doc/api/` | API related documentation. |
+| `doc/development/` | Documentation related to the development of GitLab. Any styleguides should go here. |
+| `doc/legal/` | Legal documents about contributing to GitLab. |
+| `doc/install/`| Probably the most visited directory, since `installation.md` is there. Ideally this should go under `doc/administration/`, but it's best to leave it as-is in order to avoid confusion (still debated though). |
+| `doc/update/` | Same with `doc/install/`. Should be under `administration/`, but this is a well known location, better leave as-is, at least for now. |
+
+---
+
+**General rules:**
+
+1. The correct naming and location of a new document, is a combination
+   of the relative URL of the document in question and the GitLab Map design
+   that is used for UX purposes ([source][graffle], [image][gitlab-map]).
+1. When creating a new document and it has more than one word in its name,
+   make sure to use underscores instead of spaces or dashes (`-`). For example,
+   a proper naming would be `import_projects_from_github.md`. The same rule
+   applies to images.
+1. There are four main directories, `user`, `administration`, `api` and `development`.
+1. The `doc/user/` directory has five main subdirectories: `project/`, `group/`,
+   `profile/`, `dashboard/` and `admin_area/`.
+   1. `doc/user/project/` should contain all project related documentation.
+   1. `doc/user/group/` should contain all group related documentation.
+   1. `doc/user/profile/` should contain all profile related documentation.
+      Every page you would navigate under `/profile` should have its own document,
+      i.e. `account.md`, `applications.md`, `emails.md`, etc.
+   1. `doc/user/dashboard/` should contain all dashboard related documentation.
+   1. `doc/user/admin_area/` should contain all admin related documentation
+      describing what can be achieved by accessing GitLab's admin interface
+      (_not to be confused with `doc/administration` where server access is
+      required_).
+      1. Every category under `/admin/application_settings` should have its
+         own document located at `doc/user/admin_area/settings/`. For example,
+         the **Visibility and Access Controls** category should have a document
+         located at `doc/user/admin_area/settings/visibility_and_access_controls.md`.
+
+---
+
+If you are unsure where a document should live, you can ping `@axil` in your
+merge request.
 
 ## Text
 
@@ -103,15 +155,15 @@ 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: `>**Note:** This feature was 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:
-  `>**Note:** This feature was [introduced][ce-1242] in GitLab 8.3`, where
+  `> [Introduced][ce-1242] in GitLab 8.3.`, where
   the [link identifier](#links) is named after the repository (CE) and the MR
-  number
+  number.
 - If the feature is only in GitLab EE, don't forget to mention it, like:
-  `>**Note:** This feature was introduced in GitLab EE 8.3`. Otherwise, leave
-  this mention out
+  `> Introduced in GitLab EE 8.3.`. Otherwise, leave
+  this mention out.
 
 ## References
 
@@ -303,7 +355,7 @@ Below is a set of [cURL][] examples that you can use in the API documentation.
 Get the details of a group:
 
 ```bash
-curl -H "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" https://gitlab.example.com/api/v3/groups/gitlab-org
+curl --header "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" https://gitlab.example.com/api/v3/groups/gitlab-org
 ```
 
 #### cURL example with parameters passed in the URL
@@ -311,7 +363,7 @@ curl -H "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" https://gitlab.example.com/api/v3/
 Create a new project under the authenticated user's namespace:
 
 ```bash
-curl -X POST -H "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" "https://gitlab.example.com/api/v3/projects?name=foo"
+curl --request POST --header "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" "https://gitlab.example.com/api/v3/projects?name=foo"
 ```
 
 #### Post data using cURL's --data
@@ -321,7 +373,7 @@ cURL's `--data` option. The example below will create a new project `foo` under
 the authenticated user's namespace.
 
 ```bash
-curl --data "name=foo" -H "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" "https://gitlab.example.com/api/v3/projects"
+curl --data "name=foo" --header "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" "https://gitlab.example.com/api/v3/projects"
 ```
 
 #### Post data using JSON content
@@ -330,7 +382,7 @@ curl --data "name=foo" -H "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" "https://gitlab.
 and double quotes.
 
 ```bash
-curl -X POST -H "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" -H "Content-Type: application/json" --data '{"path": "my-group", "name": "My group"}' https://gitlab.example.com/api/v3/groups
+curl --request POST --header "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" --header "Content-Type: application/json" --data '{"path": "my-group", "name": "My group"}' https://gitlab.example.com/api/v3/groups
 ```
 
 #### Post data using form-data
@@ -339,7 +391,7 @@ Instead of using JSON or urlencode you can use multipart/form-data which
 properly handles data encoding:
 
 ```bash
-curl -X POST -H "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" -F "title=ssh-key" -F "key=ssh-rsa AAAAB3NzaC1yc2EA..." https://gitlab.example.com/api/v3/users/25/keys
+curl --request POST --header "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" --form "title=ssh-key" --form "key=ssh-rsa AAAAB3NzaC1yc2EA..." https://gitlab.example.com/api/v3/users/25/keys
 ```
 
 The above example is run by and administrator and will add an SSH public key
@@ -353,7 +405,7 @@ contains spaces in its title. Observe how spaces are escaped using the `%20`
 ASCII code.
 
 ```bash
-curl -X POST -H "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" "https://gitlab.example.com/api/v3/projects/42/issues?title=Hello%20Dude"
+curl --request POST --header "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" "https://gitlab.example.com/api/v3/projects/42/issues?title=Hello%20Dude"
 ```
 
 Use `%2F` for slashes (`/`).
@@ -365,10 +417,13 @@ restrict the sign-up e-mail domains of a GitLab instance to `*.example.com` and
 `example.net`, you would do something like this:
 
 ```bash
-curl -X PUT -H "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" -d "domain_whitelist[]=*.example.com" -d "domain_whitelist[]=example.net" https://gitlab.example.com/api/v3/application/settings
+curl --request PUT --header "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" --data "domain_whitelist[]=*.example.com" --data "domain_whitelist[]=example.net" https://gitlab.example.com/api/v3/application/settings
 ```
 
 [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"
 [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
+[gitlab-map]: https://gitlab.com/gitlab-org/gitlab-design/raw/master/production/resources/gitlab-map.png
diff --git a/doc/development/gotchas.md b/doc/development/gotchas.md
index 9d7fe7440d27505ee8eb0e638d6a3d976ebcdf64..159d5ce286db39ed0fe2151f76adf42646236df6 100644
--- a/doc/development/gotchas.md
+++ b/doc/development/gotchas.md
@@ -41,10 +41,10 @@ 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 in views
+## Don't use inline CoffeeScript/JavaScript in views
 
 Using the inline `:coffee` or `:coffeescript` Haml filters comes with a
-performance overhead.
+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)
 in an initializer._
@@ -52,6 +52,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/newlines_styleguide.md b/doc/development/newlines_styleguide.md
new file mode 100644
index 0000000000000000000000000000000000000000..e03adcaadea5d38ca247b9025bcdba0283ee0405
--- /dev/null
+++ b/doc/development/newlines_styleguide.md
@@ -0,0 +1,102 @@
+# Newlines styleguide
+
+This style guide recommends best practices for newlines in Ruby code.
+
+## Rule: separate code with newlines only when it makes sense from logic perspectice
+
+```ruby
+# bad
+def method
+  issue = Issue.new
+
+  issue.save
+  
+  render json: issue 
+end
+```
+
+```ruby
+# good
+def method
+  issue = Issue.new
+  issue.save
+  
+  render json: issue 
+end
+```
+
+## Rule: separate code and block with newlines
+
+### Newline before block
+
+```ruby
+# bad
+def method
+  issue = Issue.new
+  if issue.save
+    render json: issue
+  end
+end
+```
+
+```ruby
+# good
+def method
+  issue = Issue.new
+
+  if issue.save
+    render json: issue
+  end
+end
+```
+
+## Newline after block
+
+```ruby
+# bad
+def method
+  if issue.save
+    issue.send_email
+  end
+  render json: issue
+end
+```
+
+```ruby
+# good
+def method
+  if issue.save
+    issue.send_email
+  end
+
+  render json: issue
+end
+```
+
+### Exception: no need for newline when code block starts or ends right inside another code block
+
+```ruby
+# bad
+def method
+
+  if issue
+
+    if issue.valid?
+      issue.save
+    end
+
+  end
+
+end
+```
+
+```ruby
+# good
+def method
+  if issue
+    if issue.valid?
+      issue.save
+    end
+  end
+end
+```
diff --git a/doc/development/performance.md b/doc/development/performance.md
index fb37b3a889c73279a615decd94302749d3abc40f..7ff603e2c4a91aff658cf9acf1c71c052eae692e 100644
--- a/doc/development/performance.md
+++ b/doc/development/performance.md
@@ -15,8 +15,8 @@ The process of solving performance problems is roughly as follows:
 3. Add your findings based on the measurement period (screenshots of graphs,
    timings, etc) to the issue mentioned in step 1.
 4. Solve the problem.
-5. Create a merge request, assign the "performance" label and ping the right
-   people (e.g. [@yorickpeterse][yorickpeterse] and [@joshfng][joshfng]).
+5. Create a merge request, assign the "Performance" label and assign it to
+   [@yorickpeterse][yorickpeterse] for reviewing.
 6. Once a change has been deployed make sure to _again_ measure for at least 24
    hours to see if your changes have any impact on the production environment.
 7. Repeat until you're done.
@@ -36,8 +36,8 @@ graphs/dashboards.
 
 GitLab provides two built-in tools to aid the process of improving performance:
 
-* [Sherlock](doc/development/profiling.md#sherlock)
-* [GitLab Performance Monitoring](doc/monitoring/performance/monitoring.md)
+* [Sherlock](profiling.md#sherlock)
+* [GitLab Performance Monitoring](../monitoring/performance/monitoring.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
@@ -254,5 +254,4 @@ 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
-[joshfng]: https://gitlab.com/u/joshfng
 [anti-pattern]: https://en.wikipedia.org/wiki/Anti-pattern
diff --git a/doc/development/rake_tasks.md b/doc/development/rake_tasks.md
index 8852dbcb19eab4ca018df70b83a3fe5e207c5acf..a7175f3f87e708b836c626ddfe5e93214a7be3d2 100644
--- a/doc/development/rake_tasks.md
+++ b/doc/development/rake_tasks.md
@@ -14,11 +14,33 @@ Note: `db:setup` calls `db:seed` but this does nothing.
 
 ## Run tests
 
-This runs all test suites present in GitLab.
+In order to run the test you can use the following commands:
+- `rake spinach` to run the spinach suite
+- `rake spec` to run the rspec suite
+- `rake teaspoon` to run the teaspoon test suite
+- `rake gitlab:test` to run all the tests
 
-```
-bundle exec rake test
-```
+Note: Both `rake spinach` and `rake spec` takes significant time to pass. 
+Instead of running full test suite locally you can save a lot of time by running
+a single test or directory related to your changes. After you submit merge request 
+CI will run full test suite for you. Green CI status in the merge request means 
+full test suite is passed.  
+
+Note: You can't run `rspec .` since this will try to run all the `_spec.rb`
+files it can find, also the ones in `/tmp`
+
+To run a single test file you can use:
+
+- `bundle exec rspec spec/controllers/commit_controller_spec.rb` for a rspec test
+- `bundle exec spinach features/project/issues/milestones.feature` for a spinach test
+
+To run several tests inside one directory:
+
+- `bundle exec rspec spec/requests/api/` for the rspec tests if you want to test API only
+- `bundle exec spinach features/profile/` for the spinach tests if you want to test only profile pages
+
+If you want to use [Spring](https://github.com/rails/spring) set
+`ENABLE_SPRING=1` in your environment.
 
 ## Generate searchable docs for source code
 
diff --git a/doc/development/ui_guide.md b/doc/development/ui_guide.md
index 6525228801990c6bca46d8e6f95da0d0c6d352a6..2d1d504202cc93b7fa42e0bb39d4b1369dca4c85 100644
--- a/doc/development/ui_guide.md
+++ b/doc/development/ui_guide.md
@@ -15,11 +15,14 @@ repository and maintained by GitLab UX designers.
 ## Navigation
 
 GitLab's layout contains 2 sections: the left sidebar and the content. The left sidebar contains a static navigation menu.
-This menu will be visible regardless of what page you visit. The left sidebar also contains the GitLab logo
-and the current user's profile picture. The content section contains a header and the content itself.
-The header describes the current GitLab page and what navigation is
-available to user in this area. Depending on the area (project, group, profile setting) the header name and navigation may change. For example when user visits one of the
-project pages the header will contain a project name and navigation for that project. When the user visits a group page it will contain a group name and navigation related to this group.
+This menu will be visible regardless of what page you visit.
+The content section contains a header and the content itself. The header describes the current GitLab page and what navigation is
+available to the user in this area. Depending on the area (project, group, profile setting) the header name and navigation may change. For example, when the user visits one of the
+project pages the header will contain the project's name and navigation for that project. When the user visits a group page it will contain the group's name and navigation related to this group.
+
+You can see a visual representation of the navigation in GitLab in the GitLab Product Map, which is located in the [Design Repository](gitlab-map-graffle)
+along with [PDF](gitlab-map-pdf) and [PNG](gitlab-map-png) exports.
+
 
 ### Adding new tab to header navigation
 
@@ -47,6 +50,42 @@ information from database or file system
 * `rss` for rss/atom feed
 * `plus` for link or dropdown that lead to page where you create new object (For example new issue page)
 
+### SVGs
+
+When exporting SVGs, be sure to follow the following guidelines:
+
+1. Convert all strokes to outlines.
+2. Use pathfinder tools to combine overlapping paths and create compound paths.
+3. SVGs that are limited to one color should be exported without a fill color so the color can be set using CSS.
+4. Ensure that exported SVGs have been run through an [SVG cleaner](https://github.com/RazrFalcon/SVGCleaner) to remove unused elements and attributes.
+
+You can open your svg in a text editor to ensure that it is clean. 
+Incorrect files will look like this:
+
+```xml
+<?xml version="1.0" encoding="UTF-8" standalone="no"?>
+<svg width="16px" height="17px" viewBox="0 0 16 17" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink">
+    <!-- Generator: Sketch 3.7.2 (28276) - http://www.bohemiancoding.com/sketch -->
+    <title>Group</title>
+    <desc>Created with Sketch.</desc>
+    <defs></defs>
+    <g id="Page-1" stroke="none" stroke-width="1" fill="none" fill-rule="evenodd">
+        <g id="Group" fill="#7E7C7C">
+            <path d="M15.1111,1 L0.8891,1 C0.3981,1 0.0001,1.446 0.0001,1.996 L0.0001,15.945 C0.0001,16.495 0.3981,16.941 0.8891,16.941 L15.1111,16.941 C15.6021,16.941 16.0001,16.495 16.0001,15.945 L16.0001,1.996 C16.0001,1.446 15.6021,1 15.1111,1 L15.1111,1 L15.1111,1 Z M14.0001,6.0002 L14.0001,14.949 L2.0001,14.949 L2.0001,6.0002 L14.0001,6.0002 Z M14.0001,4.0002 L14.0001,2.993 L2.0001,2.993 L2.0001,4.0002 L14.0001,4.0002 Z" id="Combined-Shape"></path>
+            <polygon id="Fill-11" points="3 2.0002 5 2.0002 5 0.0002 3 0.0002"></polygon>
+            <polygon id="Fill-16" points="11 2.0002 13 2.0002 13 0.0002 11 0.0002"></polygon>
+            <path d="M5.37709616,11.5511984 L6.92309616,12.7821984 C7.35112915,13.123019 7.97359761,13.0565604 8.32002627,12.6330535 L10.7740263,9.63305349 C11.1237073,9.20557058 11.0606364,8.57555475 10.6331535,8.22587373 C10.2056706,7.87619272 9.57565475,7.93926361 9.22597373,8.36674651 L6.77197373,11.3667465 L8.16890384,11.2176016 L6.62290384,9.98660159 C6.19085236,9.6425813 5.56172188,9.71394467 5.21770159,10.1459962 C4.8736813,10.5780476 4.94504467,11.2071781 5.37709616,11.5511984 L5.37709616,11.5511984 Z" id="Stroke-21"></path>
+        </g>
+    </g>
+</svg>
+```
+
+Correct file will look like this:
+
+```xml
+<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 16 17" enable-background="new 0 0 16 17"><path d="m15.1 1h-2.1v-1h-2v1h-6v-1h-2v1h-2.1c-.5 0-.9.5-.9 1v14c0 .6.4 1 .9 1h14.2c.5 0 .9-.4.9-1v-14c0-.5-.4-1-.9-1m-1.1 14h-12v-9h12v9m0-11h-12v-1h12v1"/><path d="m5.4 11.6l1.5 1.2c.4.3 1.1.3 1.4-.1l2.5-3c.3-.4.3-1.1-.1-1.4-.5-.4-1.1-.3-1.5.1l-1.8 2.2-.8-.6c-.4-.3-1.1-.3-1.4.2-.3.4-.3 1 .2 1.4"/></svg>
+```
+
 
 ## Buttons
 
@@ -63,3 +102,6 @@ Do not use both green and blue button in one form.
   display counts in the UI.
 
 [number_with_delimiter]: http://api.rubyonrails.org/classes/ActionView/Helpers/NumberHelper.html#method-i-number_with_delimiter
+[gitlab-map-graffle]: https://gitlab.com/gitlab-org/gitlab-design/blob/master/production/resources/gitlab-map.graffle
+[gitlab-map-pdf]: https://gitlab.com/gitlab-org/gitlab-design/raw/master/production/resources/gitlab-map.pdf
+[gitlab-map-png]: https://gitlab.com/gitlab-org/gitlab-design/raw/master/production/resources/gitlab-map.png
\ No newline at end of file
diff --git a/doc/development/what_requires_downtime.md b/doc/development/what_requires_downtime.md
new file mode 100644
index 0000000000000000000000000000000000000000..2574c2c04727c58a76bca7a86d1a04ef2ceff035
--- /dev/null
+++ b/doc/development/what_requires_downtime.md
@@ -0,0 +1,161 @@
+# What requires downtime?
+
+When working with a database certain operations can be performed without taking
+GitLab offline, others do require a downtime period. This guide describes
+various operations and their impact.
+
+## Adding Columns
+
+On PostgreSQL you can safely add a new column to an existing table as long as it
+does **not** have a default value. For example, this query would not require
+downtime:
+
+```sql
+ALTER TABLE projects ADD COLUMN random_value int;
+```
+
+Add a column _with_ a default however does require downtime. For example,
+consider this query:
+
+```sql
+ALTER TABLE projects ADD COLUMN random_value int DEFAULT 42;
+```
+
+This requires updating every single row in the `projects` table so that
+`random_value` is set to `42` by default. This requires updating all rows and
+indexes in a table. This in turn acquires enough locks on the table for it to
+effectively block any other queries.
+
+As of MySQL 5.6 adding a column to a table is still quite an expensive
+operation, even when using `ALGORITHM=INPLACE` and `LOCK=NONE`. This means
+downtime _may_ be required when modifying large tables as otherwise the
+operation could potentially take hours to complete.
+
+Adding a column with a default value _can_ be done without requiring downtime
+when using the migration helper method
+`Gitlab::Database::MigrationHelpers#add_column_with_default`. This method works
+similar to `add_column` except it updates existing rows in batches without
+blocking access to the table being modified. See ["Adding Columns With Default
+Values"](migration_style_guide.html#adding-columns-with-default-values) for more
+information on how to use this method.
+
+## Dropping Columns
+
+On PostgreSQL you can safely remove an existing column without the need for
+downtime. When you drop a column in PostgreSQL it's not immediately removed,
+instead it is simply disabled. The data is removed on the next vacuum run.
+
+On MySQL this operation requires downtime.
+
+While database wise dropping a column may be fine on PostgreSQL this operation
+still requires downtime because the application code may still be using the
+column that was removed. For example, consider the following migration:
+
+```ruby
+class MyMigration < ActiveRecord::Migration
+  def change
+    remove_column :projects, :dummy
+  end
+end
+```
+
+Now imagine that the GitLab instance is running and actively uses the `dummy`
+column. If we were to run the migration this would result in the GitLab instance
+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.
+
+## Changing Column Constraints
+
+Generally changing column constraints requires checking all rows in the table to
+see if they meet the new constraint, unless a constraint is _removed_. For
+example, changing a column that previously allowed NULL values to not allow NULL
+values requires the database to verify all existing rows.
+
+The specific behaviour varies a bit between databases but in general the safest
+approach is to assume changing constraints requires downtime.
+
+## Changing Column Types
+
+This operation requires downtime.
+
+## Adding Indexes
+
+Adding indexes is an expensive process that blocks INSERT and UPDATE queries for
+the duration. When using PostgreSQL one can work arounds this by using the
+`CONCURRENTLY` option:
+
+```sql
+CREATE INDEX CONCURRENTLY index_name ON projects (column_name);
+```
+
+Migrations can take advantage of this by using the method
+`add_concurrent_index`. For example:
+
+```ruby
+class MyMigration < ActiveRecord::Migration
+  def change
+    add_concurrent_index :projects, :column_name
+  end
+end
+```
+
+When running this on PostgreSQL the `CONCURRENTLY` option mentioned above is
+used. On MySQL this method produces a regular `CREATE INDEX` query.
+
+MySQL doesn't really have a workaround for this. Supposedly it _can_ create
+indexes without the need for downtime but only for variable width columns. The
+details on this are a bit sketchy. Since it's better to be safe than sorry one
+should assume that adding indexes requires downtime on MySQL.
+
+## Dropping Indexes
+
+Dropping an index does not require downtime on both PostgreSQL and MySQL.
+
+## Adding Tables
+
+This operation is safe as there's no code using the table just yet.
+
+## Dropping Tables
+
+This operation requires downtime as application code may still be using the
+table.
+
+## Adding Foreign Keys
+
+Adding foreign keys acquires an exclusive lock on both the source and target
+tables in PostgreSQL. This requires downtime as otherwise the entire application
+grinds to a halt for the duration of the operation.
+
+On MySQL this operation also requires downtime _unless_ foreign key checks are
+disabled. Because this means checks aren't enforced this is not ideal, as such
+one should assume MySQL also requires downtime.
+
+## Removing Foreign Keys
+
+This operation should not require downtime on both PostgreSQL and MySQL.
+
+## Updating Data
+
+Updating data should generally be safe. The exception to this is data that's
+being migrated from one version to another while the application still produces
+data in the old version.
+
+For example, imagine the application writes the string `'dog'` to a column but
+it really is meant to write `'cat'` instead. One might think that the following
+migration is all that is needed to solve this problem:
+
+```ruby
+class MyMigration < ActiveRecord::Migration
+  def up
+    execute("UPDATE some_table SET column = 'cat' WHERE column = 'dog';")
+  end
+end
+```
+
+Unfortunately this is not enough. Because the application is still running and
+using the old value this may result in the table still containing rows where
+`column` is set to `dog`, even after the migration finished.
+
+In these cases downtime _is_ required, even for rarely updated tables.
diff --git a/doc/install/installation.md b/doc/install/installation.md
index af8e31a705bfeb3422945120edde9f121c40b1c3..b0ec9b4a9611de3c2666858f94a421eacaba7396 100644
--- a/doc/install/installation.md
+++ b/doc/install/installation.md
@@ -89,7 +89,7 @@ Is the system packaged Git too old? Remove it and compile from source.
 
     # Download and compile from source
     cd /tmp
-    curl -O --progress https://www.kernel.org/pub/software/scm/git/git-2.7.4.tar.gz
+    curl --remote-name --progress https://www.kernel.org/pub/software/scm/git/git-2.7.4.tar.gz
     echo '7104c4f5d948a75b499a954524cb281fe30c6649d8abe20982936f75ec1f275b  git-2.7.4.tar.gz' | shasum -a256 -c - && tar -xzf git-2.7.4.tar.gz
     cd git-2.7.4/
     ./configure
@@ -108,8 +108,7 @@ Then select 'Internet Site' and press enter to confirm the hostname.
 
 ## 2. Ruby
 
-_**Note:** The current supported Ruby version is 2.1.x. Ruby 2.2 and 2.3 are
-currently not supported._
+_**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,
@@ -124,9 +123,9 @@ Remove the old Ruby 1.8 if present:
 Download Ruby and compile it:
 
     mkdir /tmp/ruby && cd /tmp/ruby
-    curl -O --progress https://cache.ruby-lang.org/pub/ruby/2.1/ruby-2.1.8.tar.gz
-    echo 'c7e50159357afd87b13dc5eaf4ac486a70011149  ruby-2.1.8.tar.gz' | shasum -c - && tar xzf ruby-2.1.8.tar.gz
-    cd ruby-2.1.8
+    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 -c - && tar xzf ruby-2.3.1.tar.gz
+    cd ruby-2.3.1
     ./configure --disable-install-rdoc
     make
     sudo make install
@@ -143,7 +142,7 @@ 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).
 
-    curl -O --progress https://storage.googleapis.com/golang/go1.5.3.linux-amd64.tar.gz
+    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
     sudo ln -sf /usr/local/go/bin/{go,godoc,gofmt} /usr/local/bin/
@@ -398,7 +397,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 v0.7.10
     sudo -u git -H make
 
 ### Initialize Database and Activate Advanced Features
@@ -588,15 +587,17 @@ for the changes to take effect.
 
 ### Custom Redis Connection
 
-If you'd like Resque to connect to a Redis server on a non-standard port or on a different host, you can configure its connection string via the `config/resque.yml` file.
+If you'd like to connect to a Redis server on a non-standard port or on a different host, you can configure its connection string via the `config/resque.yml` file.
 
     # example
-    production: redis://redis.example.tld:6379
+    production:
+      url: redis://redis.example.tld:6379
 
 If you want to connect the Redis server via socket, then use the "unix:" URL scheme and the path to the Redis socket file in the `config/resque.yml` file.
 
     # example
-    production: unix:/path/to/redis/socket
+    production:
+      url: unix:/path/to/redis/socket
 
 ### Custom SSH Connection
 
diff --git a/doc/integration/README.md b/doc/integration/README.md
index ddbd570ac6c9ed37b61e303be07adc7c66b26849..70895abbcadc3bb365ebd9fd0ffc254e790f9b39 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](koding.md) Configure Koding to use IDE integration
 
 GitLab Enterprise Edition contains [advanced Jenkins support][jenkins].
 
diff --git a/doc/integration/akismet.md b/doc/integration/akismet.md
index c222d21612f602a49021dfa8834b003d7b5d0330..a6436b5f9260a4bdee644076b7c7b4ae2d6477be 100644
--- a/doc/integration/akismet.md
+++ b/doc/integration/akismet.md
@@ -22,14 +22,37 @@ To use Akismet:
 
 2. Sign-in or create a new account.
 
-3. Click on "Show" to reveal the API key.
+3. Click on **Show** to reveal the API key.
 
 4. Go to Applications Settings on Admin Area (`admin/application_settings`)
 
-5. Check the `Enable Akismet` checkbox
+5. Check the **Enable Akismet** checkbox
 
 6. Fill in the API key from step 3.
 
 7. Save the configuration.
 
 ![Screenshot of Akismet settings](img/akismet_settings.png)
+
+
+## Training
+
+> *Note:* Training the Akismet filter is only available in 8.11 and above.
+
+As a way to better recognize between spam and ham, you can train the Akismet
+filter whenever there is a false positive or false negative.
+
+When an entry is recognized as spam, it is rejected and added to the Spam Logs. 
+From here you can review if they are really spam. If one of them is not really
+spam, you can use the **Submit as ham** button to tell Akismet that it falsely 
+recognized an entry as spam.
+
+![Screenshot of Spam Logs](img/spam_log.png)
+
+If an entry that is actually spam was not recognized as such, you will be able
+to also submit this to Akismet. The **Submit as spam** button will only appear
+to admin users.
+
+![Screenshot of Issue](img/submit_issue.png)
+
+Training Akismet will help it to recognize spam more accurately in the future.
diff --git a/doc/integration/bitbucket.md b/doc/integration/bitbucket.md
index 63432b044323a1d7d8f430d631baf71e165fe3ca..2eb6266ebe7952e8235ef09273711b343e567a3f 100644
--- a/doc/integration/bitbucket.md
+++ b/doc/integration/bitbucket.md
@@ -14,7 +14,7 @@ Bitbucket will generate an application ID and secret key for you to use.
 1.  Select "Add consumer".
 
 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.
+    - 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".
diff --git a/doc/integration/github.md b/doc/integration/github.md
index 340c8a55fb3a50b9924b25f12243cd8d12ce5b73..8a01afd1177931b7d140d03ed5d73f13728876af 100644
--- a/doc/integration/github.md
+++ b/doc/integration/github.md
@@ -16,7 +16,7 @@ GitHub will generate an application ID and secret key for you to use.
 1.  Select "Register new application".
 
 1.  Provide the required details.
-    - Application name: This can be anything. Consider something like "\<Organization\>'s GitLab" or "\<Your Name\>'s GitLab" or something else descriptive.
+    - Application name: This can be anything. Consider something like `<Organization>'s GitLab` or `<Your Name>'s GitLab` or something else descriptive.
     - Homepage URL: The URL to your GitLab installation. 'https://gitlab.company.com'
     - Application description: Fill this in if you wish.
     - Authorization callback URL is 'http(s)://${YOUR_DOMAIN}'
diff --git a/doc/integration/gitlab.md b/doc/integration/gitlab.md
index b215cc7c609a7cfc4473f9f1368106b9dec36f5f..6d8f3912ede101f0bcfbd7ef00734d7e7473cde8 100644
--- a/doc/integration/gitlab.md
+++ b/doc/integration/gitlab.md
@@ -14,7 +14,7 @@ GitLab.com will generate an application ID and secret key for you to use.
 1.  Select "New application".
 
 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.
+    - Name: This can be anything. Consider something like `<Organization>'s GitLab` or `<Your Name>'s GitLab` or something else descriptive.
     - Redirect URI:
 
     ```
diff --git a/doc/integration/img/spam_log.png b/doc/integration/img/spam_log.png
new file mode 100644
index 0000000000000000000000000000000000000000..8d5744486908ab01c22a615c033868cd35126464
Binary files /dev/null and b/doc/integration/img/spam_log.png differ
diff --git a/doc/integration/img/submit_issue.png b/doc/integration/img/submit_issue.png
new file mode 100644
index 0000000000000000000000000000000000000000..5c7896a7eec31ee81297c1b8c1612cdca351fad7
Binary files /dev/null and b/doc/integration/img/submit_issue.png differ
diff --git a/doc/integration/twitter.md b/doc/integration/twitter.md
index 4769f26b259ff5fdb668156f6ad8b96c85a2ba2c..abbea09f22fcc8bc7d0274851ed5c0c45ec6e84c 100644
--- a/doc/integration/twitter.md
+++ b/doc/integration/twitter.md
@@ -7,7 +7,7 @@ To enable the Twitter OmniAuth provider you must register your application with
 1.  Select "Create new app"
 
 1.  Fill in the application details.
-    - Name: This can be anything. Consider something like "\<Organization\>'s GitLab" or "\<Your Name\>'s GitLab" or
+    - Name: This can be anything. Consider something like `<Organization>'s GitLab` or `<Your Name>'s GitLab` or
     something else descriptive.
     - Description: Create a description.
     - Website: The URL to your GitLab installation. 'https://gitlab.example.com'
diff --git a/doc/legal/corporate_contributor_license_agreement.md b/doc/legal/corporate_contributor_license_agreement.md
index 7b94506c29785b55d27a9e2c1ea40fa3ed3779ae..7f08188bd652eb8cd31d6bab307bfb92464df3ad 100644
--- a/doc/legal/corporate_contributor_license_agreement.md
+++ b/doc/legal/corporate_contributor_license_agreement.md
@@ -6,13 +6,17 @@ You accept and agree to the following terms and conditions for Your present and
 
 	"You" (or "Your") shall mean the copyright owner or legal entity authorized by the copyright owner that is making this Agreement with GitLab B.V.. For legal entities, the entity making a Contribution and all other entities that control, are controlled by, or are under common control with that entity are considered to be a single Contributor. For the purposes of this definition, "control" means (i) the power, direct or indirect, to cause the direction or management of such entity, whether by contract or otherwise, or (ii) ownership of fifty percent (50%) or more of the outstanding shares, or (iii) beneficial ownership of such entity.
 
-	"Contribution" shall mean the code, documentation or other original works of authorship expressly identified in Schedule B, as well as any original work of authorship, including any modifications or additions to an existing work, that is intentionally submitted by You to GitLab B.V. for inclusion in, or documentation of, any of the products owned or managed by GitLab B.V. (the "Work"). For the purposes of this definition, "submitted" means any form of electronic, verbal, or written communication sent to GitLab B.V. or its representatives, including but not limited to communication on electronic mailing lists, source code control systems, and issue tracking systems that are managed by, or on behalf of, GitLab B.V. for the purpose of discussing and improving the Work, but excluding communication that is conspicuously marked or otherwise designated in writing by You as "Not a Contribution."
+	"Contribution" shall mean the code, documentation or other original works of authorship, including any modifications or additions to an existing work, that is submitted by You to GitLab B.V. for inclusion in, or documentation of, any of the products owned or managed by GitLab B.V. (the "Work"). For the purposes of this definition, "submitted" means any form of electronic, verbal, or written communication sent to GitLab B.V. or its representatives, including but not limited to communication on electronic mailing lists, source code control systems, and issue tracking systems that are managed by, or on behalf of, GitLab B.V. for the purpose of discussing and improving the Work, but excluding communication that is conspicuously marked or otherwise designated in writing by You as "Not a Contribution."
 
-2.  Grant of Copyright License. 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 copyright license to reproduce, prepare derivative works of, publicly display, publicly perform, sublicense, and distribute Your Contributions and such derivative works.
+2.  Grant of Copyright License.
 
-3.  Grant of Patent License. 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.
+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 copyright license to reproduce, prepare derivative works of, publicly display, publicly perform, sublicense, and distribute Your Contributions and such derivative works.
 
-4.  You represent that You are legally entitled to grant the above license. You represent further that each employee of the Corporation designated on Schedule A below (or in a subsequent written modification to that Schedule) is authorized to submit Contributions on behalf of the Corporation.
+3.  Grant of Patent License.
+
+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 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).
 
@@ -20,6 +24,6 @@ You accept and agree to the following terms and conditions for Your present and
 
 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 list of designated employees 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/markdown/markdown.md b/doc/markdown/markdown.md
index c6c7ac81c0d055ae08bda306306a8c6014683cd9..4ac81ab3ee7c5e5b307a7a39508cc974cc87f670 100644
--- a/doc/markdown/markdown.md
+++ b/doc/markdown/markdown.md
@@ -1,704 +1 @@
-# Markdown
-
-## Table of Contents
-
-**[GitLab Flavored Markdown](#gitlab-flavored-markdown-gfm)**
-
-* [Newlines](#newlines)
-* [Multiple underscores in words](#multiple-underscores-in-words)
-* [URL auto-linking](#url-auto-linking)
-* [Multiline Blockquote](#multiline-blockquote)
-* [Code and Syntax Highlighting](#code-and-syntax-highlighting)
-* [Inline Diff](#inline-diff)
-* [Emoji](#emoji)
-* [Special GitLab references](#special-gitlab-references)
-* [Task Lists](#task-lists)
-* [Videos](#videos)
-
-**[Standard Markdown](#standard-markdown)**
-
-* [Headers](#headers)
-* [Emphasis](#emphasis)
-* [Lists](#lists)
-* [Links](#links)
-* [Images](#images)
-* [Blockquotes](#blockquotes)
-* [Inline HTML](#inline-html)
-* [Horizontal Rule](#horizontal-rule)
-* [Line Breaks](#line-breaks)
-* [Tables](#tables)
-
-**[References](#references)**
-
-## GitLab Flavored Markdown (GFM)
-
-> **Note:**
-Not all of the GitLab-specific extensions to Markdown that are described in
-this document currently work on our documentation website.
->
-For the best result, we encourage you to check this document out as rendered
-by GitLab: [markdown.md]
-
-_GitLab uses the [Redcarpet Ruby library][redcarpet] for Markdown processing._
-
-GitLab uses "GitLab Flavored Markdown" (GFM). It extends the standard Markdown in a few significant ways to add some useful functionality. It was inspired by [GitHub Flavored Markdown](https://help.github.com/articles/basic-writing-and-formatting-syntax/).
-
-You can use GFM in the following areas:
-
-- comments
-- issues
-- merge requests
-- milestones
-- snippets (the snippet must be named with a `.md` extension)
-- wiki pages
-- markdown documents inside the repository
-
-You can also use other rich text files in GitLab. You might have to install a
-dependency to do so. Please see the [github-markup gem readme](https://github.com/gitlabhq/markup#markups) for more information.
-
-## Newlines
-
-> If this is not rendered correctly, see
-https://gitlab.com/gitlab-org/gitlab-ce/blob/master/doc/markdown/markdown.md#newlines
-
-GFM honors the markdown specification in how [paragraphs and line breaks are handled](https://daringfireball.net/projects/markdown/syntax#p).
-
-A paragraph is simply one or more consecutive lines of text, separated by one or more blank lines.
-Line-breaks, or softreturns, are rendered if you end a line with two or more spaces:
-
-    Roses are red [followed by two or more spaces]
-    Violets are blue
-
-    Sugar is sweet
-
-Roses are red  
-Violets are blue
-
-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
-
-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:
-
-    perform_complicated_task
-
-    do_this_and_do_that_and_another_thing
-
-perform_complicated_task
-
-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
-
-GFM will autolink almost any URL you copy and paste into your text:
-
-    * https://www.google.com
-    * https://google.com/
-    * ftp://ftp.us.debian.org/debian/
-    * smb://foo/bar/baz
-    * irc://irc.freenode.net/gitlab
-    * http://localhost:3000
-
-* https://www.google.com
-* https://google.com/
-* ftp://ftp.us.debian.org/debian/
-* smb://foo/bar/baz
-* irc://irc.freenode.net/gitlab
-* http://localhost:3000
-
-## Multiline Blockquote
-
-> If this is not rendered correctly, see
-https://gitlab.com/gitlab-org/gitlab-ce/blob/master/doc/markdown/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>:
-
-```no-highlight
->>>
-If you paste a message from somewhere else
-
-that
-
-spans
-
-multiple lines,
-
-you can quote that without having to manually prepend `>` to every line!
->>>
-```
-
->>>
-If you paste a message from somewhere else
-
-that
-
-spans
-
-multiple lines,
-
-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
-
-_GitLab uses the [Rouge Ruby library][rouge] for syntax highlighting. For a
-list of supported languages visit the Rouge website._
-
-Blocks of code are either fenced by lines with three back-ticks <code>```</code>,
-or are indented with four spaces. Only the fenced code blocks support syntax
-highlighting:
-
-```no-highlight
-Inline `code` has `back-ticks around` it.
-```
-
-Inline `code` has `back-ticks around` it.
-
-Example:
-
-    ```javascript
-    var s = "JavaScript syntax highlighting";
-    alert(s);
-    ```
-
-    ```python
-    def function():
-        #indenting works just fine in the fenced code block
-        s = "Python syntax highlighting"
-        print s
-    ```
-
-    ```ruby
-    require 'redcarpet'
-    markdown = Redcarpet.new("Hello World!")
-    puts markdown.to_html
-    ```
-
-    ```
-    No language indicated, so no syntax highlighting.
-    s = "There is no highlighting for this."
-    But let's throw in a <b>tag</b>.
-    ```
-
-becomes:
-
-```javascript
-var s = "JavaScript syntax highlighting";
-alert(s);
-```
-
-```python
-def function():
-    #indenting works just fine in the fenced code block
-    s = "Python syntax highlighting"
-    print s
-```
-
-```ruby
-require 'redcarpet'
-markdown = Redcarpet.new("Hello World!")
-puts markdown.to_html
-```
-
-```
-No language indicated, so no syntax highlighting.
-s = "There is no highlighting for this."
-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
-
-With inline diffs tags you can display {+ additions +} or [- deletions -].
-
-The wrapping tags can be either curly braces or square brackets [+ additions +] or {- deletions -}.
-
-However the wrapping tags cannot be mixed as such:
-
-- {+ additions +]
-- [+ additions +}
-- {- deletions -]
-- [- deletions -}
-
-## Emoji
-
-> If this is not rendered correctly, see
-https://gitlab.com/gitlab-org/gitlab-ce/blob/master/doc/markdown/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:
-
-	:zap: You can use emoji anywhere GFM is supported. :v:
-
-	You can use it to point out a :bug: or warn about :speak_no_evil: patches. And if someone improves your really :snail: code, send them some :birthday:. People will :heart: you for that.
-
-	If you are new to this, don't be :fearful:. You can easily join the emoji :family:. All you need to do is to look up on the supported codes.
-
-	Consult the [Emoji Cheat Sheet](http://emoji.codes) for a list of all supported emoji codes. :thumbsup:
-
-Sometimes you want to :monkey: around a bit and add some :star2: to your :speech_balloon:. Well we have a gift for you:
-
-:zap: You can use emoji anywhere GFM is supported. :v:
-
-You can use it to point out a :bug: or warn about :speak_no_evil: patches. And if someone improves your really :snail: code, send them some :birthday:. People will :heart: you for that.
-
-If you are new to this, don't be :fearful:. You can easily join the emoji :family:. All you need to do is to look up on the supported codes.
-
-Consult the [Emoji Cheat Sheet](http://emoji.codes) for a list of all supported emoji codes. :thumbsup:
-
-## Special GitLab References
-
-GFM recognizes special references.
-
-You can easily reference e.g. an issue, a commit, a team member or even the whole team within a project.
-
-GFM will turn that reference into a link so you can navigate between them easily.
-
-GFM will recognize the following:
-
-| input                  | references                   |
-|:-----------------------|:---------------------------  |
-| `@user_name`           | specific user                |
-| `@group_name`          | specific group               |
-| `@all`                 | entire team                  |
-| `#123`                 | issue                        |
-| `!123`                 | merge request                |
-| `$123`                 | snippet                      |
-| `~123`                 | label by ID                  |
-| `~bug`                 | one-word label by name       |
-| `~"feature request"`   | multi-word label by name     |
-| `%123`                 | milestone by ID              |
-| `%v1.23`               | one-word milestone by name   |
-| `%"release candidate"` | multi-word milestone by name |
-| `9ba12248`             | specific commit              |
-| `9ba12248...b19a04f5`  | commit range comparison      |
-| `[README](doc/README)` | repository file references   |
-
-GFM also recognizes certain cross-project references:
-
-| input                                   | references              |
-|:----------------------------------------|:------------------------|
-| `namespace/project#123`                 | issue                   |
-| `namespace/project!123`                 | merge request           |
-| `namespace/project%123`                 | milestone               |
-| `namespace/project$123`                 | snippet                 |
-| `namespace/project@9ba12248`            | specific commit         |
-| `namespace/project@9ba12248...b19a04f5` | commit range comparison |
-| `namespace/project~"Some label"`        | issues with given label |
-
-## Task Lists
-
-> If this is not rendered correctly, see
-https://gitlab.com/gitlab-org/gitlab-ce/blob/master/doc/markdown/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:
-
-```no-highlight
-- [x] Completed task
-- [ ] Incomplete task
-    - [ ] Sub-task 1
-    - [x] Sub-task 2
-    - [ ] Sub-task 3
-```
-
-- [x] Completed task
-- [ ] Incomplete task
-    - [ ] Sub-task 1
-    - [x] Sub-task 2
-    - [ ] Sub-task 3
-
-Task lists can only be created in descriptions, not in titles. Task item state can be managed by editing the description's Markdown or by toggling the rendered check boxes.
-
-## Videos
-
-> If this is not rendered correctly, see
-https://gitlab.com/gitlab-org/gitlab-ce/blob/master/doc/markdown/markdown.md#videos
-
-Image tags with a video extension are automatically converted to a video player.
-
-The valid video extensions are `.mp4`, `.m4v`, `.mov`, `.webm`, and `.ogv`.
-
-    Here's a sample video:
-
-    ![Sample Video](img/video.mp4)
-
-Here's a sample video:
-
-![Sample Video](img/video.mp4)
-
-# Standard Markdown
-
-## Headers
-
-```no-highlight
-# H1
-## H2
-### H3
-#### H4
-##### H5
-###### H6
-
-Alternatively, for H1 and H2, an underline-ish style:
-
-Alt-H1
-======
-
-Alt-H2
-------
-```
-
-# H1
-## H2
-### H3
-#### H4
-##### H5
-###### H6
-
-Alternatively, for H1 and H2, an underline-ish style:
-
-Alt-H1
-======
-
-Alt-H2
-------
-
-### Header IDs and links
-
-All Markdown-rendered headers automatically get IDs, except in comments.
-
-On hover a link to those IDs becomes visible to make it easier to copy the link to the header to give it to someone else.
-
-The IDs are generated from the content of the header according to the following rules:
-
-1. All text is converted to lowercase
-1. All non-word text (e.g., punctuation, HTML) is removed
-1. All spaces are converted to hyphens
-1. Two or more hyphens in a row are converted to one
-1. If a header with the same ID has already been generated, a unique
-   incrementing number is appended, starting at 1.
-
-For example:
-
-```
-# This header has spaces in it
-## This header has a :thumbsup: in it
-# This header has Unicode in it: 한글
-## This header has spaces in it
-### This header has spaces in it
-```
-
-Would generate the following link IDs:
-
-1. `this-header-has-spaces-in-it`
-1. `this-header-has-a-in-it`
-1. `this-header-has-unicode-in-it-한글`
-1. `this-header-has-spaces-in-it`
-1. `this-header-has-spaces-in-it-1`
-
-Note that the Emoji processing happens before the header IDs are generated, so the Emoji is converted to an image which then gets removed from the ID.
-
-## Emphasis
-
-```no-highlight
-Emphasis, aka italics, with *asterisks* or _underscores_.
-
-Strong emphasis, aka bold, with **asterisks** or __underscores__.
-
-Combined emphasis with **asterisks and _underscores_**.
-
-Strikethrough uses two tildes. ~~Scratch this.~~
-```
-
-Emphasis, aka italics, with *asterisks* or _underscores_.
-
-Strong emphasis, aka bold, with **asterisks** or __underscores__.
-
-Combined emphasis with **asterisks and _underscores_**.
-
-Strikethrough uses two tildes. ~~Scratch this.~~
-
-## Lists
-
-```no-highlight
-1. First ordered list item
-2. Another item
-  * Unordered sub-list.
-1. Actual numbers don't matter, just that it's a number
-  1. Ordered sub-list
-4. And another item.
-
-* Unordered list can use asterisks
-- Or minuses
-+ Or pluses
-```
-
-1. First ordered list item
-2. Another item
-  * Unordered sub-list.
-1. Actual numbers don't matter, just that it's a number
-  1. Ordered sub-list
-4. And another item.
-
-* Unordered list can use asterisks
-- Or minuses
-+ Or pluses
-
-If a list item contains multiple paragraphs,
-each subsequent paragraph should be indented with four spaces.
-
-```no-highlight
-1.  First ordered list item
-
-    Second paragraph of first item.
-2.  Another item
-```
-
-1.  First ordered list item
-
-    Second paragraph of first item.
-2.  Another item
-
-If the second paragraph isn't indented with four spaces,
-the second list item will be incorrectly labeled as `1`.
-
-```no-highlight
-1. First ordered list item
-
-   Second paragraph of first item.
-2. Another item
-```
-
-1. First ordered list item
-
-   Second paragraph of first item.
-2. Another item
-
-## Links
-
-There are two ways to create links, inline-style and reference-style.
-
-    [I'm an inline-style link](https://www.google.com)
-
-    [I'm a reference-style link][Arbitrary case-insensitive reference text]
-
-    [I'm a relative reference to a repository file](LICENSE)
-
-    [You can use numbers for reference-style link definitions][1]
-
-    Or leave it empty and use the [link text itself][]
-
-    Some text to show that the reference links can follow later.
-
-    [arbitrary case-insensitive reference text]: https://www.mozilla.org
-    [1]: http://slashdot.org
-    [link text itself]: https://www.reddit.com
-
-[I'm an inline-style link](https://www.google.com)
-
-[I'm a reference-style link][Arbitrary case-insensitive reference text]
-
-[I'm a relative reference to a repository file](LICENSE)[^1]
-
-[You can use numbers for reference-style link definitions][1]
-
-Or leave it empty and use the [link text itself][]
-
-Some text to show that the reference links can follow later.
-
-[arbitrary case-insensitive reference text]: https://www.mozilla.org
-[1]: http://slashdot.org
-[link text itself]: https://www.reddit.com
-
-**Note**
-
-Relative links do not allow referencing project files in a wiki page or wiki page in a project file. The reason for this is that, in GitLab, wiki is always a separate git repository. For example:
-
-`[I'm a reference-style link](style)`
-
-will point the link to `wikis/style` when the link is inside of a wiki markdown file.
-
-## Images
-
-    Here's our logo (hover to see the title text):
-
-    Inline-style:
-    ![alt text](img/logo.png)
-
-    Reference-style:
-    ![alt text1][logo]
-
-    [logo]: img/logo.png
-
-Here's our logo:
-
-Inline-style:
-
-![alt text](img/logo.png)
-
-Reference-style:
-
-![alt text][logo]
-
-[logo]: img/logo.png
-
-## Blockquotes
-
-```no-highlight
-> Blockquotes are very handy in email to emulate reply text.
-> This line is part of the same quote.
-
-Quote break.
-
-> This is a very long line that will still be quoted properly when it wraps. Oh boy let's keep writing to make sure this is long enough to actually wrap for everyone. Oh, you can *put* **Markdown** into a blockquote.
-```
-
-> Blockquotes are very handy in email to emulate reply text.
-> This line is part of the same quote.
-
-Quote break.
-
-> This is a very long line that will still be quoted properly when it wraps. Oh boy let's keep writing to make sure this is long enough to actually wrap for everyone. Oh, you can *put* **Markdown** into a blockquote.
-
-## Inline HTML
-
-You can also use raw HTML in your Markdown, and it'll mostly work pretty well.
-
-See the documentation for HTML::Pipeline's [SanitizationFilter](http://www.rubydoc.info/gems/html-pipeline/HTML/Pipeline/SanitizationFilter#WHITELIST-constant) class for the list of allowed HTML tags and attributes.  In addition to the default `SanitizationFilter` whitelist, GitLab allows `span` elements.
-
-```no-highlight
-<dl>
-  <dt>Definition list</dt>
-  <dd>Is something people use sometimes.</dd>
-
-  <dt>Markdown in HTML</dt>
-  <dd>Does *not* work **very** well. Use HTML <em>tags</em>.</dd>
-</dl>
-```
-
-<dl>
-  <dt>Definition list</dt>
-  <dd>Is something people use sometimes.</dd>
-
-  <dt>Markdown in HTML</dt>
-  <dd>Does *not* work **very** well. Use HTML <em>tags</em>.</dd>
-</dl>
-
-## Horizontal Rule
-
-```
-Three or more...
-
----
-
-Hyphens
-
-***
-
-Asterisks
-
-___
-
-Underscores
-```
-
-Three or more...
-
----
-
-Hyphens
-
-***
-
-Asterisks
-
-___
-
-Underscores
-
-## Line Breaks
-
-My basic recommendation for learning how line breaks work is to experiment and discover -- hit &lt;Enter&gt; once (i.e., insert one newline), then hit it twice (i.e., insert two newlines), see what happens. You'll soon learn to get what you want. "Markdown Toggle" is your friend.
-
-Here are some things to try out:
-
-```
-Here's a line for us to start with.
-
-This line is separated from the one above by two newlines, so it will be a *separate paragraph*.
-
-This line is also a separate paragraph, but...
-This line is only separated by a single newline, so it's a separate line in the *same paragraph*.
-
-This line is also a separate paragraph, and...
-This line is on its own line, because the previous line ends with two
-spaces.
-```
-
-Here's a line for us to start with.
-
-This line is separated from the one above by two newlines, so it will be a *separate paragraph*.
-
-This line is also begins a separate paragraph, but...
-This line is only separated by a single newline, so it's a separate line in the *same paragraph*.
-
-This line is also a separate paragraph, and...
-This line is on its own line, because the previous line ends with two
-spaces.
-
-## Tables
-
-Tables aren't part of the core Markdown spec, but they are part of GFM and Markdown Here supports them.
-
-```
-| header 1 | header 2 |
-| -------- | -------- |
-| cell 1   | cell 2   |
-| cell 3   | cell 4   |
-```
-
-Code above produces next output:
-
-| header 1 | header 2 |
-| -------- | -------- |
-| cell 1   | cell 2   |
-| cell 3   | cell 4   |
-
-**Note**
-
-The row of dashes between the table header and body must have at least three dashes in each column.
-
-By including colons in the header row, you can align the text within that column:
-
-```
-| Left Aligned | Centered | Right Aligned | Left Aligned | Centered | Right Aligned |
-| :----------- | :------: | ------------: | :----------- | :------: | ------------: |
-| Cell 1       | Cell 2   | Cell 3        | Cell 4       | Cell 5   | Cell 6        |
-| Cell 7       | Cell 8   | Cell 9        | Cell 10      | Cell 11  | Cell 12       |
-```
-
-| Left Aligned | Centered | Right Aligned | Left Aligned | Centered | Right Aligned |
-| :----------- | :------: | ------------: | :----------- | :------: | ------------: |
-| Cell 1       | Cell 2   | Cell 3        | Cell 4       | Cell 5   | Cell 6        |
-| Cell 7       | Cell 8   | Cell 9        | Cell 10      | Cell 11  | Cell 12       |
-
-## References
-
-- This document leveraged heavily from the [Markdown-Cheatsheet](https://github.com/adam-p/markdown-here/wiki/Markdown-Cheatsheet).
-- 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
-[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
+This document was moved to [user/markdown.md](../user/markdown.md).
diff --git a/doc/monitoring/health_check.md b/doc/monitoring/health_check.md
index 0d17799372f4738412f65ee15f5bb55a1369b180..eac57bc3de4b9da3e95b57c846f2a0bb85694139 100644
--- a/doc/monitoring/health_check.md
+++ b/doc/monitoring/health_check.md
@@ -1,6 +1,6 @@
 # Health Check
 
->**Note:** This feature was [introduced][ce-3888] in GitLab 8.8.
+> [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
@@ -24,7 +24,7 @@ https://gitlab.example.com/health_check.json?token=ACCESS_TOKEN
 or as an HTTP header:
 
 ```bash
-curl -H "TOKEN: ACCESS_TOKEN" https://gitlab.example.com/health_check.json
+curl --header "TOKEN: ACCESS_TOKEN" https://gitlab.example.com/health_check.json
 ```
 
 ## Using the Endpoint
@@ -45,7 +45,7 @@ You can also ask for the status of specific services:
 For example, the JSON output of the following health check:
 
 ```bash
-curl -H "TOKEN: ACCESS_TOKEN" https://gitlab.example.com/health_check.json
+curl --header "TOKEN: ACCESS_TOKEN" https://gitlab.example.com/health_check.json
 ```
 
 would be like:
diff --git a/doc/monitoring/performance/influxdb_schema.md b/doc/monitoring/performance/influxdb_schema.md
index 41861860b6d2d23ed9a5ec54fdefdcce18c6fcc0..eff0e29f58d5e856d762d6cc815b73b09b5806c3 100644
--- a/doc/monitoring/performance/influxdb_schema.md
+++ b/doc/monitoring/performance/influxdb_schema.md
@@ -9,6 +9,7 @@ The following measurements are currently stored in InfluxDB:
 - `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.
@@ -78,6 +79,14 @@ following value fields are available:
 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:
diff --git a/doc/raketasks/backup_restore.md b/doc/raketasks/backup_restore.md
index 5fa96736d5914729c895fec9e03764d04a51d99a..835af5443a329bd05f46fea2fdd5ee0c591d484f 100644
--- a/doc/raketasks/backup_restore.md
+++ b/doc/raketasks/backup_restore.md
@@ -11,12 +11,13 @@ You can only restore a backup to exactly the same version of GitLab that you cre
 on, for example 7.2.1. The best way to migrate your repositories from one server to
 another is through backup restore.
 
-You need to keep a separate copy of `/etc/gitlab/gitlab-secrets.json`
-(for omnibus packages) or `/home/git/gitlab/.secret` (for installations
-from source). This file contains the database encryption key used
-for two-factor authentication. 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.
+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.
 
 ```
 # use this command if you've installed GitLab with the Omnibus package
@@ -221,11 +222,12 @@ 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 `.secret` 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 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).
 
-At the very **minimum** you should backup `/etc/gitlab/gitlab-secrets.json`
-(Omnibus) or `/home/git/gitlab/.secret` (source) to preserve your
-database encryption key.
+At the very **minimum** you should backup `/etc/gitlab/gitlab.rb` and
+`/etc/gitlab/gitlab-secrets.json` (Omnibus), or
+`/home/git/gitlab/config/secrets.yml` (source) to preserve your database
+encryption key.
 
 ## Restore a previously created backup
 
@@ -240,11 +242,11 @@ the SQL database it needs to import data into ('gitlabhq_production').
 All existing data will be either erased (SQL) or moved to a separate
 directory (repositories, uploads).
 
-If some or all of your GitLab users are using two-factor authentication
-(2FA) then you must also make sure to restore
-`/etc/gitlab/gitlab-secrets.json` (Omnibus) or `/home/git/gitlab/.secret`
-(installations from source). Note that you need to run `gitlab-ctl
-reconfigure` after changing `gitlab-secrets.json`.
+If some or all of your GitLab users are using two-factor authentication (2FA)
+then you must also make sure to restore `/etc/gitlab/gitlab.rb` and
+`/etc/gitlab/gitlab-secrets.json` (Omnibus), or
+`/home/git/gitlab/config/secrets.yml` (installations from source). Note that you
+need to run `gitlab-ctl reconfigure` after changing `gitlab-secrets.json`.
 
 ### Installation from source
 
diff --git a/doc/raketasks/user_management.md b/doc/raketasks/user_management.md
index 629d38efc5355042124c94a0a9c88ba73a7a32aa..8a5e2d6e16bfe94e4d194dc421411ce10e2a9dfc 100644
--- a/doc/raketasks/user_management.md
+++ b/doc/raketasks/user_management.md
@@ -60,8 +60,8 @@ block_auto_created_users: false
 ## Disable Two-factor Authentication (2FA) for all users
 
 This task will disable 2FA for all users that have it enabled. This can be
-useful if GitLab's `.secret` file has been lost and users are unable to login,
-for example.
+useful if GitLab's `config/secrets.yml` file has been lost and users are unable
+to login, for example.
 
 ```bash
 # omnibus-gitlab
diff --git a/doc/update/4.0-to-4.1.md b/doc/update/4.0-to-4.1.md
index c163bfd348d285b0d33142d9185ca17ef00cbe42..c66c6dd0fd8d9a0172f409593b5201fb93a485f3 100644
--- a/doc/update/4.0-to-4.1.md
+++ b/doc/update/4.0-to-4.1.md
@@ -42,7 +42,7 @@ sudo -u gitlab -H bundle exec rake db:migrate RAILS_ENV=production
 sudo mv /etc/init.d/gitlab /etc/init.d/gitlab.old
 
 # get new one using sidekiq
-sudo curl -L --output /etc/init.d/gitlab https://raw.github.com/gitlabhq/gitlab-recipes/4-1-stable/init.d/gitlab
+sudo curl --location --output /etc/init.d/gitlab https://raw.github.com/gitlabhq/gitlab-recipes/4-1-stable/init.d/gitlab
 sudo chmod +x /etc/init.d/gitlab
 
 ```
diff --git a/doc/update/4.2-to-5.0.md b/doc/update/4.2-to-5.0.md
index ee6de51c9233207055f19bdd0f044b419f173a99..7654f4a0131d1348b2a710c9870eb41121dc0822 100644
--- a/doc/update/4.2-to-5.0.md
+++ b/doc/update/4.2-to-5.0.md
@@ -126,7 +126,7 @@ sudo chmod -R u+rwX  /home/git/gitlab/tmp/pids
 ```bash
 # init.d
 sudo rm /etc/init.d/gitlab
-sudo curl -L --output /etc/init.d/gitlab https://raw.github.com/gitlabhq/gitlab-recipes/5-0-stable/init.d/gitlab
+sudo curl --location --output /etc/init.d/gitlab https://raw.github.com/gitlabhq/gitlab-recipes/5-0-stable/init.d/gitlab
 sudo chmod +x /etc/init.d/gitlab
 
 # unicorn
diff --git a/doc/update/5.0-to-5.1.md b/doc/update/5.0-to-5.1.md
index f0fddcf83afd9e9e1d7edde97c4f74eccfed30f7..c19a819ab5a3f88826b850887fd0dcc5aecf4639 100644
--- a/doc/update/5.0-to-5.1.md
+++ b/doc/update/5.0-to-5.1.md
@@ -63,7 +63,7 @@ sudo -u git -H bundle exec rake assets:precompile RAILS_ENV=production
 ```bash
 # init.d
 sudo rm /etc/init.d/gitlab
-sudo curl -L --output /etc/init.d/gitlab https://raw.github.com/gitlabhq/gitlab-recipes/5-1-stable/init.d/gitlab
+sudo curl --location --output /etc/init.d/gitlab https://raw.github.com/gitlabhq/gitlab-recipes/5-1-stable/init.d/gitlab
 sudo chmod +x /etc/init.d/gitlab
 ```
 
diff --git a/doc/update/5.2-to-5.3.md b/doc/update/5.2-to-5.3.md
index c5254f6fb0c73fb0e94b91c495b5965bf13f676e..fe8990b6843ccfaeed4da7a146b78db8940a3f7f 100644
--- a/doc/update/5.2-to-5.3.md
+++ b/doc/update/5.2-to-5.3.md
@@ -67,7 +67,7 @@ sudo -u git -H bundle exec rake assets:precompile RAILS_ENV=production
 
 ```bash
 sudo rm /etc/init.d/gitlab
-sudo curl -L --output /etc/init.d/gitlab https://raw.github.com/gitlabhq/gitlabhq/5-3-stable/lib/support/init.d/gitlab
+sudo curl --location --output /etc/init.d/gitlab https://raw.github.com/gitlabhq/gitlabhq/5-3-stable/lib/support/init.d/gitlab
 sudo chmod +x /etc/init.d/gitlab
 ```
 
diff --git a/doc/update/5.3-to-5.4.md b/doc/update/5.3-to-5.4.md
index c4a6146dcda3cbd7b4d4de6d1e1f4f36f22fe424..5f82ad7d444723d9a95d180a8d88b7f2acc867b2 100644
--- a/doc/update/5.3-to-5.4.md
+++ b/doc/update/5.3-to-5.4.md
@@ -71,7 +71,7 @@ sudo -u git -H bundle exec rake assets:precompile RAILS_ENV=production
 
 ```bash
 sudo rm /etc/init.d/gitlab
-sudo curl -L --output /etc/init.d/gitlab https://raw.github.com/gitlabhq/gitlabhq/5-4-stable/lib/support/init.d/gitlab
+sudo curl --location --output /etc/init.d/gitlab https://raw.github.com/gitlabhq/gitlabhq/5-4-stable/lib/support/init.d/gitlab
 sudo chmod +x /etc/init.d/gitlab
 ```
 
diff --git a/doc/update/6.9-to-7.0.md b/doc/update/6.9-to-7.0.md
index 236430b5951618062a534e88e9a20ce9dfd2e5cc..5352fd52f93ac287a0c63e49509154343c4cedc6 100644
--- a/doc/update/6.9-to-7.0.md
+++ b/doc/update/6.9-to-7.0.md
@@ -33,7 +33,7 @@ Download and compile Ruby:
 
 ```bash
 mkdir /tmp/ruby && cd /tmp/ruby
-curl -L --progress ftp://ftp.ruby-lang.org/pub/ruby/2.1/ruby-2.1.2.tar.gz | tar xz
+curl --location --progress ftp://ftp.ruby-lang.org/pub/ruby/2.1/ruby-2.1.2.tar.gz | tar xz
 cd ruby-2.1.2
 ./configure --disable-install-rdoc
 make
diff --git a/doc/update/7.0-to-7.1.md b/doc/update/7.0-to-7.1.md
index a4e9be9946e07fdcbc7f1e7f9416607862a45236..71f39c44077e34443663f2bf31ab4f94cae90c87 100644
--- a/doc/update/7.0-to-7.1.md
+++ b/doc/update/7.0-to-7.1.md
@@ -33,7 +33,7 @@ Download and compile Ruby:
 
 ```bash
 mkdir /tmp/ruby && cd /tmp/ruby
-curl -L --progress ftp://ftp.ruby-lang.org/pub/ruby/2.1/ruby-2.1.2.tar.gz | tar xz
+curl --location --progress ftp://ftp.ruby-lang.org/pub/ruby/2.1/ruby-2.1.2.tar.gz | tar xz
 cd ruby-2.1.2
 ./configure --disable-install-rdoc
 make
diff --git a/doc/update/7.14-to-8.0.md b/doc/update/7.14-to-8.0.md
index 305017b704816cea41a22b80e18efbd1d355ae7a..117e2afaaa0b946d67bcc269ef0b44b07aad5ed3 100644
--- a/doc/update/7.14-to-8.0.md
+++ b/doc/update/7.14-to-8.0.md
@@ -71,7 +71,7 @@ sudo -u git -H git checkout v2.6.5
 First we download Go 1.5 and install it into `/usr/local/go`:
 
 ```bash
-curl -O --progress https://storage.googleapis.com/golang/go1.5.linux-amd64.tar.gz
+curl --remote-name --progress https://storage.googleapis.com/golang/go1.5.linux-amd64.tar.gz
 echo '5817fa4b2252afdb02e11e8b9dc1d9173ef3bd5a  go1.5.linux-amd64.tar.gz' | shasum -c - && \
   sudo tar -C /usr/local -xzf go1.5.linux-amd64.tar.gz
 sudo ln -sf /usr/local/go/bin/{go,godoc,gofmt} /usr/local/bin/
diff --git a/doc/update/8.10-to-8.11.md b/doc/update/8.10-to-8.11.md
index fc6262dd108fd4468b8972b61481fb3ab017c3c8..9872176356628837b3ab146596e26319020be801 100644
--- a/doc/update/8.10-to-8.11.md
+++ b/doc/update/8.10-to-8.11.md
@@ -20,7 +20,31 @@ cd /home/git/gitlab
 sudo -u git -H bundle exec rake gitlab:backup:create RAILS_ENV=production
 ```
 
-### 3. Get latest code
+### 3. Update Ruby
+
+If you are you running Ruby 2.1.x, you do not _need_ to upgrade Ruby yet, but you should note that support for 2.1.x is deprecated and we will require 2.3.x in 8.13. It's strongly recommended that you upgrade as soon as possible.
+
+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 +65,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.2.1
+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
@@ -62,22 +86,6 @@ sudo -u git -H git checkout v0.7.8
 sudo -u git -H make
 ```
 
-### 6. Update MySQL permissions
-
-If you are using MySQL you need to grant the GitLab user the necessary
-permissions on the database:
-
-```bash
-# Login to MySQL
-mysql -u root -p
-
-# Grant the GitLab user the REFERENCES permission on the database
-GRANT REFERENCES ON `gitlabhq_production`.* TO 'git'@'localhost';
-
-# Quit the database session
-mysql> \q
-```
-
 ### 7. Install libs, migrations, etc.
 
 ```bash
@@ -110,14 +118,6 @@ There are new configuration options available for [`gitlab.yml`](config/gitlab.y
 git diff origin/8-10-stable:config/gitlab.yml.example origin/8-11-stable:config/gitlab.yml.example
 ```
 
-#### Git configuration
-
-Disable `git gc --auto` because GitLab runs `git gc` for us already.
-
-```sh
-sudo -u git -H git config --global gc.auto 0
-```
-
 #### Nginx configuration
 
 Ensure you're still up-to-date with the latest NGINX configuration changes:
diff --git a/doc/user/admin_area/img/admin_labels.png b/doc/user/admin_area/img/admin_labels.png
new file mode 100644
index 0000000000000000000000000000000000000000..1ee33a534abe4aca634869dea9330243ab3521f8
Binary files /dev/null and b/doc/user/admin_area/img/admin_labels.png differ
diff --git a/doc/user/admin_area/labels.md b/doc/user/admin_area/labels.md
new file mode 100644
index 0000000000000000000000000000000000000000..9e2a89ebdf600f4ed3130a9667f988316286f629
--- /dev/null
+++ b/doc/user/admin_area/labels.md
@@ -0,0 +1,9 @@
+# Labels
+
+## Default Labels
+
+### Define your own default Label Set
+
+Labels that are created within the Labels view on the Admin Dashboard will be automatically added to each new project.
+
+![Default label set](img/admin_labels.png)
diff --git a/doc/user/admin_area/settings/continuous_integration.md b/doc/user/admin_area/settings/continuous_integration.md
new file mode 100644
index 0000000000000000000000000000000000000000..34e2e557f8974ee6996ac4fdf511402bc5ae79b9
--- /dev/null
+++ b/doc/user/admin_area/settings/continuous_integration.md
@@ -0,0 +1,20 @@
+# Continuous integration Admin settings
+
+## Maximum artifacts size
+
+The maximum size of the [build artifacts][art-yml] can be set in the Admin area
+of your GitLab instance. The value is in MB and the default is 100MB. Note that
+this setting is set for each build.
+
+1. Go to **Admin area > Settings** (`/admin/application_settings`).
+
+    ![Admin area settings button](img/admin_area_settings_button.png)
+
+1. Change the value of the maximum artifacts size (in MB):
+
+    ![Admin area maximum artifacts size](img/admin_area_maximum_artifacts_size.png)
+
+1. Hit **Save** for the changes to take effect.
+
+
+[art-yml]: ../../../administration/build_artifacts.md
diff --git a/doc/user/admin_area/settings/img/admin_area_maximum_artifacts_size.png b/doc/user/admin_area/settings/img/admin_area_maximum_artifacts_size.png
new file mode 100644
index 0000000000000000000000000000000000000000..53f7e76033ede024d42dfa977da7399eccad0d46
Binary files /dev/null and b/doc/user/admin_area/settings/img/admin_area_maximum_artifacts_size.png differ
diff --git a/doc/user/admin_area/settings/img/admin_area_settings_button.png b/doc/user/admin_area/settings/img/admin_area_settings_button.png
new file mode 100644
index 0000000000000000000000000000000000000000..509708b627f545643aef6cd0bb17da3b36bf3b5c
Binary files /dev/null and b/doc/user/admin_area/settings/img/admin_area_settings_button.png differ
diff --git a/doc/markdown/img/logo.png b/doc/user/img/markdown_logo.png
similarity index 100%
rename from doc/markdown/img/logo.png
rename to doc/user/img/markdown_logo.png
diff --git a/doc/markdown/img/video.mp4 b/doc/user/img/markdown_video.mp4
similarity index 100%
rename from doc/markdown/img/video.mp4
rename to doc/user/img/markdown_video.mp4
diff --git a/doc/user/markdown.md b/doc/user/markdown.md
new file mode 100644
index 0000000000000000000000000000000000000000..7fe96e67dbbfd9028b8f6f0693acace82bbd68ed
--- /dev/null
+++ b/doc/user/markdown.md
@@ -0,0 +1,786 @@
+# Markdown
+
+## Table of Contents
+
+**[GitLab Flavored Markdown](#gitlab-flavored-markdown-gfm)**
+
+* [Newlines](#newlines)
+* [Multiple underscores in words](#multiple-underscores-in-words)
+* [URL auto-linking](#url-auto-linking)
+* [Multiline Blockquote](#multiline-blockquote)
+* [Code and Syntax Highlighting](#code-and-syntax-highlighting)
+* [Inline Diff](#inline-diff)
+* [Emoji](#emoji)
+* [Special GitLab references](#special-gitlab-references)
+* [Task Lists](#task-lists)
+* [Videos](#videos)
+
+**[Standard Markdown](#standard-markdown)**
+
+* [Headers](#headers)
+* [Emphasis](#emphasis)
+* [Lists](#lists)
+* [Links](#links)
+* [Images](#images)
+* [Blockquotes](#blockquotes)
+* [Inline HTML](#inline-html)
+* [Horizontal Rule](#horizontal-rule)
+* [Line Breaks](#line-breaks)
+* [Tables](#tables)
+
+**[Wiki-Specific Markdown](#wiki-specific-markdown)**
+
+* [Wiki - Direct page link](#wiki-direct-page-link)
+* [Wiki - Direct file link](#wiki-direct-file-link)
+* [Wiki - Hierarchical link](#wiki-hierarchical-link)
+* [Wiki - Root link](#wiki-root-link)
+
+**[References](#references)**
+
+## GitLab Flavored Markdown (GFM)
+
+> **Note:**
+Not all of the GitLab-specific extensions to Markdown that are described in
+this document currently work on our documentation website.
+>
+For the best result, we encourage you to check this document out as rendered
+by GitLab: [markdown.md]
+
+_GitLab uses the [Redcarpet Ruby library][redcarpet] for Markdown processing._
+
+GitLab uses "GitLab Flavored Markdown" (GFM). It extends the standard Markdown in a few significant ways to add some useful functionality. It was inspired by [GitHub Flavored Markdown](https://help.github.com/articles/basic-writing-and-formatting-syntax/).
+
+You can use GFM in the following areas:
+
+- comments
+- issues
+- merge requests
+- milestones
+- snippets (the snippet must be named with a `.md` extension)
+- wiki pages
+- markdown documents inside the repository
+
+You can also use other rich text files in GitLab. You might have to install a
+dependency to do so. Please see the [github-markup gem readme](https://github.com/gitlabhq/markup#markups) for more information.
+
+## Newlines
+
+> If this is not rendered correctly, see
+https://gitlab.com/gitlab-org/gitlab-ce/blob/master/doc/markdown/markdown.md#newlines
+
+GFM honors the markdown specification in how [paragraphs and line breaks are handled](https://daringfireball.net/projects/markdown/syntax#p).
+
+A paragraph is simply one or more consecutive lines of text, separated by one or more blank lines.
+Line-breaks, or softreturns, are rendered if you end a line with two or more spaces:
+
+    Roses are red [followed by two or more spaces]
+    Violets are blue
+
+    Sugar is sweet
+
+Roses are red  
+Violets are blue
+
+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
+
+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:
+
+    perform_complicated_task
+
+    do_this_and_do_that_and_another_thing
+
+perform_complicated_task
+
+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
+
+GFM will autolink almost any URL you copy and paste into your text:
+
+    * https://www.google.com
+    * https://google.com/
+    * ftp://ftp.us.debian.org/debian/
+    * smb://foo/bar/baz
+    * irc://irc.freenode.net/gitlab
+    * http://localhost:3000
+
+* https://www.google.com
+* https://google.com/
+* ftp://ftp.us.debian.org/debian/
+* smb://foo/bar/baz
+* irc://irc.freenode.net/gitlab
+* http://localhost:3000
+
+## Multiline Blockquote
+
+> If this is not rendered correctly, see
+https://gitlab.com/gitlab-org/gitlab-ce/blob/master/doc/markdown/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>:
+
+```no-highlight
+>>>
+If you paste a message from somewhere else
+
+that
+
+spans
+
+multiple lines,
+
+you can quote that without having to manually prepend `>` to every line!
+>>>
+```
+
+>>>
+If you paste a message from somewhere else
+
+that
+
+spans
+
+multiple lines,
+
+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
+
+_GitLab uses the [Rouge Ruby library][rouge] for syntax highlighting. For a
+list of supported languages visit the Rouge website._
+
+Blocks of code are either fenced by lines with three back-ticks <code>```</code>,
+or are indented with four spaces. Only the fenced code blocks support syntax
+highlighting:
+
+```no-highlight
+Inline `code` has `back-ticks around` it.
+```
+
+Inline `code` has `back-ticks around` it.
+
+Example:
+
+    ```javascript
+    var s = "JavaScript syntax highlighting";
+    alert(s);
+    ```
+
+    ```python
+    def function():
+        #indenting works just fine in the fenced code block
+        s = "Python syntax highlighting"
+        print s
+    ```
+
+    ```ruby
+    require 'redcarpet'
+    markdown = Redcarpet.new("Hello World!")
+    puts markdown.to_html
+    ```
+
+    ```
+    No language indicated, so no syntax highlighting.
+    s = "There is no highlighting for this."
+    But let's throw in a <b>tag</b>.
+    ```
+
+becomes:
+
+```javascript
+var s = "JavaScript syntax highlighting";
+alert(s);
+```
+
+```python
+def function():
+    #indenting works just fine in the fenced code block
+    s = "Python syntax highlighting"
+    print s
+```
+
+```ruby
+require 'redcarpet'
+markdown = Redcarpet.new("Hello World!")
+puts markdown.to_html
+```
+
+```
+No language indicated, so no syntax highlighting.
+s = "There is no highlighting for this."
+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
+
+With inline diffs tags you can display {+ additions +} or [- deletions -].
+
+The wrapping tags can be either curly braces or square brackets [+ additions +] or {- deletions -}.
+
+However the wrapping tags cannot be mixed as such:
+
+- {+ additions +]
+- [+ additions +}
+- {- deletions -]
+- [- deletions -}
+
+## Emoji
+
+> If this is not rendered correctly, see
+https://gitlab.com/gitlab-org/gitlab-ce/blob/master/doc/markdown/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:
+
+	:zap: You can use emoji anywhere GFM is supported. :v:
+
+	You can use it to point out a :bug: or warn about :speak_no_evil: patches. And if someone improves your really :snail: code, send them some :birthday:. People will :heart: you for that.
+
+	If you are new to this, don't be :fearful:. You can easily join the emoji :family:. All you need to do is to look up on the supported codes.
+
+	Consult the [Emoji Cheat Sheet](http://emoji.codes) for a list of all supported emoji codes. :thumbsup:
+
+Sometimes you want to :monkey: around a bit and add some :star2: to your :speech_balloon:. Well we have a gift for you:
+
+:zap: You can use emoji anywhere GFM is supported. :v:
+
+You can use it to point out a :bug: or warn about :speak_no_evil: patches. And if someone improves your really :snail: code, send them some :birthday:. People will :heart: you for that.
+
+If you are new to this, don't be :fearful:. You can easily join the emoji :family:. All you need to do is to look up on the supported codes.
+
+Consult the [Emoji Cheat Sheet](http://emoji.codes) for a list of all supported emoji codes. :thumbsup:
+
+## Special GitLab References
+
+GFM recognizes special references.
+
+You can easily reference e.g. an issue, a commit, a team member or even the whole team within a project.
+
+GFM will turn that reference into a link so you can navigate between them easily.
+
+GFM will recognize the following:
+
+| input                  | references                   |
+|:-----------------------|:---------------------------  |
+| `@user_name`           | specific user                |
+| `@group_name`          | specific group               |
+| `@all`                 | entire team                  |
+| `#123`                 | issue                        |
+| `!123`                 | merge request                |
+| `$123`                 | snippet                      |
+| `~123`                 | label by ID                  |
+| `~bug`                 | one-word label by name       |
+| `~"feature request"`   | multi-word label by name     |
+| `%123`                 | milestone by ID              |
+| `%v1.23`               | one-word milestone by name   |
+| `%"release candidate"` | multi-word milestone by name |
+| `9ba12248`             | specific commit              |
+| `9ba12248...b19a04f5`  | commit range comparison      |
+| `[README](doc/README)` | repository file references   |
+
+GFM also recognizes certain cross-project references:
+
+| input                                   | references              |
+|:----------------------------------------|:------------------------|
+| `namespace/project#123`                 | issue                   |
+| `namespace/project!123`                 | merge request           |
+| `namespace/project%123`                 | milestone               |
+| `namespace/project$123`                 | snippet                 |
+| `namespace/project@9ba12248`            | specific commit         |
+| `namespace/project@9ba12248...b19a04f5` | commit range comparison |
+| `namespace/project~"Some label"`        | issues with given label |
+
+## Task Lists
+
+> If this is not rendered correctly, see
+https://gitlab.com/gitlab-org/gitlab-ce/blob/master/doc/markdown/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:
+
+```no-highlight
+- [x] Completed task
+- [ ] Incomplete task
+    - [ ] Sub-task 1
+    - [x] Sub-task 2
+    - [ ] Sub-task 3
+```
+
+- [x] Completed task
+- [ ] Incomplete task
+    - [ ] Sub-task 1
+    - [x] Sub-task 2
+    - [ ] Sub-task 3
+
+Task lists can only be created in descriptions, not in titles. Task item state can be managed by editing the description's Markdown or by toggling the rendered check boxes.
+
+## Videos
+
+> If this is not rendered correctly, see
+https://gitlab.com/gitlab-org/gitlab-ce/blob/master/doc/markdown/markdown.md#videos
+
+Image tags with a video extension are automatically converted to a video player.
+
+The valid video extensions are `.mp4`, `.m4v`, `.mov`, `.webm`, and `.ogv`.
+
+    Here's a sample video:
+
+    ![Sample Video](img/markdown_video.mp4)
+
+Here's a sample video:
+
+![Sample Video](img/markdown_video.mp4)
+
+# Standard Markdown
+
+## Headers
+
+```no-highlight
+# H1
+## H2
+### H3
+#### H4
+##### H5
+###### H6
+
+Alternatively, for H1 and H2, an underline-ish style:
+
+Alt-H1
+======
+
+Alt-H2
+------
+```
+
+# H1
+## H2
+### H3
+#### H4
+##### H5
+###### H6
+
+Alternatively, for H1 and H2, an underline-ish style:
+
+Alt-H1
+======
+
+Alt-H2
+------
+
+### Header IDs and links
+
+All Markdown-rendered headers automatically get IDs, except in comments.
+
+On hover a link to those IDs becomes visible to make it easier to copy the link to the header to give it to someone else.
+
+The IDs are generated from the content of the header according to the following rules:
+
+1. All text is converted to lowercase
+1. All non-word text (e.g., punctuation, HTML) is removed
+1. All spaces are converted to hyphens
+1. Two or more hyphens in a row are converted to one
+1. If a header with the same ID has already been generated, a unique
+   incrementing number is appended, starting at 1.
+
+For example:
+
+```
+# This header has spaces in it
+## This header has a :thumbsup: in it
+# This header has Unicode in it: 한글
+## This header has spaces in it
+### This header has spaces in it
+```
+
+Would generate the following link IDs:
+
+1. `this-header-has-spaces-in-it`
+1. `this-header-has-a-in-it`
+1. `this-header-has-unicode-in-it-한글`
+1. `this-header-has-spaces-in-it`
+1. `this-header-has-spaces-in-it-1`
+
+Note that the Emoji processing happens before the header IDs are generated, so the Emoji is converted to an image which then gets removed from the ID.
+
+## Emphasis
+
+```no-highlight
+Emphasis, aka italics, with *asterisks* or _underscores_.
+
+Strong emphasis, aka bold, with **asterisks** or __underscores__.
+
+Combined emphasis with **asterisks and _underscores_**.
+
+Strikethrough uses two tildes. ~~Scratch this.~~
+```
+
+Emphasis, aka italics, with *asterisks* or _underscores_.
+
+Strong emphasis, aka bold, with **asterisks** or __underscores__.
+
+Combined emphasis with **asterisks and _underscores_**.
+
+Strikethrough uses two tildes. ~~Scratch this.~~
+
+## Lists
+
+```no-highlight
+1. First ordered list item
+2. Another item
+  * Unordered sub-list.
+1. Actual numbers don't matter, just that it's a number
+  1. Ordered sub-list
+4. And another item.
+
+* Unordered list can use asterisks
+- Or minuses
++ Or pluses
+```
+
+1. First ordered list item
+2. Another item
+  * Unordered sub-list.
+1. Actual numbers don't matter, just that it's a number
+  1. Ordered sub-list
+4. And another item.
+
+* Unordered list can use asterisks
+- Or minuses
++ Or pluses
+
+If a list item contains multiple paragraphs,
+each subsequent paragraph should be indented with four spaces.
+
+```no-highlight
+1.  First ordered list item
+
+    Second paragraph of first item.
+2.  Another item
+```
+
+1.  First ordered list item
+
+    Second paragraph of first item.
+2.  Another item
+
+If the second paragraph isn't indented with four spaces,
+the second list item will be incorrectly labeled as `1`.
+
+```no-highlight
+1. First ordered list item
+
+   Second paragraph of first item.
+2. Another item
+```
+
+1. First ordered list item
+
+   Second paragraph of first item.
+2. Another item
+
+## Links
+
+There are two ways to create links, inline-style and reference-style.
+
+    [I'm an inline-style link](https://www.google.com)
+
+    [I'm a reference-style link][Arbitrary case-insensitive reference text]
+
+    [I'm a relative reference to a repository file](LICENSE)
+
+    [You can use numbers for reference-style link definitions][1]
+
+    Or leave it empty and use the [link text itself][]
+
+    Some text to show that the reference links can follow later.
+
+    [arbitrary case-insensitive reference text]: https://www.mozilla.org
+    [1]: http://slashdot.org
+    [link text itself]: https://www.reddit.com
+
+[I'm an inline-style link](https://www.google.com)
+
+[I'm a reference-style link][Arbitrary case-insensitive reference text]
+
+[I'm a relative reference to a repository file](LICENSE)[^1]
+
+[You can use numbers for reference-style link definitions][1]
+
+Or leave it empty and use the [link text itself][]
+
+Some text to show that the reference links can follow later.
+
+[arbitrary case-insensitive reference text]: https://www.mozilla.org
+[1]: http://slashdot.org
+[link text itself]: https://www.reddit.com
+
+**Note**
+
+Relative links do not allow referencing project files in a wiki page or wiki page in a project file. The reason for this is that, in GitLab, wiki is always a separate git repository. For example:
+
+`[I'm a reference-style link](style)`
+
+will point the link to `wikis/style` when the link is inside of a wiki markdown file.
+
+## Images
+
+    Here's our logo (hover to see the title text):
+
+    Inline-style:
+    ![alt text](img/markdown_logo.png)
+
+    Reference-style:
+    ![alt text1][logo]
+
+    [logo]: img/markdown_logo.png
+
+Here's our logo:
+
+Inline-style:
+
+![alt text](img/markdown_logo.png)
+
+Reference-style:
+
+![alt text][logo]
+
+[logo]: img/markdown_logo.png
+
+## Blockquotes
+
+```no-highlight
+> Blockquotes are very handy in email to emulate reply text.
+> This line is part of the same quote.
+
+Quote break.
+
+> This is a very long line that will still be quoted properly when it wraps. Oh boy let's keep writing to make sure this is long enough to actually wrap for everyone. Oh, you can *put* **Markdown** into a blockquote.
+```
+
+> Blockquotes are very handy in email to emulate reply text.
+> This line is part of the same quote.
+
+Quote break.
+
+> This is a very long line that will still be quoted properly when it wraps. Oh boy let's keep writing to make sure this is long enough to actually wrap for everyone. Oh, you can *put* **Markdown** into a blockquote.
+
+## Inline HTML
+
+You can also use raw HTML in your Markdown, and it'll mostly work pretty well.
+
+See the documentation for HTML::Pipeline's [SanitizationFilter](http://www.rubydoc.info/gems/html-pipeline/HTML/Pipeline/SanitizationFilter#WHITELIST-constant) class for the list of allowed HTML tags and attributes.  In addition to the default `SanitizationFilter` whitelist, GitLab allows `span` elements.
+
+```no-highlight
+<dl>
+  <dt>Definition list</dt>
+  <dd>Is something people use sometimes.</dd>
+
+  <dt>Markdown in HTML</dt>
+  <dd>Does *not* work **very** well. Use HTML <em>tags</em>.</dd>
+</dl>
+```
+
+<dl>
+  <dt>Definition list</dt>
+  <dd>Is something people use sometimes.</dd>
+
+  <dt>Markdown in HTML</dt>
+  <dd>Does *not* work **very** well. Use HTML <em>tags</em>.</dd>
+</dl>
+
+## Horizontal Rule
+
+```
+Three or more...
+
+---
+
+Hyphens
+
+***
+
+Asterisks
+
+___
+
+Underscores
+```
+
+Three or more...
+
+---
+
+Hyphens
+
+***
+
+Asterisks
+
+___
+
+Underscores
+
+## Line Breaks
+
+My basic recommendation for learning how line breaks work is to experiment and discover -- hit &lt;Enter&gt; once (i.e., insert one newline), then hit it twice (i.e., insert two newlines), see what happens. You'll soon learn to get what you want. "Markdown Toggle" is your friend.
+
+Here are some things to try out:
+
+```
+Here's a line for us to start with.
+
+This line is separated from the one above by two newlines, so it will be a *separate paragraph*.
+
+This line is also a separate paragraph, but...
+This line is only separated by a single newline, so it's a separate line in the *same paragraph*.
+
+This line is also a separate paragraph, and...
+This line is on its own line, because the previous line ends with two
+spaces.
+```
+
+Here's a line for us to start with.
+
+This line is separated from the one above by two newlines, so it will be a *separate paragraph*.
+
+This line is also begins a separate paragraph, but...
+This line is only separated by a single newline, so it's a separate line in the *same paragraph*.
+
+This line is also a separate paragraph, and...
+This line is on its own line, because the previous line ends with two
+spaces.
+
+## Tables
+
+Tables aren't part of the core Markdown spec, but they are part of GFM and Markdown Here supports them.
+
+```
+| header 1 | header 2 |
+| -------- | -------- |
+| cell 1   | cell 2   |
+| cell 3   | cell 4   |
+```
+
+Code above produces next output:
+
+| header 1 | header 2 |
+| -------- | -------- |
+| cell 1   | cell 2   |
+| cell 3   | cell 4   |
+
+**Note**
+
+The row of dashes between the table header and body must have at least three dashes in each column.
+
+By including colons in the header row, you can align the text within that column:
+
+```
+| Left Aligned | Centered | Right Aligned | Left Aligned | Centered | Right Aligned |
+| :----------- | :------: | ------------: | :----------- | :------: | ------------: |
+| Cell 1       | Cell 2   | Cell 3        | Cell 4       | Cell 5   | Cell 6        |
+| Cell 7       | Cell 8   | Cell 9        | Cell 10      | Cell 11  | Cell 12       |
+```
+
+| Left Aligned | Centered | Right Aligned | Left Aligned | Centered | Right Aligned |
+| :----------- | :------: | ------------: | :----------- | :------: | ------------: |
+| Cell 1       | Cell 2   | Cell 3        | Cell 4       | Cell 5   | Cell 6        |
+| Cell 7       | Cell 8   | Cell 9        | Cell 10      | Cell 11  | Cell 12       |
+
+
+## Wiki-specific Markdown
+
+The following examples show how links inside wikis behave.
+
+### Wiki - Direct page link
+
+A link which just includes the slug for a page will point to that page,
+_at the base level of the wiki_.
+
+This snippet would link to a `documentation` page at the root of your wiki:
+
+```markdown
+[Link to Documentation](documentation)
+```
+
+### Wiki - Direct file link
+
+Links with a file extension point to that file, _relative to the current page_.
+
+If this snippet was placed on a page at `<your_wiki>/documentation/related`,
+it would link to `<your_wiki>/documentation/file.md`:
+
+```markdown
+[Link to File](file.md)
+```
+
+### Wiki - Hierarchical link
+
+A link can be constructed relative to the current wiki page using `./<page>`,
+`../<page>`, etc.
+
+- If this snippet was placed on a page at `<your_wiki>/documentation/main`,
+  it would link to `<your_wiki>/documentation/related`:
+
+	```markdown
+	[Link to Related Page](./related)
+	```
+
+- If this snippet was placed on a page at `<your_wiki>/documentation/related/content`,
+  it would link to `<your_wiki>/documentation/main`:
+
+	```markdown
+	[Link to Related Page](../main)
+	```
+
+- If this snippet was placed on a page at `<your_wiki>/documentation/main`,
+  it would link to `<your_wiki>/documentation/related.md`:
+
+	```markdown
+	[Link to Related Page](./related.md)
+	```
+
+- If this snippet was placed on a page at `<your_wiki>/documentation/related/content`,
+  it would link to `<your_wiki>/documentation/main.md`:
+
+	```markdown
+	[Link to Related Page](../main.md)
+	```
+
+### Wiki - Root link
+
+A link starting with a `/` is relative to the wiki root.
+
+- This snippet links to `<wiki_root>/documentation`:
+
+	```markdown
+	[Link to Related Page](/documentation)
+	```
+
+- This snippet links to `<wiki_root>/miscellaneous.md`:
+
+	```markdown
+	[Link to Related Page](/miscellaneous.md)
+	```
+## References
+
+- This document leveraged heavily from the [Markdown-Cheatsheet](https://github.com/adam-p/markdown-here/wiki/Markdown-Cheatsheet).
+- 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
+[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/project/builds/artifacts.md b/doc/user/project/builds/artifacts.md
new file mode 100644
index 0000000000000000000000000000000000000000..c93ae1c369ca8892ff9ddf2c4ea910f0eb58baba
--- /dev/null
+++ b/doc/user/project/builds/artifacts.md
@@ -0,0 +1,104 @@
+# Introduction to build artifacts
+
+>**Notes:**
+>- Since GitLab 8.2 and GitLab Runner 0.7.0, build artifacts that are created by
+   GitLab Runner are uploaded to GitLab and are downloadable as a single archive
+   (`tar.gz`) using the GitLab UI.
+>- Starting from GitLab 8.4 and GitLab Runner 1.0, the artifacts archive format
+   changed to `ZIP`, and it is now possible to browse its contents, with the added
+   ability of downloading the files separately.
+>- The artifacts browser will be available only for new artifacts that are sent
+   to GitLab using GitLab Runner version 1.0 and up. It will not be possible to
+   browse old artifacts already uploaded to GitLab.
+>- This is the user documentation. For the administration guide see
+   [administration/build_artifacts.md](../../../administration/build_artifacts.md).
+
+Artifacts is a list of files and directories which are attached to a build
+after it completes successfully.  This feature is enabled by default in all GitLab installations.
+
+## Defining artifacts in `.gitlab-ci.yml`
+
+A simple example of using the artifacts definition in `.gitlab-ci.yml` would be
+the following:
+
+```yaml
+pdf:
+  script: xelatex mycv.tex
+  artifacts:
+    paths:
+    - mycv.pdf
+```
+
+A job named `pdf` calls the `xelatex` command in order to build a pdf file from
+the latex source file `mycv.tex`. We then define the `artifacts` paths which in
+turn are defined with the `paths` keyword. All paths to files and directories
+are relative to the repository that was cloned during the build.
+
+For more examples on artifacts, follow the artifacts reference in
+[`.gitlab-ci.yml` documentation](../../../ci/yaml/README.md#artifacts).
+
+## Browsing build artifacts
+
+When GitLab receives an artifacts archive, an archive metadata file is also
+generated. This metadata file describes all the entries that are located in the
+artifacts archive itself. The metadata file is in a binary format, with
+additional GZIP compression.
+
+GitLab does not extract the artifacts archive in order to save space, memory
+and disk I/O. It instead inspects the metadata file which contains all the
+relevant information. This is especially important when there is a lot of
+artifacts, or an archive is a very large file.
+
+---
+
+After a build finishes, if you visit the build's specific page, you can see
+that there are two buttons. One is for downloading the artifacts archive and
+the other for browsing its contents.
+
+![Build artifacts browser button](img/build_artifacts_browser_button.png)
+
+---
+
+The archive browser shows the name and the actual file size of each file in the
+archive. If your artifacts contained directories, then you are also able to
+browse inside them.
+
+Below you can see how browsing looks like. In this case we have browsed inside
+the archive and at this point there is one directory and one HTML file.
+
+![Build artifacts browser](img/build_artifacts_browser.png)
+
+---
+
+## Downloading build artifacts
+
+>**Note:**
+GitLab does not extract the entire artifacts archive to send just a single file
+to the user. When clicking on a specific file, [GitLab Workhorse] extracts it
+from the archive and the download begins. This implementation saves space,
+memory and disk I/O.
+
+If you need to download the whole archive, there are buttons in various places
+inside GitLab that make that possible.
+
+1. While on the pipelines page, you can see the download icon for each build's
+   artifacts archive in the right corner:
+
+    ![Build artifacts in Pipelines page](img/build_artifacts_pipelines_page.png)
+
+1. While on the builds page, you can see the download icon for each build's
+   artifacts archive in the right corner:
+
+    ![Build artifacts in Builds page](img/build_artifacts_builds_page.png)
+
+1. While inside a specific build, you are presented with a download button
+   along with the one that browses the archive:
+
+    ![Build artifacts browser button](img/build_artifacts_browser_button.png)
+
+1. And finally, when browsing an archive you can see the download button at
+   the top right corner:
+
+    ![Build artifacts browser](img/build_artifacts_browser.png)
+
+[gitlab workhorse]: https://gitlab.com/gitlab-org/gitlab-workhorse "GitLab Workhorse repository"
diff --git a/doc/user/project/builds/img/build_artifacts_browser.png b/doc/user/project/builds/img/build_artifacts_browser.png
new file mode 100644
index 0000000000000000000000000000000000000000..d95e2800c0ff872dca34e63f0eeb9cb859203b85
Binary files /dev/null and b/doc/user/project/builds/img/build_artifacts_browser.png differ
diff --git a/doc/user/project/builds/img/build_artifacts_browser_button.png b/doc/user/project/builds/img/build_artifacts_browser_button.png
new file mode 100644
index 0000000000000000000000000000000000000000..463540634e35a39af62be89c4c16e7864bf1c0de
Binary files /dev/null and b/doc/user/project/builds/img/build_artifacts_browser_button.png differ
diff --git a/doc/user/project/builds/img/build_artifacts_builds_page.png b/doc/user/project/builds/img/build_artifacts_builds_page.png
new file mode 100644
index 0000000000000000000000000000000000000000..db78386ba7bb8be33e069f92559241777e7ea544
Binary files /dev/null and b/doc/user/project/builds/img/build_artifacts_builds_page.png differ
diff --git a/doc/user/project/builds/img/build_artifacts_pipelines_page.png b/doc/user/project/builds/img/build_artifacts_pipelines_page.png
new file mode 100644
index 0000000000000000000000000000000000000000..6c2d1a4bdc7225eaa3f32dfae6c582e6654738d3
Binary files /dev/null and b/doc/user/project/builds/img/build_artifacts_pipelines_page.png differ
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/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..accd0900cbbda0d74e19cc41ef545b827ffe243b
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/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..9df9ed9c9da1d1eaa10d6715afc2ff6ef9bbdfdd
--- /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.
+
+![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/koding.md b/doc/user/project/koding.md
new file mode 100644
index 0000000000000000000000000000000000000000..e54587fab682a4f720b34a7283ea92b727a2604a
--- /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 to 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 4258185b7d08cd7cec24bed4dc2abba80106542a..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
@@ -22,34 +22,47 @@ created yet.
 
 ![Generate new labels](img/labels_generate.png)
 
+Creating a new label from scratch is as easy as pressing the **New label**
+button. From there on you can choose the name, give it an optional description,
+a color and you are set.
+
+When you are ready press the **Create label** button to create the new label.
+
+![New label](img/labels_new_label.png)
+
 ---
 
-You can skip that and create a new label or click that link and GitLab will
-generate a set of predefined labels for you. There 8 default generated labels
+## Default Labels
+
+It's possible to populate the labels for your project from a set of predefined labels.
+
+### Generate GitLab's predefined label set
+
+![Generate new labels](img/labels_generate.png)
+
+Click the link to 'Generate a default set of labels' and GitLab will
+generate a set of predefined labels for you. There are 8 default generated labels
 in total and you can see them in the screenshot below.
 
 ![Default generated labels](img/labels_default.png)
 
 ---
 
-You can see that from the labels page you can have an overview of the number of
-issues and merge requests assigned to each label.
-
-Creating a new label from scratch is as easy as pressing the **New label**
-button. From there on you can choose the name, give it an optional description,
-a color and you are set.
+## Labels Overview
 
-When you are ready press the **Create label** button to create the new label.
+![Default generated labels](img/labels_default.png)
 
-![New label](img/labels_new_label.png)
+You can see that from the labels page you can have an overview of the number of
+issues and merge requests assigned to each label.
 
 ## Prioritize labels
 
 >**Notes:**
- - This feature was introduced in GitLab 8.9.
- - Priority sorting is based on the highest priority label only. This might
-   change in the future, follow the discussion in
-   https://gitlab.com/gitlab-org/gitlab-ce/issues/18554.
+>
+> - Introduced in GitLab 8.9.
+> - Priority sorting is based on the highest priority label only. This might
+>   change in the future, follow the discussion in
+>   https://gitlab.com/gitlab-org/gitlab-ce/issues/18554.
 
 Prioritized labels are like any other label, but sorted by priority. This allows
 you to sort issues and merge requests by priority.
@@ -87,8 +100,7 @@ important.
 
 ## Create a new label right from the issue tracker
 
->**Note:**
-This feature was introduced in GitLab 8.6.
+> Introduced in GitLab 8.6.
 
 There are times when you are already in the issue tracker searching for a
 label, only to realize it doesn't exist. Instead of going to the **Labels**
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_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/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/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..2559f5f5250e4174e049aaa956e6d972a1350e6d
--- /dev/null
+++ b/doc/user/project/merge_requests/merge_request_discussion_resolution.md
@@ -0,0 +1,40 @@
+# 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]
+
+[ce-5022]: https://gitlab.com/gitlab-org/gitlab-ce/merge_requests/5022
+[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/resolve_conflicts.md b/doc/user/project/merge_requests/resolve_conflicts.md
new file mode 100644
index 0000000000000000000000000000000000000000..44b76ffc8e689e867b2be786f8e8fb8bd6536f97
--- /dev/null
+++ b/doc/user/project/merge_requests/resolve_conflicts.md
@@ -0,0 +1,41 @@
+# 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 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/protected_branches.md b/doc/user/project/protected_branches.md
index 6a8170b5ecb9dbd365c0a7ee5b8af1188a1ed067..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,27 +35,45 @@ 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
 
->**Note:**
-This feature was [introduced][ce-4665] in GitLab 8.10.
+> [Introduced][ce-4665] in GitLab 8.10.
 
 You can specify a wildcard protected branch, which will protect all branches
 matching the wildcard. For example:
@@ -67,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/user/project/settings/import_export.md b/doc/user/project/settings/import_export.md
index 38e9786123dc580c24e050929efecc6642eec4fa..08ff89ce6aeee7c2b128074278b8c50728c92cd9 100644
--- a/doc/user/project/settings/import_export.md
+++ b/doc/user/project/settings/import_export.md
@@ -1,18 +1,18 @@
 # Project import/export
 
 >**Notes:**
-  - This feature was [introduced][ce-3050] in GitLab 8.9
-  - Importing will not be possible if the import instance version is lower
-    than that of the exporter.
-  - For existing installations, the project import option has to be enabled in
-    application settings (`/admin/application_settings`) under 'Import sources'.
-    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.
-  - The exports are stored in a temporary [shared directory][tmp] and are deleted
-    every 24 hours by a specific worker.
+>
+>  - [Introduced][ce-3050] in GitLab 8.9.
+>  - Importing will not be possible if the import instance version is lower
+>    than 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.
+>  - You can find some useful raketasks if you are an administrator in the
+>    [import_export](../../../administration/raketasks/project_import_export.md)
+>    raketask.
+>  - The exports are stored in a temporary [shared directory][tmp] and are deleted
+>    every 24 hours by a specific worker.
 
 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.
diff --git a/doc/user/project/slash_commands.md b/doc/user/project/slash_commands.md
new file mode 100644
index 0000000000000000000000000000000000000000..11e1574f7724df92a7bda282f3725337500f1356
--- /dev/null
+++ b/doc/user/project/slash_commands.md
@@ -0,0 +1,30 @@
+# 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 |
+| `/due <in 2 days or this Friday or December 31st>` | Set due date |
+| `/remove_due_date`          | Remove due date |
diff --git a/doc/web_hooks/web_hooks.md b/doc/web_hooks/web_hooks.md
index 8559b67af0465652f7f2406dd5874d5427cdd9da..33c1a79d59cb4e671478968f296d1eeffda423a7 100644
--- a/doc/web_hooks/web_hooks.md
+++ b/doc/web_hooks/web_hooks.md
@@ -26,6 +26,10 @@ GitLab webhooks keep in mind the following things:
     you are writing a low-level hook this is important to remember.
 -   GitLab ignores the HTTP status code returned by your endpoint.
 
+## Secret Token
+
+If you specify a secret token, it will be sent with the hook request in the `X-Gitlab-Token` HTTP header. Your webhook endpoint can check that to verify that the request is legitimate.
+
 ## SSL Verification
 
 By default, the SSL certificate of the webhook endpoint is verified based on
@@ -750,6 +754,174 @@ X-Gitlab-Event: Wiki Page Hook
 }
 ```
 
+## Pipeline events
+
+Triggered on status change of Pipeline.
+
+**Request Header**:
+
+```
+X-Gitlab-Event: Pipeline Hook
+```
+
+**Request Body**:
+
+```json
+{
+   "object_kind": "pipeline",
+   "object_attributes":{
+      "id": 31,
+      "ref": "master",
+      "tag": false,
+      "sha": "bcbb5ec396a2c0f828686f14fac9b80b780504f2",
+      "before_sha": "bcbb5ec396a2c0f828686f14fac9b80b780504f2",
+      "status": "success",
+      "stages":[
+         "build",
+         "test",
+         "deploy"
+      ],
+      "created_at": "2016-08-12 15:23:28 UTC",
+      "finished_at": "2016-08-12 15:26:29 UTC",
+      "duration": 63
+   },
+   "user":{
+      "name": "Administrator",
+      "username": "root",
+      "avatar_url": "http://www.gravatar.com/avatar/e32bd13e2add097461cb96824b7a829c?s=80\u0026d=identicon"
+   },
+   "project":{
+      "name": "Gitlab Test",
+      "description": "Atque in sunt eos similique dolores voluptatem.",
+      "web_url": "http://192.168.64.1:3005/gitlab-org/gitlab-test",
+      "avatar_url": null,
+      "git_ssh_url": "git@192.168.64.1:gitlab-org/gitlab-test.git",
+      "git_http_url": "http://192.168.64.1:3005/gitlab-org/gitlab-test.git",
+      "namespace": "Gitlab Org",
+      "visibility_level": 20,
+      "path_with_namespace": "gitlab-org/gitlab-test",
+      "default_branch": "master"
+   },
+   "commit":{
+      "id": "bcbb5ec396a2c0f828686f14fac9b80b780504f2",
+      "message": "test\n",
+      "timestamp": "2016-08-12T17:23:21+02:00",
+      "url": "http://example.com/gitlab-org/gitlab-test/commit/bcbb5ec396a2c0f828686f14fac9b80b780504f2",
+      "author":{
+         "name": "User",
+         "email": "user@gitlab.com"
+      }
+   },
+   "builds":[
+      {
+         "id": 380,
+         "stage": "deploy",
+         "name": "production",
+         "status": "skipped",
+         "created_at": "2016-08-12 15:23:28 UTC",
+         "started_at": null,
+         "finished_at": null,
+         "when": "manual",
+         "manual": true,
+         "user":{
+            "name": "Administrator",
+            "username": "root",
+            "avatar_url": "http://www.gravatar.com/avatar/e32bd13e2add097461cb96824b7a829c?s=80\u0026d=identicon"
+         },
+         "runner": null,
+         "artifacts_file":{
+            "filename": null,
+            "size": null
+         }
+      },
+      {
+         "id": 377,
+         "stage": "test",
+         "name": "test-image",
+         "status": "success",
+         "created_at": "2016-08-12 15:23:28 UTC",
+         "started_at": "2016-08-12 15:26:12 UTC",
+         "finished_at": null,
+         "when": "on_success",
+         "manual": false,
+         "user":{
+            "name": "Administrator",
+            "username": "root",
+            "avatar_url": "http://www.gravatar.com/avatar/e32bd13e2add097461cb96824b7a829c?s=80\u0026d=identicon"
+         },
+         "runner": null,
+         "artifacts_file":{
+            "filename": null,
+            "size": null
+         }
+      },
+      {
+         "id": 378,
+         "stage": "test",
+         "name": "test-build",
+         "status": "success",
+         "created_at": "2016-08-12 15:23:28 UTC",
+         "started_at": "2016-08-12 15:26:12 UTC",
+         "finished_at": "2016-08-12 15:26:29 UTC",
+         "when": "on_success",
+         "manual": false,
+         "user":{
+            "name": "Administrator",
+            "username": "root",
+            "avatar_url": "http://www.gravatar.com/avatar/e32bd13e2add097461cb96824b7a829c?s=80\u0026d=identicon"
+         },
+         "runner": null,
+         "artifacts_file":{
+            "filename": null,
+            "size": null
+         }
+      },
+      {
+         "id": 376,
+         "stage": "build",
+         "name": "build-image",
+         "status": "success",
+         "created_at": "2016-08-12 15:23:28 UTC",
+         "started_at": "2016-08-12 15:24:56 UTC",
+         "finished_at": "2016-08-12 15:25:26 UTC",
+         "when": "on_success",
+         "manual": false,
+         "user":{
+            "name": "Administrator",
+            "username": "root",
+            "avatar_url": "http://www.gravatar.com/avatar/e32bd13e2add097461cb96824b7a829c?s=80\u0026d=identicon"
+         },
+         "runner": null,
+         "artifacts_file":{
+            "filename": null,
+            "size": null
+         }
+      },
+      {
+         "id": 379,
+         "stage": "deploy",
+         "name": "staging",
+         "status": "created",
+         "created_at": "2016-08-12 15:23:28 UTC",
+         "started_at": null,
+         "finished_at": null,
+         "when": "on_success",
+         "manual": false,
+         "user":{
+            "name": "Administrator",
+            "username": "root",
+            "avatar_url": "http://www.gravatar.com/avatar/e32bd13e2add097461cb96824b7a829c?s=80\u0026d=identicon"
+         },
+         "runner": null,
+         "artifacts_file":{
+            "filename": null,
+            "size": null
+         }
+      }
+   ]
+}
+```
+
 #### Example webhook receiver
 
 If you want to see GitLab's webhooks in action for testing purposes you can use
diff --git a/doc/workflow/README.md b/doc/workflow/README.md
index 49dec613716562bb6108952fcd3aaaad5d2ab27d..0cf56449de230e7774cfaa82572c0e1f33edeb90 100644
--- a/doc/workflow/README.md
+++ b/doc/workflow/README.md
@@ -2,9 +2,11 @@
 
 - [Authorization for merge requests](authorization_for_merge_requests.md)
 - [Change your time zone](timezone.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,6 +15,7 @@
 - [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)
diff --git a/doc/workflow/award_emoji.md b/doc/workflow/award_emoji.md
index e6f8b792707450f786498b1ab24dddc900d92d95..1df0698afd016e4b6deb73ac9a2ac87e4b4faf1a 100644
--- a/doc/workflow/award_emoji.md
+++ b/doc/workflow/award_emoji.md
@@ -1,7 +1,7 @@
 # Award emoji
 
 >**Note:**
-This feature was [introduced][1825] in GitLab 8.2.
+[Introduced][1825] in GitLab 8.2.
 
 When you're collaborating online, you get fewer opportunities for high-fives
 and thumbs-ups. Emoji can be awarded to issues and merge requests, making
@@ -16,7 +16,7 @@ award emoji.
 ## Sort issues and merge requests on vote count
 
 >**Note:**
-This feature was [introduced][2871] in GitLab 8.5.
+[Introduced][2871] in GitLab 8.5.
 
 You can quickly sort issues and merge requests by the number of votes they
 have received. The sort options can be found in the dropdown menu as "Most
@@ -45,7 +45,7 @@ downvotes.
 ## Award emoji for comments
 
 >**Note:**
-This feature was [introduced][4291] in GitLab 8.9.
+[Introduced][4291] in GitLab 8.9.
 
 Award emoji can also be applied to individual comments when you want to
 celebrate an accomplishment or agree with an opinion.
diff --git a/doc/workflow/cherry_pick_changes.md b/doc/workflow/cherry_pick_changes.md
index 4a4990098429c19e989b1570da951fc2c6a00ace..64b94d810242317c9f6c70827ed527ab4ca39767 100644
--- a/doc/workflow/cherry_pick_changes.md
+++ b/doc/workflow/cherry_pick_changes.md
@@ -1,7 +1,6 @@
 # Cherry-pick changes
 
->**Note:**
-This feature was [introduced][ce-3514] in GitLab 8.7.
+> [Introduced][ce-3514] in GitLab 8.7.
 
 ---
 
diff --git a/doc/workflow/file_finder.md b/doc/workflow/file_finder.md
index b69ae663272859d5c35d03ae8ea41b20f40179db..8d87b030c834f8ea6104f32f9f2ad4d81d58801b 100644
--- a/doc/workflow/file_finder.md
+++ b/doc/workflow/file_finder.md
@@ -1,6 +1,6 @@
 # File finder
 
-_**Note:** This feature was [introduced][gh-9889] in GitLab 8.4._
+> [Introduced][gh-9889] in GitLab 8.4.
 
 ---
 
diff --git a/doc/workflow/gitlab_flow.md b/doc/workflow/gitlab_flow.md
index 2b2f140f8bf195ad357b607d0337771023b549fd..8119324bb62055bd6260554f3af00916a47d5272 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/).
 
diff --git a/doc/workflow/importing/import_projects_from_github.md b/doc/workflow/importing/import_projects_from_github.md
index a2b2a4b88f9d6690d525c207fa01be3d50f1958c..306caabf6e6fb1dcd0524f90817d65a5dc18a750 100644
--- a/doc/workflow/importing/import_projects_from_github.md
+++ b/doc/workflow/importing/import_projects_from_github.md
@@ -18,9 +18,6 @@ At its current state, GitHub importer can import:
 
 With GitLab 8.7+, references to pull requests and issues are preserved.
 
-It is not yet possible to import your cross-repository pull requests (those from
-forks). We are working on improving this in the near future.
-
 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
diff --git a/doc/workflow/notifications.md b/doc/workflow/notifications.md
index b4a9c2f3d3e622a837cdc943607261656efecd64..1b49a5c385fa8fba633bf1a2271329b80088c1bf 100644
--- a/doc/workflow/notifications.md
+++ b/doc/workflow/notifications.md
@@ -67,7 +67,7 @@ In all of the below cases, the notification will be sent to:
 - Participants:
   - the author and assignee of the issue/merge request
   - 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
@@ -89,6 +89,11 @@ In all of the below cases, the notification will be sent to:
 | Merge merge request    | |
 | New comment            | The above, plus anyone mentioned by `@username` in the comment, with notification level "Mention" or higher |
 
+
+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
 somebody else comments or adds changes to the ones that you've created or
diff --git a/doc/workflow/revert_changes.md b/doc/workflow/revert_changes.md
index 399366b0cdc395fa2696d800150b1c04d0be85b4..5ead9f4177f2bb35a85a1a971330b1be64d61772 100644
--- a/doc/workflow/revert_changes.md
+++ b/doc/workflow/revert_changes.md
@@ -1,6 +1,6 @@
 # Reverting changes
 
-_**Note:** This feature was [introduced][ce-1990] in GitLab 8.5._
+> [Introduced][ce-1990] in GitLab 8.5.
 
 ---
 
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/shortcuts.md b/doc/workflow/shortcuts.md
index ffcb832cdd7999fe6b99b851f34918181dfad8ae..36516883ef6234f76a82d1dd4ff31153dc14fa59 100644
--- a/doc/workflow/shortcuts.md
+++ b/doc/workflow/shortcuts.md
@@ -2,4 +2,75 @@
 
 You can see GitLab's keyboard shortcuts by using 'shift + ?'
 
-![Shortcuts](shortcuts.png)
\ No newline at end of file
+## Global Shortcuts
+
+| Keyboard Shortcut | Description |
+| ----------------- | ----------- |
+| <kbd>s</kbd> | Focus search |
+| <kbd>?</kbd> | Show/hide this dialog |
+| <kbd>⌘</kbd> + <kbd>shift</kbd> + <kbd>p</kbd> | Toggle markdown preview |
+| <kbd>↑</kbd> | Edit last comment (when focused on an empty textarea) |
+
+## Project Files Browsing
+
+| Keyboard Shortcut | Description |
+| ----------------- | ----------- |
+| <kbd>↑</kbd> | Move selection up |
+| <kbd>↓</kbd> | Move selection down |
+| <kbd>enter</kbd> | Open selection |
+
+## Finding Project File
+
+| Keyboard Shortcut | Description |
+| ----------------- | ----------- |
+| <kbd>↑</kbd> | Move selection up |
+| <kbd>↓</kbd> | Move selection down |
+| <kbd>enter</kbd> | Open selection |
+| <kbd>esc</kbd> | Go back |
+
+## Global Dashboard
+
+| Keyboard Shortcut | Description |
+| ----------------- | ----------- |
+| <kbd>g</kbd> + <kbd>a</kbd> | Go to the activity feed |
+| <kbd>g</kbd> + <kbd>p</kbd> | Go to projects |
+| <kbd>g</kbd> + <kbd>i</kbd> | Go to issues |
+| <kbd>g</kbd> + <kbd>m</kbd> | Go to merge requests |
+
+## Project
+
+| Keyboard Shortcut | Description |
+| ----------------- | ----------- |
+| <kbd>g</kbd> + <kbd>p</kbd> | Go to the project's home page |
+| <kbd>g</kbd> + <kbd>e</kbd> | Go to the project's activity feed |
+| <kbd>g</kbd> + <kbd>f</kbd> | Go to files |
+| <kbd>g</kbd> + <kbd>c</kbd> | Go to commits |
+| <kbd>g</kbd> + <kbd>b</kbd> | Go to builds |
+| <kbd>g</kbd> + <kbd>n</kbd> | Go to network graph |
+| <kbd>g</kbd> + <kbd>g</kbd> | Go to graphs |
+| <kbd>g</kbd> + <kbd>i</kbd> | Go to issues |
+| <kbd>g</kbd> + <kbd>m</kbd> | Go to merge requests |
+| <kbd>g</kbd> + <kbd>s</kbd> | Go to snippets |
+| <kbd>t</kbd> | Go to finding file |
+| <kbd>i</kbd> | New issue |
+
+## Network Graph
+
+| Keyboard Shortcut | Description |
+| ----------------- | ----------- |
+| <kbd>←</kbd> or <kbd>h</kbd> | Scroll left |
+| <kbd>→</kbd> or <kbd>l</kbd> | Scroll right |
+| <kbd>↑</kbd> or <kbd>k</kbd> | Scroll up |
+| <kbd>↓</kbd> or <kbd>j</kbd> | Scroll down |
+| <kbd>shift</kbd> + <kbd>↑</kbd> or <kbd>shift</kbd> + <kbd>k</kbd> | Scroll to top |
+| <kbd>shift</kbd> + <kbd>↓</kbd> or <kbd>shift</kbd> + <kbd>j</kbd> | Scroll to bottom |
+
+## Issues and Merge Requests
+
+| Keyboard Shortcut | Description |
+| ----------------- | ----------- |
+| <kbd>a</kbd> | Change assignee |
+| <kbd>m</kbd> | Change milestone |
+| <kbd>r</kbd> | Reply (quoting selected text) |
+| <kbd>e</kbd> | Edit issue/merge request |
+| <kbd>l</kbd> | Change label |
\ No newline at end of file
diff --git a/doc/workflow/shortcuts.png b/doc/workflow/shortcuts.png
deleted file mode 100644
index a9b1c4b4dccf119a33781fd5e839e3dc4d6a808f..0000000000000000000000000000000000000000
Binary files a/doc/workflow/shortcuts.png and /dev/null differ
diff --git a/doc/workflow/todos.md b/doc/workflow/todos.md
index 9524ffd54200ad960e287a7c725b084816435fcb..a50ba305deb392936278ef3540c414cfbedf3302 100644
--- a/doc/workflow/todos.md
+++ b/doc/workflow/todos.md
@@ -1,6 +1,6 @@
 # GitLab Todos
 
->**Note:** This feature was [introduced][ce-2817] in GitLab 8.5.
+> [Introduced][ce-2817] in GitLab 8.5.
 
 When you log into GitLab, you normally want to see where you should spend your
 time and take some action, or what you need to keep an eye on. All without the
diff --git a/doc/workflow/web_editor.md b/doc/workflow/web_editor.md
index 1832567a34c94dd783b7b3d257e549109c370c4e..ee8e78625727bd8d51a835c6a2852492a22c3326 100644
--- a/doc/workflow/web_editor.md
+++ b/doc/workflow/web_editor.md
@@ -70,8 +70,7 @@ There are multiple ways to create a branch from GitLab's web interface.
 
 ### Create a new branch from an issue
 
->**Note:**
-This feature was [introduced][ce-2808] in GitLab 8.6.
+> [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
diff --git a/features/dashboard/new_project.feature b/features/dashboard/new_project.feature
index 8ddafb6a7ac13c6b41789c92f9ee89462d539f5c..046e2815d4ea8d3735c1f5c94c712d21ea701454 100644
--- a/features/dashboard/new_project.feature
+++ b/features/dashboard/new_project.feature
@@ -9,7 +9,7 @@ Background:
   @javascript
   Scenario: I should see New Projects page
   Then I see "New Project" page
-  Then I see all possible import optios
+  Then I see all possible import options
 
   @javascript
   Scenario: I should see instructions on how to import from Git URL
diff --git a/features/explore/groups.feature b/features/explore/groups.feature
index 5fc9b1356010c2433d626b51e83b203656e367cb..9eacbe0b25e8bb09089f393d41166bf1d3e1cd90 100644
--- a/features/explore/groups.feature
+++ b/features/explore/groups.feature
@@ -24,14 +24,6 @@ Feature: Explore Groups
     Then I should see project "Internal" items
     And I should not see project "Enterprise" items
 
-  Scenario: I should see group's members as user
-    Given group "TestGroup" has internal project "Internal"
-    And "John Doe" is owner of group "TestGroup"
-    When I sign in as a user
-    And I visit group "TestGroup" members page
-    Then I should see group member "John Doe"
-    And I should not see member roles
-
   Scenario: I should see group with private, internal and public projects as visitor
     Given group "TestGroup" has internal project "Internal"
     Given group "TestGroup" has public project "Community"
@@ -56,14 +48,6 @@ Feature: Explore Groups
     And I should not see project "Internal" items
     And I should not see project "Enterprise" items
 
-  Scenario: I should see group's members as visitor
-    Given group "TestGroup" has internal project "Internal"
-    Given group "TestGroup" has public project "Community"
-    And "John Doe" is owner of group "TestGroup"
-    When I visit group "TestGroup" members page
-    Then I should see group member "John Doe"
-    And I should not see member roles
-
   Scenario: I should see group with private, internal and public projects as user
     Given group "TestGroup" has internal project "Internal"
     Given group "TestGroup" has public project "Community"
@@ -91,15 +75,6 @@ Feature: Explore Groups
     And I should see project "Internal" items
     And I should not see project "Enterprise" items
 
-  Scenario: I should see group's members as user
-    Given group "TestGroup" has internal project "Internal"
-    Given group "TestGroup" has public project "Community"
-    And "John Doe" is owner of group "TestGroup"
-    When I sign in as a user
-    And I visit group "TestGroup" members page
-    Then I should see group member "John Doe"
-    And I should not see member roles
-
   Scenario: I should see group with public project in public groups area
     Given group "TestGroup" has public project "Community"
     When I visit the public groups area
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/merge_requests.feature b/features/project/merge_requests.feature
index 21768c15c170d4e29fabcb958d83681b7f9b057f..967f2edb243f8f693381d496d4076e967d064085 100644
--- a/features/project/merge_requests.feature
+++ b/features/project/merge_requests.feature
@@ -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
@@ -236,6 +236,15 @@ Feature: Project Merge Requests
     And I unfold diff
     Then I should see additional file lines
 
+  @javascript
+  Scenario: I unfold diff in Side-by-Side view
+    Given project "Shop" have "Bug NS-05" open merge request with diffs inside
+    And I visit merge request page "Bug NS-05"
+    And I click on the Changes tab
+    And I click Side-by-side Diff tab
+    And I unfold diff
+    Then I should see additional file lines
+
   @javascript
   Scenario: I show comments on a merge request side-by-side diff with comments in multiple files
     Given project "Shop" have "Bug NS-05" open merge request with diffs inside
diff --git a/features/steps/dashboard/dashboard.rb b/features/steps/dashboard/dashboard.rb
index 80ed4c6d64cad9e0bc67de679d1ee006fac12559..a7d61bc28e0a639a6ed8412638ff90689abba5be 100644
--- a/features/steps/dashboard/dashboard.rb
+++ b/features/steps/dashboard/dashboard.rb
@@ -26,6 +26,7 @@ class Spinach::Features::Dashboard < Spinach::FeatureSteps
   end
 
   step 'I see prefilled new Merge Request page' do
+    expect(page).to have_selector('.merge-request-form')
     expect(current_path).to eq new_namespace_project_merge_request_path(@project.namespace, @project)
     expect(find("#merge_request_target_project_id").value).to eq @project.id.to_s
     expect(find("input#merge_request_source_branch").value).to eq "fix"
diff --git a/features/steps/dashboard/event_filters.rb b/features/steps/dashboard/event_filters.rb
index 726b37cfde53b89128da1aa3a63ea5249dff398f..ca3cd0ecc4e0d7b57db856e0b912ea79ae4506e6 100644
--- a/features/steps/dashboard/event_filters.rb
+++ b/features/steps/dashboard/event_filters.rb
@@ -1,4 +1,5 @@
 class Spinach::Features::EventFilters < Spinach::FeatureSteps
+  include WaitForAjax
   include SharedAuthentication
   include SharedPaths
   include SharedProject
@@ -72,14 +73,20 @@ class Spinach::Features::EventFilters < Spinach::FeatureSteps
   end
 
   When 'I click "push" event filter' do
-    click_link("push_event_filter")
+    wait_for_ajax
+    click_link("Push events")
+    wait_for_ajax
   end
 
   When 'I click "team" event filter' do
-    click_link("team_event_filter")
+    wait_for_ajax
+    click_link("Team")
+    wait_for_ajax
   end
 
   When 'I click "merge" event filter' do
-    click_link("merged_event_filter")
+    wait_for_ajax
+    click_link("Merge events")
+    wait_for_ajax
   end
 end
diff --git a/features/steps/dashboard/issues.rb b/features/steps/dashboard/issues.rb
index 8706f0e8e789fb73a0570e3c47db967e72064879..39c65bb6cde36b45fec07e892574580551aa824f 100644
--- a/features/steps/dashboard/issues.rb
+++ b/features/steps/dashboard/issues.rb
@@ -43,9 +43,14 @@ class Spinach::Features::DashboardIssues < Spinach::FeatureSteps
 
   step 'I click "All" link' do
     find(".js-author-search").click
+    expect(page).to have_selector(".dropdown-menu-author li a")
     find(".dropdown-menu-author li a", match: :first).click
+    expect(page).not_to have_selector(".dropdown-menu-author li a")
+
     find(".js-assignee-search").click
+    expect(page).to have_selector(".dropdown-menu-assignee li a")
     find(".dropdown-menu-assignee li a", match: :first).click
+    expect(page).not_to have_selector(".dropdown-menu-assignee li a")
   end
 
   def should_see(issue)
diff --git a/features/steps/dashboard/merge_requests.rb b/features/steps/dashboard/merge_requests.rb
index 06db36c701407687122d6ee9c1d7dcd9bf645772..6777101fb155dc05097da72a31bd42983c81662c 100644
--- a/features/steps/dashboard/merge_requests.rb
+++ b/features/steps/dashboard/merge_requests.rb
@@ -47,9 +47,14 @@ class Spinach::Features::DashboardMergeRequests < Spinach::FeatureSteps
 
   step 'I click "All" link' do
     find(".js-author-search").click
+    expect(page).to have_selector(".dropdown-menu-author li a")
     find(".dropdown-menu-author li a", match: :first).click
+    expect(page).not_to have_selector(".dropdown-menu-author li a")
+
     find(".js-assignee-search").click
+    expect(page).to have_selector(".dropdown-menu-assignee li a")
     find(".dropdown-menu-assignee li a", match: :first).click
+    expect(page).not_to have_selector(".dropdown-menu-assignee li a")
   end
 
   def should_see(merge_request)
diff --git a/features/steps/dashboard/new_project.rb b/features/steps/dashboard/new_project.rb
index 727a6a71373ac3b3f80c6289babfe30256b0927e..f0d8d498e4623b7e220621ca102f5adec1c0abf0 100644
--- a/features/steps/dashboard/new_project.rb
+++ b/features/steps/dashboard/new_project.rb
@@ -14,14 +14,13 @@ class Spinach::Features::NewProject < Spinach::FeatureSteps
     expect(page).to have_content('Project name')
   end
 
-  step 'I see all possible import optios' do
+  step 'I see all possible import options' do
     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
@@ -29,6 +28,7 @@ class Spinach::Features::NewProject < Spinach::FeatureSteps
   end
 
   step 'I am redirected to the GitHub import page' do
+    expect(page).to have_content('Import Projects from GitHub')
     expect(current_path).to eq new_import_github_path
   end
 
@@ -47,6 +47,7 @@ class Spinach::Features::NewProject < Spinach::FeatureSteps
   end
 
   step 'I redirected to Google Code import page' do
+    expect(page).to have_content('Import projects from Google Code')
     expect(current_path).to eq new_import_google_code_path
   end
 end
diff --git a/features/steps/explore/groups.rb b/features/steps/explore/groups.rb
index 87f32e70d59257c2b9365c692451d12ad55848d1..409bf0cb4167cbefc7af0c14d66bb318b4f2a5cf 100644
--- a/features/steps/explore/groups.rb
+++ b/features/steps/explore/groups.rb
@@ -62,10 +62,6 @@ class Spinach::Features::ExploreGroups < Spinach::FeatureSteps
     expect(page).to have_content "John Doe"
   end
 
-  step 'I should not see member roles' do
-    expect(body).not_to match(%r{owner|developer|reporter|guest}i)
-  end
-
   protected
 
   def group_has_project(groupname, projectname, visibility_level)
diff --git a/features/steps/group/members.rb b/features/steps/group/members.rb
index dfa2fa75def097b4a4fdf0c2b62827a33c8dc810..e9b45823c67a72e95476541fe638bb7962b8a532 100644
--- a/features/steps/group/members.rb
+++ b/features/steps/group/members.rb
@@ -116,8 +116,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_button 'Edit'
+      select 'Developer', from: "member_access_level_#{member.id}"
       click_on 'Save'
     end
   end
diff --git a/features/steps/project/badges/build.rb b/features/steps/project/badges/build.rb
index 66a48a176e58703e062660a7cd51e2f2f6957e47..96c59322f9b2f59d7a0231a836c2d5bb751f996b 100644
--- a/features/steps/project/badges/build.rb
+++ b/features/steps/project/badges/build.rb
@@ -26,7 +26,7 @@ class Spinach::Features::ProjectBadgesBuild < Spinach::FeatureSteps
 
   def expect_badge(status)
     svg = Nokogiri::XML.parse(page.body)
-    expect(page.response_headers).to include('Content-Type' => 'image/svg+xml')
+    expect(page.response_headers['Content-Type']).to include('image/svg+xml')
     expect(svg.at(%Q{text:contains("#{status}")})).to be_truthy
   end
 end
diff --git a/features/steps/project/builds/artifacts.rb b/features/steps/project/builds/artifacts.rb
index b4a32ed2e38782af3248bc9d5247e560ca31ecd9..055fca036d3014c49d657deee90dcd1a38923cba 100644
--- a/features/steps/project/builds/artifacts.rb
+++ b/features/steps/project/builds/artifacts.rb
@@ -10,6 +10,7 @@ class Spinach::Features::ProjectBuildsArtifacts < Spinach::FeatureSteps
 
   step 'I click artifacts browse button' do
     click_link 'Browse'
+    expect(page).not_to have_selector('.build-sidebar')
   end
 
   step 'I should see content of artifacts archive' do
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/forked_merge_requests.rb b/features/steps/project/forked_merge_requests.rb
index 6b56a77b832b99eebfdc3421344fa279a9aad8f7..dacab6c79775103fe004a2a5e0dc79b41f9e339e 100644
--- a/features/steps/project/forked_merge_requests.rb
+++ b/features/steps/project/forked_merge_requests.rb
@@ -34,6 +34,9 @@ class Spinach::Features::ProjectForkedMergeRequests < Spinach::FeatureSteps
   end
 
   step 'I fill out a "Merge Request On Forked Project" merge request' do
+    expect(page).to have_content('Source branch')
+    expect(page).to have_content('Target branch')
+
     first('.js-source-project').click
     first('.dropdown-source-project a', text: @forked_project.path_with_namespace)
 
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 35f166c7c08bc5196f10dd17c754d127b0f9b8f6..e21f76d00d9aae388e4c14cd6f62403bcc853a9e 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
@@ -297,7 +299,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
diff --git a/features/steps/project/merge_requests.rb b/features/steps/project/merge_requests.rb
index da848afd48ebfe9aa4e06785106a0bf9755944fe..9778ff4a6c7d89e5266a31807198df25519686ac 100644
--- a/features/steps/project/merge_requests.rb
+++ b/features/steps/project/merge_requests.rb
@@ -22,6 +22,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
@@ -477,6 +479,9 @@ class Spinach::Features::ProjectMergeRequests < Spinach::FeatureSteps
 
   step 'I click Side-by-side Diff tab' do
     find('a', text: 'Side-by-side').trigger('click')
+
+    # Waits for load
+    expect(page).to have_css('.parallel')
   end
 
   step 'I should see comments on the side-by-side diff page' do
@@ -490,6 +495,7 @@ class Spinach::Features::ProjectMergeRequests < Spinach::FeatureSteps
   end
 
   step 'I click the "Target branch" dropdown' do
+    expect(page).to have_content('Target branch')
     first('.target_branch').click
   end
 
diff --git a/features/steps/project/source/browse_files.rb b/features/steps/project/source/browse_files.rb
index 9a8896acb158a687bb8f6e33ff52e43a881c4258..bb79424ee08aa30e17afaeeeaaf077ad41b7fb38 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,15 +65,16 @@ 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
+    expect(page).to have_selector('.file-editor')
     set_new_content
   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
@@ -131,6 +132,7 @@ class Spinach::Features::ProjectSourceBrowseFiles < Spinach::FeatureSteps
   step 'I click on "New file" link in repo' do
     find('.add-to-tree').click
     click_link 'New file'
+    expect(page).to have_selector('.file-editor')
   end
 
   step 'I click on "Upload file" link in repo' do
@@ -376,7 +378,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/team_management.rb b/features/steps/project/team_management.rb
index f32576d2cb1b6dcbd33acb87adc56eebf0c5ae44..e920f5a706ba44d60cfb5e78bcbab4f82cda9878 100644
--- a/features/steps/project/team_management.rb
+++ b/features/steps/project/team_management.rb
@@ -65,8 +65,8 @@ 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 'Edit'
+      select "Reporter", from: "member_access_level_#{project_member.id}"
       click_button "Save"
     end
   end
diff --git a/features/steps/project/wiki.rb b/features/steps/project/wiki.rb
index 732dc5d0b9392901b28577b24a7bc2d93ae87115..07a955b1a14bb0d68867fc5c1c5710ad2ba441b1 100644
--- a/features/steps/project/wiki.rb
+++ b/features/steps/project/wiki.rb
@@ -142,7 +142,9 @@ class Spinach::Features::ProjectWiki < Spinach::FeatureSteps
   end
 
   step 'I edit the Wiki page with a path' do
+    expect(page).to have_content('three')
     click_on 'three'
+    expect(find('.nav-text')).to have_content('Three')
     click_on 'Edit'
   end
 
diff --git a/features/steps/shared/builds.rb b/features/steps/shared/builds.rb
index 4d6b258f5778d11876204e144ceb50656cc8d993..70e6d4836b2463703c66c28d8ca36aa8be455271 100644
--- a/features/steps/shared/builds.rb
+++ b/features/steps/shared/builds.rb
@@ -10,20 +10,20 @@ module SharedBuilds
   end
 
   step 'project has a recent build' do
-    @pipeline = create(:ci_pipeline, project: @project, sha: @project.commit.sha, ref: 'master')
+    @pipeline = create(:ci_empty_pipeline, project: @project, sha: @project.commit.sha, ref: 'master')
     @build = create(:ci_build_with_coverage, pipeline: @pipeline)
   end
 
   step 'recent build is successful' do
-    @build.update(status: 'success')
+    @build.success
   end
 
   step 'recent build failed' do
-    @build.update(status: 'failed')
+    @build.drop
   end
 
   step 'project has another build that is running' do
-    create(:ci_build, pipeline: @pipeline, name: 'second build', status: 'running')
+    create(:ci_build, pipeline: @pipeline, name: 'second build', status_event: 'run')
   end
 
   step 'I visit recent build details page' do
diff --git a/features/steps/shared/issuable.rb b/features/steps/shared/issuable.rb
index b5fd24d246f20d357aee49bc3956a7b48e981639..aa666a954bca40aed5e359a1b44c8b9c3bee039c 100644
--- a/features/steps/shared/issuable.rb
+++ b/features/steps/shared/issuable.rb
@@ -133,9 +133,7 @@ module SharedIssuable
   end
 
   step 'The list should be sorted by "Oldest updated"' do
-    page.within('.content div.dropdown.inline.prepend-left-10') do
-      expect(page.find('button.dropdown-toggle.btn')).to have_content('Oldest updated')
-    end
+    expect(find('.issues-filters')).to have_content('Oldest updated')
   end
 
   step 'I click link "Next" in the sidebar' do
diff --git a/features/support/wait_for_ajax.rb b/features/support/wait_for_ajax.rb
new file mode 100644
index 0000000000000000000000000000000000000000..b90fc1126716d1511e5af745ef3e28026ce3bef6
--- /dev/null
+++ b/features/support/wait_for_ajax.rb
@@ -0,0 +1,11 @@
+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/lib/api/access_requests.rb b/lib/api/access_requests.rb
new file mode 100644
index 0000000000000000000000000000000000000000..d02b469dac8bb25014fa3ea7c261e439e94553ac
--- /dev/null
+++ b/lib/api/access_requests.rb
@@ -0,0 +1,90 @@
+module API
+  class AccessRequests < Grape::API
+    before { authenticate! }
+
+    helpers ::API::Helpers::MembersHelpers
+
+    %w[group project].each do |source_type|
+      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
+        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))
+
+          present access_requesters.map(&:user), with: Entities::AccessRequester, access_requesters: access_requesters
+        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
+        post ":id/access_requests" do
+          source = find_source(source_type, params[:id])
+          access_requester = source.request_access(current_user)
+
+          if access_requester.persisted?
+            present access_requester.user, with: Entities::AccessRequester, access_requester: access_requester
+          else
+            render_validation_error!(access_requester)
+          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
+        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
+
+          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
+        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
+        end
+      end
+    end
+  end
+end
diff --git a/lib/api/api.rb b/lib/api/api.rb
index bd16806892b6acb50e5cf5aa922259a6b0c16f91..6b8bfbbdae6110e5fa5067653d10987d18b77d67 100644
--- a/lib/api/api.rb
+++ b/lib/api/api.rb
@@ -3,12 +3,18 @@ module API
     include APIGuard
     version 'v3', using: :path
 
+    rescue_from Gitlab::Access::AccessDeniedError do
+      rack_response({ 'message' => '403 Forbidden' }.to_json, 403)
+    end
+
     rescue_from ActiveRecord::RecordNotFound do
       rack_response({ 'message' => '404 Not found' }.to_json, 404)
     end
 
-    rescue_from Grape::Exceptions::ValidationErrors do |e|
-      error!({ messages: e.full_messages }, 400)
+    # Retain 405 error rather than a 500 error for Grape 0.15.0+.
+    # See: https://github.com/ruby-grape/grape/commit/252bfd27c320466ec3c0751812cf44245e97e5de
+    rescue_from Grape::Exceptions::Base do |e|
+      error! e.message, e.status, e.headers
     end
 
     rescue_from :all do |exception|
@@ -30,27 +36,29 @@ module API
     # Ensure the namespace is right, otherwise we might load Grape::API::Helpers
     helpers ::API::Helpers
 
+    mount ::API::AccessRequests
     mount ::API::AwardEmoji
     mount ::API::Branches
     mount ::API::Builds
     mount ::API::CommitStatuses
     mount ::API::Commits
     mount ::API::DeployKeys
+    mount ::API::Deployments
     mount ::API::Environments
     mount ::API::Files
-    mount ::API::GroupMembers
     mount ::API::Groups
     mount ::API::Internal
     mount ::API::Issues
     mount ::API::Keys
     mount ::API::Labels
     mount ::API::LicenseTemplates
+    mount ::API::Members
     mount ::API::MergeRequests
     mount ::API::Milestones
     mount ::API::Namespaces
     mount ::API::Notes
+    mount ::API::Pipelines
     mount ::API::ProjectHooks
-    mount ::API::ProjectMembers
     mount ::API::ProjectSnippets
     mount ::API::Projects
     mount ::API::Repositories
diff --git a/lib/api/branches.rb b/lib/api/branches.rb
index a77afe634f626d96737d9fbba5be9ede41400fbe..b615703df936c11a2f757dfc2ebbd5253284c7e4 100644
--- a/lib/api/branches.rb
+++ b/lib/api/branches.rb
@@ -61,22 +61,27 @@ module API
           name: @branch.name
         }
 
-        unless developers_can_merge.nil?
-          protected_branch_params.merge!({
-            merge_access_level_attributes: {
-              access_level: developers_can_merge ? Gitlab::Access::DEVELOPER : Gitlab::Access::MASTER
-            }
-          })
+        # 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
 
-        unless developers_can_push.nil?
-          protected_branch_params.merge!({
-            push_access_level_attributes: {
-              access_level: developers_can_push ? Gitlab::Access::DEVELOPER : Gitlab::Access::MASTER
-            }
-          })
+        # 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_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)
diff --git a/lib/api/builds.rb b/lib/api/builds.rb
index be5a3484ec8a8143f8fef3a64fb90a312567863c..52bdbcae5a8e68c9e182de535e58ca249cdb2af9 100644
--- a/lib/api/builds.rb
+++ b/lib/api/builds.rb
@@ -189,6 +189,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
diff --git a/lib/api/commits.rb b/lib/api/commits.rb
index 4a11c8e3620cc261a8b0c4dd12b1381a35c93abb..b4eaf1813d4958c4fafb7ddb2fe7a390d3f481f7 100644
--- a/lib/api/commits.rb
+++ b/lib/api/commits.rb
@@ -54,7 +54,7 @@ module API
         sha = params[:sha]
         commit = user_project.commit(sha)
         not_found! "Commit" unless commit
-        commit.diffs.to_a
+        commit.raw_diffs.to_a
       end
 
       # Get a commit's comments
@@ -96,7 +96,7 @@ module API
         }
 
         if params[:path] && params[:line] && params[:line_type]
-          commit.diffs(all_diffs: true).each do |diff|
+          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)
 
diff --git a/lib/api/deploy_keys.rb b/lib/api/deploy_keys.rb
index 5c570b5e5ca375e1718a721576444116d44fd017..825e05fbae3d72c930b3bff691d2339b796c4c4f 100644
--- a/lib/api/deploy_keys.rb
+++ b/lib/api/deploy_keys.rb
@@ -10,6 +10,9 @@ module API
       present keys, with: Entities::SSHKey
     end
 
+    params do
+      requires :id, type: String, desc: 'The ID of the project'
+    end
     resource :projects do
       before { authorize_admin_project }
 
@@ -17,52 +20,43 @@ module API
       # Use "projects/:id/deploy_keys/..." instead.
       #
       %w(keys deploy_keys).each do |path|
-        # Get a specific project's deploy keys
-        #
-        # Example Request:
-        #   GET /projects/:id/deploy_keys
+        desc "Get a specific project's deploy keys" do
+          success Entities::SSHKey
+        end
         get ":id/#{path}" do
           present user_project.deploy_keys, with: Entities::SSHKey
         end
 
-        # Get single deploy key owned by currently authenticated user
-        #
-        # Example Request:
-        #   GET /projects/:id/deploy_keys/:key_id
+        desc 'Get single deploy key' do
+          success Entities::SSHKey
+        end
+        params do
+          requires :key_id, type: Integer, desc: 'The ID of the deploy key'
+        end
         get ":id/#{path}/:key_id" do
           key = user_project.deploy_keys.find params[:key_id]
           present key, with: Entities::SSHKey
         end
 
-        # Add new deploy key to currently authenticated user
-        # If deploy key already exists - it will be joined to project
-        # but only if original one was accessible by same user
-        #
-        # Parameters:
-        #   key (required) - New deploy Key
-        #   title (required) - New deploy Key's title
-        # Example Request:
-        #   POST /projects/:id/deploy_keys
+        # TODO: for 9.0 we should check if params are there with the params block
+        # grape provides, at this point we'd change behaviour so we can't
+        # Behaviour now if you don't provide all required params: it renders a
+        # validation error or two.
+        desc 'Add new deploy key to currently authenticated user' do
+          success Entities::SSHKey
+        end
         post ":id/#{path}" do
           attrs = attributes_for_keys [:title, :key]
+          attrs[:key].strip! if attrs[:key]
 
-          if attrs[:key].present?
-            attrs[:key].strip!
-
-            # check if key already exist in project
-            key = user_project.deploy_keys.find_by(key: attrs[:key])
-            if key
-              present key, with: Entities::SSHKey
-              next
-            end
+          key = user_project.deploy_keys.find_by(key: attrs[:key])
+          present key, with: Entities::SSHKey if key
 
-            # 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
-              next
-            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
           end
 
           key = DeployKey.new attrs
@@ -74,12 +68,46 @@ module API
           end
         end
 
-        # Delete existing deploy key of currently authenticated user
-        #
-        # Example Request:
-        #   DELETE /projects/:id/deploy_keys/:key_id
+        desc 'Enable a deploy key for a project' do
+          detail 'This feature was added in GitLab 8.11'
+          success Entities::SSHKey
+        end
+        params do
+          requires :key_id, type: Integer, desc: 'The ID of the deploy key'
+        end
+        post ":id/#{path}/:key_id/enable" do
+          key = ::Projects::EnableDeployKeyService.new(user_project,
+                                                        current_user, declared(params)).execute
+
+          if key
+            present key, with: Entities::SSHKey
+          else
+            not_found!('Deploy Key')
+          end
+        end
+
+        desc 'Disable a deploy key for a project' do
+          detail 'This feature was added in GitLab 8.11'
+          success Entities::SSHKey
+        end
+        params do
+          requires :key_id, type: Integer, desc: 'The ID of the deploy key'
+        end
+        delete ":id/#{path}/:key_id/disable" do
+          key = user_project.deploy_keys_projects.find_by(deploy_key_id: params[:key_id])
+          key.destroy
+
+          present key.deploy_key, with: Entities::SSHKey
+        end
+
+        desc 'Delete existing deploy key of currently authenticated user' do
+          success Key
+        end
+        params do
+          requires :key_id, type: Integer, desc: 'The ID of the deploy key'
+        end
         delete ":id/#{path}/:key_id" do
-          key = user_project.deploy_keys.find params[:key_id]
+          key = user_project.deploy_keys.find(params[:key_id])
           key.destroy
         end
       end
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 b6f6b11d97bc29b9d60d393520855321b0ce9a61..66b85ab17935e1fc25434d469dc293afffec1853 100644
--- a/lib/api/entities.rb
+++ b/lib/api/entities.rb
@@ -92,9 +92,21 @@ module API
       end
     end
 
-    class ProjectMember < UserBasic
+    class Member < UserBasic
       expose :access_level do |user, options|
-        options[:project].project_members.find_by(user_id: user.id).access_level
+        member = options[:member] || options[:members].find { |m| m.user_id == user.id }
+        member.access_level
+      end
+      expose :expires_at do |user, options|
+        member = options[:member] || options[:members].find { |m| m.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.requested_at
       end
     end
 
@@ -109,12 +121,6 @@ module API
       expose :shared_projects, using: Entities::Project
     end
 
-    class GroupMember < UserBasic
-      expose :access_level do |user, options|
-        options[:group].group_members.find_by(user_id: user.id).access_level
-      end
-    end
-
     class RepoBranch < Grape::Entity
       expose :name
 
@@ -128,12 +134,14 @@ module API
 
       expose :developers_can_push do |repo_branch, options|
         project = options[:project]
-        project.protected_branches.matching(repo_branch.name).any? { |protected_branch| protected_branch.push_access_level.access_level == Gitlab::Access::DEVELOPER }
+        access_levels = project.protected_branches.matching(repo_branch.name).map(&:push_access_levels).flatten
+        access_levels.any? { |access_level| access_level.access_level == Gitlab::Access::DEVELOPER }
       end
 
       expose :developers_can_merge do |repo_branch, options|
         project = options[:project]
-        project.protected_branches.matching(repo_branch.name).any? { |protected_branch| protected_branch.merge_access_level.access_level == Gitlab::Access::DEVELOPER }
+        access_levels = project.protected_branches.matching(repo_branch.name).map(&:merge_access_levels).flatten
+        access_levels.any? { |access_level| access_level.access_level == Gitlab::Access::DEVELOPER }
       end
     end
 
@@ -225,7 +233,7 @@ module API
 
     class MergeRequestChanges < MergeRequest
       expose :diffs, as: :changes, using: Entities::RepoDiff do |compare, _|
-        compare.diffs(all_diffs: true).to_a
+        compare.raw_diffs(all_diffs: true).to_a
       end
     end
 
@@ -326,7 +334,7 @@ module API
       expose :id, :path, :kind
     end
 
-    class Member < Grape::Entity
+    class MemberAccess < Grape::Entity
       expose :access_level
       expose :notification_level do |member, options|
         if member.notification_setting
@@ -335,10 +343,10 @@ module API
       end
     end
 
-    class ProjectAccess < Member
+    class ProjectAccess < MemberAccess
     end
 
-    class GroupAccess < Member
+    class GroupAccess < MemberAccess
     end
 
     class ProjectService < Grape::Entity
@@ -498,10 +506,29 @@ module API
       expose :key, :value
     end
 
-    class Environment < Grape::Entity
+    class Pipeline < Grape::Entity
+      expose :id, :status, :ref, :sha, :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
diff --git a/lib/api/group_members.rb b/lib/api/group_members.rb
deleted file mode 100644
index dbe5bb08d3ff05bf22e8de31acbd80ee8967c175..0000000000000000000000000000000000000000
--- a/lib/api/group_members.rb
+++ /dev/null
@@ -1,87 +0,0 @@
-module API
-  class GroupMembers < Grape::API
-    before { authenticate! }
-
-    resource :groups do
-      # Get a list of group members viewable by the authenticated user.
-      #
-      # Example Request:
-      #  GET /groups/:id/members
-      get ":id/members" do
-        group = find_group(params[:id])
-        users = group.users
-        present users, with: Entities::GroupMember, group: group
-      end
-
-      # Add a user to the list of group members
-      #
-      # Parameters:
-      #   id (required) - group id
-      #   user_id (required) - the users id
-      #   access_level (required) - Project access level
-      # Example Request:
-      #  POST /groups/:id/members
-      post ":id/members" do
-        group = find_group(params[:id])
-        authorize! :admin_group, group
-        required_attributes! [:user_id, :access_level]
-
-        unless validate_access_level?(params[:access_level])
-          render_api_error!("Wrong access level", 422)
-        end
-
-        if group.group_members.find_by(user_id: params[:user_id])
-          render_api_error!("Already exists", 409)
-        end
-
-        group.add_users([params[:user_id]], params[:access_level], current_user)
-        member = group.group_members.find_by(user_id: params[:user_id])
-        present member.user, with: Entities::GroupMember, group: group
-      end
-
-      # Update group member
-      #
-      # Parameters:
-      #   id (required) - The ID of a group
-      #   user_id (required) - The ID of a group member
-      #   access_level (required) - Project access level
-      # Example Request:
-      #   PUT /groups/:id/members/:user_id
-      put ':id/members/:user_id' do
-        group = find_group(params[:id])
-        authorize! :admin_group, group
-        required_attributes! [:access_level]
-
-        group_member = group.group_members.find_by(user_id: params[:user_id])
-        not_found!('User can not be found') if group_member.nil?
-
-        if group_member.update_attributes(access_level: params[:access_level])
-          @member = group_member.user
-          present @member, with: Entities::GroupMember, group: group
-        else
-          handle_member_errors group_member.errors
-        end
-      end
-
-      # Remove member.
-      #
-      # Parameters:
-      #   id (required) - group id
-      #   user_id (required) - the users id
-      #
-      # Example Request:
-      #   DELETE /groups/:id/members/:user_id
-      delete ":id/members/:user_id" do
-        group = find_group(params[:id])
-        authorize! :admin_group, group
-        member = group.group_members.find_by(user_id: params[:user_id])
-
-        if member.nil?
-          render_api_error!("404 Not Found - user_id:#{params[:user_id]} not a member of group #{group.name}", 404)
-        else
-          member.destroy
-        end
-      end
-    end
-  end
-end
diff --git a/lib/api/helpers.rb b/lib/api/helpers.rb
index 130509cdad6ca3123d0d5d6c0c093e8b24de18e3..d0469d6602d0df0feb595a1285a0c8420703c02b 100644
--- a/lib/api/helpers.rb
+++ b/lib/api/helpers.rb
@@ -28,7 +28,7 @@ module API
 
       # If the sudo is the current user do nothing
       if identifier && !(@current_user.id == identifier || @current_user.username == identifier)
-        render_api_error!('403 Forbidden: Must be admin to use sudo', 403) unless @current_user.is_admin?
+        forbidden!('Must be admin to use sudo') unless @current_user.is_admin?
         @current_user = User.by_username_or_id(identifier)
         not_found!("No user id or username for: #{identifier}") if @current_user.nil?
       end
@@ -49,16 +49,15 @@ module API
 
     def user_project
       @project ||= find_project(params[:id])
-      @project || not_found!("Project")
     end
 
     def find_project(id)
       project = Project.find_with_namespace(id) || Project.find_by(id: id)
 
-      if project && can?(current_user, :read_project, project)
+      if can?(current_user, :read_project, project)
         project
       else
-        nil
+        not_found!('Project')
       end
     end
 
@@ -89,11 +88,7 @@ module API
     end
 
     def find_group(id)
-      begin
-        group = Group.find(id)
-      rescue ActiveRecord::RecordNotFound
-        group = Group.find_by!(path: id)
-      end
+      group = Group.find_by(path: id) || Group.find_by(id: id)
 
       if can?(current_user, :read_group, group)
         group
@@ -135,7 +130,7 @@ module API
     end
 
     def authorize!(action, subject)
-      forbidden! unless abilities.allowed?(current_user, action, subject)
+      forbidden! unless can?(current_user, action, subject)
     end
 
     def authorize_push_project
@@ -197,10 +192,6 @@ module API
       errors
     end
 
-    def validate_access_level?(level)
-      Gitlab::Access.options_with_owner.values.include? level.to_i
-    end
-
     # Checks the occurrences of datetime attributes, each attribute if present in the params hash must be in ISO 8601
     # format (YYYY-MM-DDTHH:MM:SSZ) or a Bad Request error is invoked.
     #
@@ -411,11 +402,6 @@ module API
       File.read(Gitlab.config.gitlab_shell.secret_file).chomp
     end
 
-    def handle_member_errors(errors)
-      error!(errors[:access_level], 422) if errors[:access_level].any?
-      not_found!(errors)
-    end
-
     def send_git_blob(repository, blob)
       env['api.format'] = :txt
       content_type 'text/plain'
diff --git a/lib/api/helpers/members_helpers.rb b/lib/api/helpers/members_helpers.rb
new file mode 100644
index 0000000000000000000000000000000000000000..90114f6f6677de987ba665d356ed318318508199
--- /dev/null
+++ b/lib/api/helpers/members_helpers.rb
@@ -0,0 +1,13 @@
+module API
+  module Helpers
+    module MembersHelpers
+      def find_source(source_type, id)
+        public_send("find_#{source_type}", id)
+      end
+
+      def authorize_admin_source!(source_type, source)
+        authorize! :"admin_#{source_type}", source
+      end
+    end
+  end
+end
diff --git a/lib/api/internal.rb b/lib/api/internal.rb
index 959b700de782aee89412bb3d53119786d3127cab..d8e9ac406c4c4067df971508e6c52af123a35224 100644
--- a/lib/api/internal.rb
+++ b/lib/api/internal.rb
@@ -74,6 +74,10 @@ module API
         response
       end
 
+      get "/merge_request_urls" do
+        ::MergeRequests::GetUrlsService.new(project).execute(params[:changes])
+      end
+
       #
       # Discover user by ssh key
       #
diff --git a/lib/api/issues.rb b/lib/api/issues.rb
index c4d3134da6c3e6c1031809ee3a1feb1689e86f36..077258faee19235a01025d992e04fe9e528b6cd5 100644
--- a/lib/api/issues.rb
+++ b/lib/api/issues.rb
@@ -3,8 +3,6 @@ module API
   class Issues < Grape::API
     before { authenticate! }
 
-    helpers ::Gitlab::AkismetHelper
-
     helpers do
       def filter_issues_state(issues, state)
         case state
diff --git a/lib/api/members.rb b/lib/api/members.rb
new file mode 100644
index 0000000000000000000000000000000000000000..94c16710d9a5b5cf0fa1ef89c90836ba1a11ebc9
--- /dev/null
+++ b/lib/api/members.rb
@@ -0,0 +1,158 @@
+module API
+  class Members < Grape::API
+    before { authenticate! }
+
+    helpers ::API::Helpers::MembersHelpers
+
+    %w[group project].each do |source_type|
+      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
+        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)
+
+          present members.map(&:user), with: Entities::Member, members: members
+        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
+        get ":id/members/:user_id" do
+          source = find_source(source_type, params[:id])
+
+          members = source.members
+          member = members.find_by!(user_id: params[:user_id])
+
+          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
+        #   expires_at (optional) - Date string in the format YEAR-MONTH-DAY
+        #
+        # Example Request:
+        #   POST /groups/:id/members
+        #   POST /projects/:id/members
+        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!
+          conflict!('Member already exists') if source_type == 'group' && member
+
+          unless member
+            source.add_user(params[:user_id], params[:access_level], current_user: current_user, expires_at: params[:expires_at])
+            member = source.members.find_by(user_id: params[:user_id])
+          end
+
+          if member
+            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, :expires_at]))
+            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)
+            render_validation_error!(member)
+          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
+        #   expires_at (optional) - Date string in the format YEAR-MONTH-DAY
+        #
+        # Example Request:
+        #   PUT /groups/:id/members/:user_id
+        #   PUT /projects/:id/members/:user_id
+        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(attrs)
+            present member.user, with: Entities::Member, member: member
+          else
+            # This is to ensure back-compatibility but 400 behavior should be used
+            # for all validation errors in 9.0!
+            render_api_error!('Access level is not known', 422) if member.errors.key?(:access_level)
+            render_validation_error!(member)
+          end
+        end
+
+        # 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
+        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!
+          member = source.members.find_by(user_id: params[:user_id])
+
+          # This is to ensure back-compatibility but this should be removed in
+          # favor of find_by! in 9.0!
+          not_found!("Member: user_id:#{params[:user_id]}") if source_type == 'group' && member.nil?
+
+          # This is to ensure back-compatibility but 204 behavior should be used
+          # for all DELETE endpoints in 9.0!
+          if member.nil?
+            { message: "Access revoked", id: params[:user_id].to_i }
+          else
+            ::Members::DestroyService.new(member, current_user).execute
+
+            present member.user, with: Entities::Member, member: member
+          end
+        end
+      end
+    end
+  end
+end
diff --git a/lib/api/pipelines.rb b/lib/api/pipelines.rb
new file mode 100644
index 0000000000000000000000000000000000000000..2aae75c471d337c6919e4506f5511800c1e8c5be
--- /dev/null
+++ b/lib/api/pipelines.rb
@@ -0,0 +1,74 @@
+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'
+      end
+      get ':id/pipelines' do
+        authorize! :read_pipeline, user_project
+
+        present paginate(user_project.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_members.rb b/lib/api/project_members.rb
deleted file mode 100644
index 6a0b3e7d134321ff5c8c15d0e32aa5d847a1af45..0000000000000000000000000000000000000000
--- a/lib/api/project_members.rb
+++ /dev/null
@@ -1,110 +0,0 @@
-module API
-  # Projects members API
-  class ProjectMembers < Grape::API
-    before { authenticate! }
-
-    resource :projects do
-      # Get a project team members
-      #
-      # Parameters:
-      #   id (required) - The ID of a project
-      #   query         - Query string
-      # Example Request:
-      #   GET /projects/:id/members
-      get ":id/members" do
-        if params[:query].present?
-          @members = paginate user_project.users.where("username LIKE ?", "%#{params[:query]}%")
-        else
-          @members = paginate user_project.users
-        end
-        present @members, with: Entities::ProjectMember, project: user_project
-      end
-
-      # Get a project team members
-      #
-      # Parameters:
-      #   id (required) - The ID of a project
-      #   user_id (required) - The ID of a user
-      # Example Request:
-      #   GET /projects/:id/members/:user_id
-      get ":id/members/:user_id" do
-        @member = user_project.users.find params[:user_id]
-        present @member, with: Entities::ProjectMember, project: user_project
-      end
-
-      # Add a new project team member
-      #
-      # Parameters:
-      #   id (required) - The ID of a project
-      #   user_id (required) - The ID of a user
-      #   access_level (required) - Project access level
-      # Example Request:
-      #   POST /projects/:id/members
-      post ":id/members" do
-        authorize! :admin_project, user_project
-        required_attributes! [:user_id, :access_level]
-
-        # either the user is already a team member or a new one
-        project_member = user_project.project_member(params[:user_id])
-        if project_member.nil?
-          project_member = user_project.project_members.new(
-            user_id: params[:user_id],
-            access_level: params[:access_level]
-          )
-        end
-
-        if project_member.save
-          @member = project_member.user
-          present @member, with: Entities::ProjectMember, project: user_project
-        else
-          handle_member_errors project_member.errors
-        end
-      end
-
-      # Update project team member
-      #
-      # Parameters:
-      #   id (required) - The ID of a project
-      #   user_id (required) - The ID of a team member
-      #   access_level (required) - Project access level
-      # Example Request:
-      #   PUT /projects/:id/members/:user_id
-      put ":id/members/:user_id" do
-        authorize! :admin_project, user_project
-        required_attributes! [:access_level]
-
-        project_member = user_project.project_members.find_by(user_id: params[:user_id])
-        not_found!("User can not be found") if project_member.nil?
-
-        if project_member.update_attributes(access_level: params[:access_level])
-          @member = project_member.user
-          present @member, with: Entities::ProjectMember, project: user_project
-        else
-          handle_member_errors project_member.errors
-        end
-      end
-
-      # Remove a team member from project
-      #
-      # Parameters:
-      #   id (required) - The ID of a project
-      #   user_id (required) - The ID of a team member
-      # Example Request:
-      #   DELETE /projects/:id/members/:user_id
-      delete ":id/members/:user_id" do
-        project_member = user_project.project_members.find_by(user_id: params[:user_id])
-
-        unless current_user.can?(:admin_project, user_project) ||
-                current_user.can?(:destroy_project_member, project_member)
-          forbidden!
-        end
-
-        if project_member.nil?
-          { message: "Access revoked", id: params[:user_id].to_i }
-        else
-          project_member.destroy
-        end
-      end
-    end
-  end
-end
diff --git a/lib/api/projects.rb b/lib/api/projects.rb
index 8fed7db8803992a2c5169cd24592d0113e87d5ea..60cfc103afd27f94a8b5afc360f970a41c7fca4a 100644
--- a/lib/api/projects.rb
+++ b/lib/api/projects.rb
@@ -323,7 +323,7 @@ module API
       #   DELETE /projects/:id
       delete ":id" do
         authorize! :remove_project, user_project
-        ::Projects::DestroyService.new(user_project, current_user, {}).pending_delete!
+        ::Projects::DestroyService.new(user_project, current_user, {}).async_execute
       end
 
       # Mark this project as forked from another
diff --git a/lib/api/session.rb b/lib/api/session.rb
index 56c202f129435eedad105f015b8a845afb4f5994..55ec66a6d674a3a7faeed2df452f7459bf0b30e7 100644
--- a/lib/api/session.rb
+++ b/lib/api/session.rb
@@ -14,6 +14,7 @@ module API
       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/templates.rb b/lib/api/templates.rb
index 1840879775666751c702f2bb3b2e0a076ed70e86..b9e718147e10d1e9285c96e0ed582b0f1a023357 100644
--- a/lib/api/templates.rb
+++ b/lib/api/templates.rb
@@ -1,21 +1,28 @@
 module API
   class Templates < Grape::API
-    TEMPLATE_TYPES = {
-      gitignores:     Gitlab::Template::Gitignore,
-      gitlab_ci_ymls: Gitlab::Template::GitlabCiYml
+    GLOBAL_TEMPLATE_TYPES = {
+      gitignores:     Gitlab::Template::GitignoreTemplate,
+      gitlab_ci_ymls: Gitlab::Template::GitlabCiYmlTemplate
     }.freeze
 
-    TEMPLATE_TYPES.each do |template, klass|
+    helpers do
+      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.to_s do
+      get template_type.to_s do
         present klass.all, with: Entities::TemplatesList
       end
 
-      # Get the text for a specific template
+      # Get the text for a specific template present in local filesystem
       #
       # Parameters:
       #   name (required) - The name of a template
@@ -23,13 +30,10 @@ module API
       # Example Request:
       #   GET /gitignores/Elixir
       #   GET /gitlab_ci_ymls/Ruby
-      get "#{template}/:name" do
+      get "#{template_type}/:name" do
         required_attributes! [:name]
-
         new_template = klass.find(params[:name])
-        not_found!(template.to_s.singularize) unless new_template
-
-        present new_template, with: Entities::Template
+        render_response(template_type, new_template)
       end
     end
   end
diff --git a/lib/api/todos.rb b/lib/api/todos.rb
index 26c24c3baffcf67b71630651c5f80cfefd9bfe2d..19df13d8aacad3eca25872bda4e7ee8877d3de2e 100644
--- a/lib/api/todos.rb
+++ b/lib/api/todos.rb
@@ -61,9 +61,9 @@ module API
       #
       delete ':id' do
         todo = current_user.todos.find(params[:id])
-        todo.done
+        TodoService.new.mark_todos_as_done([todo], current_user)
 
-        present todo, with: Entities::Todo, current_user: current_user
+        present todo.reload, with: Entities::Todo, current_user: current_user
       end
 
       # Mark all todos as done
@@ -73,9 +73,7 @@ module API
       #
       delete do
         todos = find_todos
-        todos.each(&:done)
-
-        todos.length
+        TodoService.new.mark_todos_as_done(todos, current_user)
       end
     end
   end
diff --git a/lib/backup/files.rb b/lib/backup/files.rb
index 654b4d1c8962dc35300e7590c90e3cdf88dc9baa..cedbb289f6a80be0ac52a9923b673fd2f8ab4b1a 100644
--- a/lib/backup/files.rb
+++ b/lib/backup/files.rb
@@ -27,7 +27,7 @@ module Backup
 
     def backup_existing_files_dir
       timestamped_files_path = File.join(files_parent_dir, "#{name}.#{Time.now.to_i}")
-      if File.exists?(app_files_dir)
+      if File.exist?(app_files_dir)
         FileUtils.mv(app_files_dir, File.expand_path(timestamped_files_path))
       end
     end
diff --git a/lib/backup/manager.rb b/lib/backup/manager.rb
index 2ff3e3bdfb017bc288972d99ed85b10eda5e78ca..0dfffaf0bc6d6d6ba5cc95995a027c72d8c4e67a 100644
--- a/lib/backup/manager.rb
+++ b/lib/backup/manager.rb
@@ -114,7 +114,7 @@ module Backup
 
       tar_file = ENV["BACKUP"].nil? ? File.join("#{file_list.first}_gitlab_backup.tar") : File.join(ENV["BACKUP"] + "_gitlab_backup.tar")
 
-      unless File.exists?(tar_file)
+      unless File.exist?(tar_file)
         puts "The specified backup doesn't exist!"
         exit 1
       end
diff --git a/lib/backup/repository.rb b/lib/backup/repository.rb
index 1f5917b81275367420a109aa7cd29fad214d9a67..f117fc3d37def0815360c09e4221dbadea2a9f11 100644
--- a/lib/backup/repository.rb
+++ b/lib/backup/repository.rb
@@ -28,7 +28,7 @@ module Backup
 
         wiki = ProjectWiki.new(project)
 
-        if File.exists?(path_to_repo(wiki))
+        if File.exist?(path_to_repo(wiki))
           $progress.print " * #{wiki.path_with_namespace} ... "
           if wiki.repository.empty?
             $progress.puts " [SKIPPED]".color(:cyan)
@@ -49,7 +49,7 @@ module Backup
 
     def restore
       Gitlab.config.repositories.storages.each do |name, path|
-        next unless File.exists?(path)
+        next unless File.exist?(path)
 
         # Move repos dir to 'repositories.old' dir
         bk_repos_path = File.join(path, '..', 'repositories.old.' + Time.now.to_i.to_s)
@@ -63,7 +63,7 @@ module Backup
 
         project.ensure_dir_exist
 
-        if File.exists?(path_to_bundle(project))
+        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)})
         else
@@ -80,7 +80,7 @@ module Backup
 
         wiki = ProjectWiki.new(project)
 
-        if File.exists?(path_to_bundle(wiki))
+        if File.exist?(path_to_bundle(wiki))
           $progress.print " * #{wiki.path_with_namespace} ... "
 
           # If a wiki bundle exists, first remove the empty repo
diff --git a/lib/banzai/filter/autolink_filter.rb b/lib/banzai/filter/autolink_filter.rb
index 9ed45707515c16a2d0baa4f09777f3876c7832e6..799b83b1069362721c040838f939a58ef94e0cc9 100644
--- a/lib/banzai/filter/autolink_filter.rb
+++ b/lib/banzai/filter/autolink_filter.rb
@@ -31,6 +31,14 @@ module Banzai
       # Text matching LINK_PATTERN inside these elements will not be linked
       IGNORE_PARENTS = %w(a code kbd pre script style).to_set
 
+      # The XPath query to use for finding text nodes to parse.
+      TEXT_QUERY = %Q(descendant-or-self::text()[
+        not(#{IGNORE_PARENTS.map { |p| "ancestor::#{p}" }.join(' or ')})
+        and contains(., '://')
+        and not(starts-with(., 'http'))
+        and not(starts-with(., 'ftp'))
+      ])
+
       def call
         return doc if context[:autolink] == false
 
@@ -66,16 +74,11 @@ module Banzai
       # Autolinks any text matching LINK_PATTERN that Rinku didn't already
       # replace
       def text_parse
-        search_text_nodes(doc).each do |node|
+        doc.xpath(TEXT_QUERY).each do |node|
           content = node.to_html
 
-          next if has_ancestor?(node, IGNORE_PARENTS)
           next unless content.match(LINK_PATTERN)
 
-          # If Rinku didn't link this, there's probably a good reason, so we'll
-          # skip it too
-          next if content.start_with?(*%w(http https ftp))
-
           html = autolink_filter(content)
 
           next if html == content
diff --git a/lib/banzai/filter/relative_link_filter.rb b/lib/banzai/filter/relative_link_filter.rb
index 5b73fc8fceec1ba2835e5ad3afb27e354ce9dc8a..4fa8d05481f991cb7ba62c58f24d05abf2be635f 100644
--- a/lib/banzai/filter/relative_link_filter.rb
+++ b/lib/banzai/filter/relative_link_filter.rb
@@ -35,6 +35,7 @@ module Banzai
 
       def process_link_attr(html_attr)
         return if html_attr.blank?
+        return if html_attr.value.start_with?('//')
 
         uri = URI(html_attr.value)
         if uri.relative? && uri.path.present?
@@ -51,7 +52,7 @@ module Banzai
           relative_url_root,
           context[:project].path_with_namespace,
           uri_type(file_path),
-          ref || context[:project].default_branch,  # if no ref exists, point to the default branch
+          ref,
           file_path
         ].compact.join('/').squeeze('/').chomp('/')
 
@@ -92,7 +93,7 @@ module Banzai
         parts = request_path.split('/')
         parts.pop if uri_type(request_path) != :tree
 
-        path.sub!(%r{^\./}, '')
+        path.sub!(%r{\A\./}, '')
 
         while path.start_with?('../')
           parts.pop
@@ -115,7 +116,7 @@ module Banzai
       end
 
       def current_commit
-        @current_commit ||= context[:commit] || ref ? repository.commit(ref) : repository.head_commit
+        @current_commit ||= context[:commit] || repository.commit(ref)
       end
 
       def relative_url_root
@@ -123,7 +124,7 @@ module Banzai
       end
 
       def ref
-        context[:ref]
+        context[:ref] || context[:project].default_branch
       end
 
       def repository
diff --git a/lib/banzai/filter/sanitization_filter.rb b/lib/banzai/filter/sanitization_filter.rb
index ca80aac5a0894c810f5a0852cc43d05a125d9b65..6e13282d5f4ebb18376f9aec2be117544948f2a0 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
+        whitelist = super.dup
 
         customize_whitelist(whitelist)
 
@@ -42,6 +42,8 @@ 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)
 
diff --git a/lib/banzai/filter/syntax_highlight_filter.rb b/lib/banzai/filter/syntax_highlight_filter.rb
index 91f0159f9a1180e13824da5c0668202a509d115b..fcdb496aed245c4321c4ffded8dedfdf257a3129 100644
--- a/lib/banzai/filter/syntax_highlight_filter.rb
+++ b/lib/banzai/filter/syntax_highlight_filter.rb
@@ -17,15 +17,12 @@ module Banzai
 
       def highlight_node(node)
         language = node.attr('class')
-        code     = node.text
-
+        code = node.text
         css_classes = "code highlight"
-
-        lexer = Rouge::Lexer.find_fancy(language) || Rouge::Lexers::PlainText
-        formatter = Rouge::Formatters::HTML.new
+        lexer = lexer_for(language)
 
         begin
-          code = formatter.format(lexer.lex(code))
+          code = format(lex(lexer, code))
 
           css_classes << " js-syntax-highlight #{lexer.tag}"
         rescue
@@ -41,14 +38,27 @@ module Banzai
 
       private
 
+      # Separate method so it can be instrumented.
+      def lex(lexer, code)
+        lexer.lex(code)
+      end
+
+      def format(tokens)
+        rouge_formatter.format(tokens)
+      end
+
+      def lexer_for(language)
+        (Rouge::Lexer.find(language) || Rouge::Lexers::PlainText).new
+      end
+
       def replace_parent_pre_element(node, highlighted)
         # Replace the parent `pre` element with the entire highlighted block
         node.parent.replace(highlighted)
       end
 
       # Override Rouge::Plugins::Redcarpet#rouge_formatter
-      def rouge_formatter(lexer)
-        Rouge::Formatters::HTML.new
+      def rouge_formatter(lexer = nil)
+        @rouge_formatter ||= Rouge::Formatters::HTML.new
       end
     end
   end
diff --git a/lib/banzai/filter/video_link_filter.rb b/lib/banzai/filter/video_link_filter.rb
index fd8b9a6f0cc642d6f2f30bd270acdc4c65e79c63..ac7bbcb0d10a7c5119af269138cabda389495e53 100644
--- a/lib/banzai/filter/video_link_filter.rb
+++ b/lib/banzai/filter/video_link_filter.rb
@@ -1,11 +1,9 @@
 module Banzai
   module Filter
-
     # Find every image that isn't already wrapped in an `a` tag, and that has
     # a `src` attribute ending with a video extension, add a new video node and
     # a "Download" link in the case the video cannot be played.
     class VideoLinkFilter < HTML::Pipeline::Filter
-
       def call
         doc.xpath(query).each do |el|
           el.replace(video_node(doc, el))
@@ -54,6 +52,5 @@ module Banzai
         container
       end
     end
-
   end
 end
diff --git a/lib/ci/api/builds.rb b/lib/ci/api/builds.rb
index 260ac81f5fab8ea822a250a664d8426f2ab8b7a0..9f3b582a263c0a9d5aa35f1570bf787778742184 100644
--- a/lib/ci/api/builds.rb
+++ b/lib/ci/api/builds.rb
@@ -20,8 +20,13 @@ module Ci
           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
+            Gitlab::Metrics.add_event(:build_not_found)
+
             not_found!
           end
         end
@@ -42,6 +47,9 @@ module Ci
 
           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
diff --git a/lib/ci/gitlab_ci_yaml_processor.rb b/lib/ci/gitlab_ci_yaml_processor.rb
index a2e8bd22a525786bc0bac9aa8a62303131e4598b..47efd5bd9f264eb3685d0f07e252bff49dedb8f5 100644
--- a/lib/ci/gitlab_ci_yaml_processor.rb
+++ b/lib/ci/gitlab_ci_yaml_processor.rb
@@ -62,7 +62,7 @@ module Ci
         #  - before script should be a concatenated command
         commands: [job[:before_script] || @before_script, job[:script]].flatten.compact.join("\n"),
         tag_list: job[:tags] || [],
-        name: job[:name],
+        name: job[:name].to_s,
         allow_failure: job[:allow_failure] || false,
         when: job[:when] || 'on_success',
         environment: job[:environment],
diff --git a/lib/ci/static_model.rb b/lib/ci/static_model.rb
deleted file mode 100644
index bb2bdbed49519850058a2ba4d9127ba760ad885b..0000000000000000000000000000000000000000
--- a/lib/ci/static_model.rb
+++ /dev/null
@@ -1,49 +0,0 @@
-# Provides an ActiveRecord-like interface to a model whose data is not persisted to a database.
-module Ci
-  module StaticModel
-    extend ActiveSupport::Concern
-
-    module ClassMethods
-      # Used by ActiveRecord's polymorphic association to set object_id
-      def primary_key
-        'id'
-      end
-
-      # Used by ActiveRecord's polymorphic association to set object_type
-      def base_class
-        self
-      end
-    end
-
-    # Used by AR for fetching attributes
-    #
-    # Pass it along if we respond to it.
-    def [](key)
-      send(key) if respond_to?(key)
-    end
-
-    def to_param
-      id
-    end
-
-    def new_record?
-      false
-    end
-
-    def persisted?
-      false
-    end
-
-    def destroyed?
-      false
-    end
-
-    def ==(other)
-      if other.is_a? ::Ci::StaticModel
-        id == other.id
-      else
-        super
-      end
-    end
-  end
-end
diff --git a/lib/extracts_path.rb b/lib/extracts_path.rb
index 51e46da82ccd24dd61ff938167bc728518d0820f..a293fa2752f8ab7878d4cfb7c358049f2b649247 100644
--- a/lib/extracts_path.rb
+++ b/lib/extracts_path.rb
@@ -94,7 +94,9 @@ module ExtractsPath
     @options = params.select {|key, value| allowed_options.include?(key) && !value.blank? }
     @options = HashWithIndifferentAccess.new(@options)
 
-    @id = Addressable::URI.unescape(get_id)
+    @id = params[:id] || params[:ref]
+    @id += "/" + params[:path] unless params[:path].blank?
+
     @ref, @path = extract_ref(@id)
     @repo = @project.repository
     if @options[:extended_sha1].blank?
@@ -116,12 +118,4 @@ module ExtractsPath
   def tree
     @tree ||= @repo.tree(@commit.id, @path)
   end
-
-  private
-
-  def get_id
-    id = params[:id] || params[:ref]
-    id += "/" + params[:path] unless params[:path].blank?
-    id
-  end
 end
diff --git a/lib/gitlab/akismet_helper.rb b/lib/gitlab/akismet_helper.rb
deleted file mode 100644
index 207736b59db18fdd55c22908eb94a6416b2910f7..0000000000000000000000000000000000000000
--- a/lib/gitlab/akismet_helper.rb
+++ /dev/null
@@ -1,47 +0,0 @@
-module Gitlab
-  module AkismetHelper
-    def akismet_enabled?
-      current_application_settings.akismet_enabled
-    end
-
-    def akismet_client
-      @akismet_client ||= ::Akismet::Client.new(current_application_settings.akismet_api_key,
-        Gitlab.config.gitlab.url)
-    end
-
-    def client_ip(env)
-      env['action_dispatch.remote_ip'].to_s
-    end
-
-    def user_agent(env)
-      env['HTTP_USER_AGENT']
-    end
-
-    def check_for_spam?(project)
-      akismet_enabled? && project.public?
-    end
-
-    def is_spam?(environment, user, text)
-      client = akismet_client
-      ip_address = client_ip(environment)
-      user_agent = user_agent(environment)
-
-      params = {
-        type: 'comment',
-        text: text,
-        created_at: DateTime.now,
-        author: user.name,
-        author_email: user.email,
-        referrer: environment['HTTP_REFERER'],
-      }
-
-      begin
-        is_spam, is_blatant = client.check(ip_address, user_agent, params)
-        is_spam || is_blatant
-      rescue => e
-        Rails.logger.error("Unable to connect to Akismet: #{e}, skipping check")
-        false
-      end
-    end
-  end
-end
diff --git a/lib/gitlab/auth.rb b/lib/gitlab/auth.rb
index db1704af75ebb26077ce156ea508f78520f8b423..91f0270818a3f2568dbcb2edcf9a810f74ab4f33 100644
--- a/lib/gitlab/auth.rb
+++ b/lib/gitlab/auth.rb
@@ -10,13 +10,12 @@ module Gitlab
 
         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
+        else
+          result = populate_result(login, password)
         end
 
-        rate_limit!(ip, success: !!result.user || (result.type == :ci), login: login)
+        success = result.user.present? || [:ci, :missing_personal_token].include?(result.type)
+        rate_limit!(ip, success: success, login: login)
         result
       end
 
@@ -76,10 +75,43 @@ module Gitlab
         end
       end
 
+      def populate_result(login, password)
+        result =
+          user_with_password_for_git(login, password) ||
+          oauth_access_token_check(login, password) ||
+          personal_access_token_check(login, password)
+
+        if result
+          result.type = nil unless result.user
+
+          if result.user && result.user.two_factor_enabled? && result.type == :gitlab_or_ldap
+            result.type = :missing_personal_token
+          end
+        end
+
+        result || Result.new
+      end
+
+      def user_with_password_for_git(login, password)
+        user = find_with_user_password(login, password)
+        Result.new(user, :gitlab_or_ldap) if user
+      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)
+            Result.new(user, :oauth)
+          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)
+          Result.new(user, :personal_token) if user == validation
         end
       end
     end
diff --git a/lib/gitlab/backend/grack_auth.rb b/lib/gitlab/backend/grack_auth.rb
deleted file mode 100644
index ab94abeda7719396865170842a90219816319ed0..0000000000000000000000000000000000000000
--- a/lib/gitlab/backend/grack_auth.rb
+++ /dev/null
@@ -1,163 +0,0 @@
-module Grack
-  class AuthSpawner
-    def self.call(env)
-      # Avoid issues with instance variables in Grack::Auth persisting across
-      # requests by creating a new instance for each request.
-      Auth.new({}).call(env)
-    end
-  end
-
-  class Auth < Rack::Auth::Basic
-    attr_accessor :user, :project, :env
-
-    def call(env)
-      @env = env
-      @request = Rack::Request.new(env)
-      @auth = Request.new(env)
-
-      @ci = false
-
-      # Need this patch due to the rails mount
-      # Need this if under RELATIVE_URL_ROOT
-      unless Gitlab.config.gitlab.relative_url_root.empty?
-        # If website is mounted using relative_url_root need to remove it first
-        @env['PATH_INFO'] = @request.path.sub(Gitlab.config.gitlab.relative_url_root, '')
-      else
-        @env['PATH_INFO'] = @request.path
-      end
-
-      @env['SCRIPT_NAME'] = ""
-
-      auth!
-
-      lfs_response = Gitlab::Lfs::Router.new(project, @user, @ci, @request).try_call
-      return lfs_response unless lfs_response.nil?
-
-      if @user.nil? && !@ci
-        unauthorized
-      else
-        render_not_found
-      end
-    end
-
-    private
-
-    def auth!
-      return unless @auth.provided?
-
-      return bad_request unless @auth.basic?
-
-      # Authentication with username and password
-      login, password = @auth.credentials
-
-      # Allow authentication for GitLab CI service
-      # if valid token passed
-      if ci_request?(login, password)
-        @ci = true
-        return
-      end
-
-      @user = authenticate_user(login, password)
-    end
-
-    def ci_request?(login, password)
-      matched_login = /(?<s>^[a-zA-Z]*-ci)-token$/.match(login)
-
-      if project && matched_login.present?
-        underscored_service = matched_login['s'].underscore
-
-        if underscored_service == 'gitlab_ci'
-          return project && project.valid_build_token?(password)
-        elsif Service.available_services_names.include?(underscored_service)
-          service_method = "#{underscored_service}_service"
-          service = project.send(service_method)
-
-          return service && service.activated? && service.valid_token?(password)
-        end
-      end
-
-      false
-    end
-
-    def oauth_access_token_check(login, password)
-      if login == "oauth2" && git_cmd == 'git-upload-pack' && password.present?
-        token = Doorkeeper::AccessToken.by_token(password)
-        token && token.accessible? && User.find_by(id: token.resource_owner_id)
-      end
-    end
-
-    def authenticate_user(login, password)
-      user = Gitlab::Auth.find_with_user_password(login, password)
-
-      unless user
-        user = oauth_access_token_check(login, password)
-      end
-
-      # If the user authenticated successfully, we reset the auth failure count
-      # from Rack::Attack for that IP. A client may attempt to authenticate
-      # with a username and blank password first, and only after it receives
-      # a 401 error does it present a password. Resetting the count prevents
-      # false positives from occurring.
-      #
-      # Otherwise, we let Rack::Attack know there was a failed authentication
-      # attempt from this IP. This information is stored in the Rails cache
-      # (Redis) and will be used by the Rack::Attack middleware to decide
-      # whether to block requests from this IP.
-      config = Gitlab.config.rack_attack.git_basic_auth
-
-      if config.enabled
-        if user
-          # A successful login will reset the auth failure count from this IP
-          Rack::Attack::Allow2Ban.reset(@request.ip, config)
-        else
-          banned = Rack::Attack::Allow2Ban.filter(@request.ip, config) do
-            # Unless the IP is whitelisted, return true so that Allow2Ban
-            # increments the counter (stored in Rails.cache) for the IP
-            if config.ip_whitelist.include?(@request.ip)
-              false
-            else
-              true
-            end
-          end
-
-          if banned
-            Rails.logger.info "IP #{@request.ip} failed to login " \
-              "as #{login} but has been temporarily banned from Git auth"
-          end
-        end
-      end
-
-      user
-    end
-
-    def git_cmd
-      if @request.get?
-        @request.params['service']
-      elsif @request.post?
-        File.basename(@request.path)
-      else
-        nil
-      end
-    end
-
-    def project
-      return @project if defined?(@project)
-
-      @project = project_by_path(@request.path_info)
-    end
-
-    def project_by_path(path)
-      if m = /^([\w\.\/-]+)\.git/.match(path).to_a
-        path_with_namespace = m.last
-        path_with_namespace.gsub!(/\.wiki$/, '')
-
-        path_with_namespace[0] = '' if path_with_namespace.start_with?('/')
-        Project.find_with_namespace(path_with_namespace)
-      end
-    end
-
-    def render_not_found
-      [404, { "Content-Type" => "text/plain" }, ["Not Found"]]
-    end
-  end
-end
diff --git a/lib/gitlab/badge/base.rb b/lib/gitlab/badge/base.rb
new file mode 100644
index 0000000000000000000000000000000000000000..909fa24fa90a0bcce7de70f09bf2fa25d27ec0f3
--- /dev/null
+++ b/lib/gitlab/badge/base.rb
@@ -0,0 +1,21 @@
+module Gitlab
+  module Badge
+    class Base
+      def entity
+        raise NotImplementedError
+      end
+
+      def status
+        raise NotImplementedError
+      end
+
+      def metadata
+        raise NotImplementedError
+      end
+
+      def template
+        raise NotImplementedError
+      end
+    end
+  end
+end
diff --git a/lib/gitlab/badge/build.rb b/lib/gitlab/badge/build.rb
deleted file mode 100644
index e5e9fab3f5c8c8a7cff53de608a79c95ff199678..0000000000000000000000000000000000000000
--- a/lib/gitlab/badge/build.rb
+++ /dev/null
@@ -1,46 +0,0 @@
-module Gitlab
-  module Badge
-    ##
-    # Build badge
-    #
-    class Build
-      include Gitlab::Application.routes.url_helpers
-      include ActionView::Helpers::AssetTagHelper
-      include ActionView::Helpers::UrlHelper
-
-      def initialize(project, ref)
-        @project, @ref = project, ref
-        @image = ::Ci::ImageForBuildService.new.execute(project, ref: ref)
-      end
-
-      def type
-        'image/svg+xml'
-      end
-
-      def data
-        File.read(@image[:path])
-      end
-
-      def to_s
-        @image[:name].sub(/\.svg$/, '')
-      end
-
-      def to_html
-        link_to(image_tag(image_url, alt: 'build status'), link_url)
-      end
-
-      def to_markdown
-        "[![build status](#{image_url})](#{link_url})"
-      end
-
-      def image_url
-        build_namespace_project_badges_url(@project.namespace,
-                                           @project, @ref, format: :svg)
-      end
-
-      def link_url
-        namespace_project_commits_url(@project.namespace, @project, id: @ref)
-      end
-    end
-  end
-end
diff --git a/lib/gitlab/badge/build/metadata.rb b/lib/gitlab/badge/build/metadata.rb
new file mode 100644
index 0000000000000000000000000000000000000000..f87a7b7942ef7cdba6fbb37f6513b0d76d127054
--- /dev/null
+++ b/lib/gitlab/badge/build/metadata.rb
@@ -0,0 +1,28 @@
+module Gitlab
+  module Badge
+    module Build
+      ##
+      # Class that describes build badge metadata
+      #
+      class Metadata < Badge::Metadata
+        def initialize(badge)
+          @project = badge.project
+          @ref = badge.ref
+        end
+
+        def title
+          'build status'
+        end
+
+        def image_url
+          build_namespace_project_badges_url(@project.namespace,
+                                             @project, @ref, format: :svg)
+        end
+
+        def link_url
+          namespace_project_commits_url(@project.namespace, @project, id: @ref)
+        end
+      end
+    end
+  end
+end
diff --git a/lib/gitlab/badge/build/status.rb b/lib/gitlab/badge/build/status.rb
new file mode 100644
index 0000000000000000000000000000000000000000..50aa45e540674456729a3c66977a446312d74e1b
--- /dev/null
+++ b/lib/gitlab/badge/build/status.rb
@@ -0,0 +1,37 @@
+module Gitlab
+  module Badge
+    module Build
+      ##
+      # Build status badge
+      #
+      class Status < Badge::Base
+        attr_reader :project, :ref
+
+        def initialize(project, ref)
+          @project = project
+          @ref = ref
+
+          @sha = @project.commit(@ref).try(:sha)
+        end
+
+        def entity
+          'build'
+        end
+
+        def status
+          @project.pipelines
+            .where(sha: @sha, ref: @ref)
+            .status || 'unknown'
+        end
+
+        def metadata
+          @metadata ||= Build::Metadata.new(self)
+        end
+
+        def template
+          @template ||= Build::Template.new(self)
+        end
+      end
+    end
+  end
+end
diff --git a/lib/gitlab/badge/build/template.rb b/lib/gitlab/badge/build/template.rb
new file mode 100644
index 0000000000000000000000000000000000000000..2b95ddfcb53e6ecd308575bc7c94a4c94a98a675
--- /dev/null
+++ b/lib/gitlab/badge/build/template.rb
@@ -0,0 +1,47 @@
+module Gitlab
+  module Badge
+    module Build
+      ##
+      # Class that represents a build badge template.
+      #
+      # Template object will be passed to badge.svg.erb template.
+      #
+      class Template < Badge::Template
+        STATUS_COLOR = {
+          success: '#4c1',
+          failed: '#e05d44',
+          running: '#dfb317',
+          pending: '#dfb317',
+          canceled: '#9f9f9f',
+          skipped: '#9f9f9f',
+          unknown: '#9f9f9f'
+        }
+
+        def initialize(badge)
+          @entity = badge.entity
+          @status = badge.status
+        end
+
+        def key_text
+          @entity.to_s
+        end
+
+        def value_text
+          @status.to_s
+        end
+
+        def key_width
+          38
+        end
+
+        def value_width
+          54
+        end
+
+        def value_color
+          STATUS_COLOR[@status.to_sym] || STATUS_COLOR[:unknown]
+        end
+      end
+    end
+  end
+end
diff --git a/lib/gitlab/badge/coverage/metadata.rb b/lib/gitlab/badge/coverage/metadata.rb
new file mode 100644
index 0000000000000000000000000000000000000000..5358818562298a4af49697d8e8708fb155b9566d
--- /dev/null
+++ b/lib/gitlab/badge/coverage/metadata.rb
@@ -0,0 +1,30 @@
+module Gitlab
+  module Badge
+    module Coverage
+      ##
+      # Class that describes coverage badge metadata
+      #
+      class Metadata < Badge::Metadata
+        def initialize(badge)
+          @project = badge.project
+          @ref = badge.ref
+          @job = badge.job
+        end
+
+        def title
+          'coverage report'
+        end
+
+        def image_url
+          coverage_namespace_project_badges_url(@project.namespace,
+                                                @project, @ref,
+                                                format: :svg)
+        end
+
+        def link_url
+          namespace_project_commits_url(@project.namespace, @project, id: @ref)
+        end
+      end
+    end
+  end
+end
diff --git a/lib/gitlab/badge/coverage/report.rb b/lib/gitlab/badge/coverage/report.rb
new file mode 100644
index 0000000000000000000000000000000000000000..95d925dc7f3eccb01165f0cd6f1ba2a92b655faa
--- /dev/null
+++ b/lib/gitlab/badge/coverage/report.rb
@@ -0,0 +1,55 @@
+module Gitlab
+  module Badge
+    module Coverage
+      ##
+      # Test coverage report badge
+      #
+      class Report < Badge::Base
+        attr_reader :project, :ref, :job
+
+        def initialize(project, ref, job = nil)
+          @project = project
+          @ref = ref
+          @job = job
+
+          @pipeline = @project.pipelines
+            .latest_successful_for(@ref)
+            .first
+        end
+
+        def entity
+          'coverage'
+        end
+
+        def status
+          @coverage ||= raw_coverage
+          return unless @coverage
+
+          @coverage.to_i
+        end
+
+        def metadata
+          @metadata ||= Coverage::Metadata.new(self)
+        end
+
+        def template
+          @template ||= Coverage::Template.new(self)
+        end
+
+        private
+
+        def raw_coverage
+          return unless @pipeline
+
+          if @job.blank?
+            @pipeline.coverage
+          else
+            @pipeline.builds
+              .find_by(name: @job)
+              .try(:coverage)
+          end
+        end
+      end
+    end
+  end
+end
diff --git a/lib/gitlab/badge/coverage/template.rb b/lib/gitlab/badge/coverage/template.rb
new file mode 100644
index 0000000000000000000000000000000000000000..06e0d084e9f1fd7bc4ffd85f5a19213591881943
--- /dev/null
+++ b/lib/gitlab/badge/coverage/template.rb
@@ -0,0 +1,52 @@
+module Gitlab
+  module Badge
+    module Coverage
+      ##
+      # Class that represents a coverage badge template.
+      #
+      # Template object will be passed to badge.svg.erb template.
+      #
+      class Template < Badge::Template
+        STATUS_COLOR = {
+          good: '#4c1',
+          acceptable: '#a3c51c',
+          medium: '#dfb317',
+          low: '#e05d44',
+          unknown: '#9f9f9f'
+        }
+
+        def initialize(badge)
+          @entity = badge.entity
+          @status = badge.status
+        end
+
+        def key_text
+          @entity.to_s
+        end
+
+        def value_text
+          @status ? "#{@status}%" : 'unknown'
+        end
+
+        def key_width
+          62
+        end
+
+        def value_width
+          @status ? 36 : 58
+        end
+
+        def value_color
+          case @status
+          when 95..100 then STATUS_COLOR[:good]
+          when 90..95 then STATUS_COLOR[:acceptable]
+          when 75..90 then STATUS_COLOR[:medium]
+          when 0..75 then STATUS_COLOR[:low]
+          else
+            STATUS_COLOR[:unknown]
+          end
+        end
+      end
+    end
+  end
+end
diff --git a/lib/gitlab/badge/metadata.rb b/lib/gitlab/badge/metadata.rb
new file mode 100644
index 0000000000000000000000000000000000000000..548f85b78bb20e3f9f56a6e881c9ca5350912746
--- /dev/null
+++ b/lib/gitlab/badge/metadata.rb
@@ -0,0 +1,36 @@
+module Gitlab
+  module Badge
+    ##
+    # Abstract class for badge metadata
+    #
+    class Metadata
+      include Gitlab::Application.routes.url_helpers
+      include ActionView::Helpers::AssetTagHelper
+      include ActionView::Helpers::UrlHelper
+
+      def initialize(badge)
+        @badge = badge
+      end
+
+      def to_html
+        link_to(image_tag(image_url, alt: title), link_url)
+      end
+
+      def to_markdown
+        "[![#{title}](#{image_url})](#{link_url})"
+      end
+
+      def title
+        raise NotImplementedError
+      end
+
+      def image_url
+        raise NotImplementedError
+      end
+
+      def link_url
+        raise NotImplementedError
+      end
+    end
+  end
+end
diff --git a/lib/gitlab/badge/template.rb b/lib/gitlab/badge/template.rb
new file mode 100644
index 0000000000000000000000000000000000000000..bfeb00526420c2e257827311a1386511cdbec944
--- /dev/null
+++ b/lib/gitlab/badge/template.rb
@@ -0,0 +1,49 @@
+module Gitlab
+  module Badge
+    ##
+    # Abstract template class for badges
+    #
+    class Template
+      def initialize(badge)
+        @entity = badge.entity
+        @status = badge.status
+      end
+
+      def key_text
+        raise NotImplementedError
+      end
+
+      def value_text
+        raise NotImplementedError
+      end
+
+      def key_width
+        raise NotImplementedError
+      end
+
+      def value_width
+        raise NotImplementedError
+      end
+
+      def value_color
+        raise NotImplementedError
+      end
+
+      def key_color
+        '#555'
+      end
+
+      def key_text_anchor
+        key_width / 2
+      end
+
+      def value_text_anchor
+        key_width + (value_width / 2)
+      end
+
+      def width
+        key_width + value_width
+      end
+    end
+  end
+end
diff --git a/lib/gitlab/changes_list.rb b/lib/gitlab/changes_list.rb
new file mode 100644
index 0000000000000000000000000000000000000000..95308aca95f788cadfb7190650b587bf4e44a6ed
--- /dev/null
+++ b/lib/gitlab/changes_list.rb
@@ -0,0 +1,25 @@
+module Gitlab
+  class ChangesList
+    include Enumerable
+
+    attr_reader :raw_changes
+
+    def initialize(changes)
+      @raw_changes = changes.kind_of?(String) ? changes.lines : changes
+    end
+
+    def each(&block)
+      changes.each(&block)
+    end
+
+    def changes
+      @changes ||= begin
+        @raw_changes.map do |change|
+          next if change.blank?
+          oldrev, newrev, ref = change.strip.split(' ')
+          { oldrev: oldrev, newrev: newrev, ref: ref }
+        end.compact
+      end
+    end
+  end
+end
diff --git a/lib/gitlab/checks/change_access.rb b/lib/gitlab/checks/change_access.rb
index 5551fac4b8bdf6b14400ffe699bf0be43d3dba0a..4b32eb966aa050c48c3afe1b6f83a276f0f372ad 100644
--- a/lib/gitlab/checks/change_access.rb
+++ b/lib/gitlab/checks/change_access.rb
@@ -4,14 +4,14 @@ module Gitlab
       attr_reader :user_access, :project
 
       def initialize(change, user_access:, project:)
-        @oldrev, @newrev, @ref = change.split(' ')
-        @branch_name = branch_name(@ref)
+        @oldrev, @newrev, @ref = change.values_at(:oldrev, :newrev, :ref)
+        @branch_name = Gitlab::Git.branch_name(@ref)
         @user_access = user_access
         @project = project
       end
 
       def exec
-        error = protected_branch_checks || tag_checks || push_checks
+        error = push_checks || tag_checks || protected_branch_checks
 
         if error
           GitAccessStatus.new(false, error)
@@ -47,7 +47,7 @@ module Gitlab
       end
 
       def tag_checks
-        tag_ref = tag_name(@ref)
+        tag_ref = Gitlab::Git.tag_name(@ref)
 
         if tag_ref && protected_tag?(tag_ref) && user_access.cannot_do_action?(:admin_project)
           "You are not allowed to change existing tags on this project."
@@ -73,24 +73,6 @@ module Gitlab
       def matching_merge_request?
         Checks::MatchingMergeRequest.new(@newrev, @branch_name, @project).match?
       end
-
-      def branch_name(ref)
-        ref = @ref.to_s
-        if Gitlab::Git.branch_ref?(ref)
-          Gitlab::Git.ref_name(ref)
-        else
-          nil
-        end
-      end
-
-      def tag_name(ref)
-        ref = @ref.to_s
-        if Gitlab::Git.tag_ref?(ref)
-          Gitlab::Git.ref_name(ref)
-        else
-          nil
-        end
-      end
     end
   end
 end
diff --git a/lib/gitlab/ci/config/node/validatable.rb b/lib/gitlab/ci/config/node/validatable.rb
index f6e2896dfb23ce2760a62aa46f50a1dbc5ba15e6..085e6e988d1085f59bc9814343b8d4d8d0897868 100644
--- a/lib/gitlab/ci/config/node/validatable.rb
+++ b/lib/gitlab/ci/config/node/validatable.rb
@@ -7,13 +7,11 @@ module Gitlab
 
           class_methods do
             def validator
-              validator = Class.new(Node::Validator)
-
-              if defined?(@validations)
-                @validations.each { |rules| validator.class_eval(&rules) }
+              @validator ||= Class.new(Node::Validator).tap do |validator|
+                if defined?(@validations)
+                  @validations.each { |rules| validator.class_eval(&rules) }
+                end
               end
-
-              validator
             end
 
             private
diff --git a/lib/gitlab/closing_issue_extractor.rb b/lib/gitlab/closing_issue_extractor.rb
index 9bef9037ad6dda28ed0480d3a75d7afde0840c9e..58f86abc5c4f219e45ef1bcb8ba9cd319c2d1ccf 100644
--- a/lib/gitlab/closing_issue_extractor.rb
+++ b/lib/gitlab/closing_issue_extractor.rb
@@ -22,7 +22,9 @@ module Gitlab
 
       @extractor.analyze(closing_statements.join(" "))
 
-      @extractor.issues
+      @extractor.issues.reject do |issue|
+        @extractor.project.forked_from?(issue.project) # Don't extract issues on original project
+      end
     end
   end
 end
diff --git a/lib/gitlab/conflict/file.rb b/lib/gitlab/conflict/file.rb
new file mode 100644
index 0000000000000000000000000000000000000000..0a1fd27ced542e1e8c8848217f6d96eb97187075
--- /dev/null
+++ b/lib/gitlab/conflict/file.rb
@@ -0,0 +1,186 @@
+module Gitlab
+  module Conflict
+    class File
+      include Gitlab::Routing.url_helpers
+      include IconsHelper
+
+      class MissingResolution < StandardError
+      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
+
+      # Array of Gitlab::Diff::Line objects
+      def lines
+        @lines ||= Gitlab::Conflict::Parser.new.parse(merge_file_result[:data],
+                                                      our_path: our_path,
+                                                      their_path: their_path,
+                                                      parent_file: self)
+      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 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 = nil)
+        {
+          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)),
+          sections: sections
+        }
+      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..bbd0427a2c82766707c6079447915148c065930e
--- /dev/null
+++ b/lib/gitlab/conflict/file_collection.rb
@@ -0,0 +1,57 @@
+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 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..6eccded7872791b5c6fab5ec2da4a1dc5e8f01fb
--- /dev/null
+++ b/lib/gitlab/conflict/parser.rb
@@ -0,0 +1,62 @@
+module Gitlab
+  module Conflict
+    class Parser
+      class ParserError < StandardError
+      end
+
+      class UnexpectedDelimiter < ParserError
+      end
+
+      class MissingEndDelimiter < ParserError
+      end
+
+      class UnmergeableFile < 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 > 102400
+
+        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/current_settings.rb b/lib/gitlab/current_settings.rb
index 735331df66cf4c72acfb0bc0b4c5a3e23c143a0a..27acd817e516511ee304fefd3f465a8d79ad28a1 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,
diff --git a/lib/gitlab/build_data_builder.rb b/lib/gitlab/data_builder/build.rb
similarity index 96%
rename from lib/gitlab/build_data_builder.rb
rename to lib/gitlab/data_builder/build.rb
index 9f45aefda0f6937eaa1d8e52689e429b4a61e849..6548e6475c60fcd5da5aca3a92fc225f10b5d2e1 100644
--- a/lib/gitlab/build_data_builder.rb
+++ b/lib/gitlab/data_builder/build.rb
@@ -1,6 +1,8 @@
 module Gitlab
-  class BuildDataBuilder
-    class << self
+  module DataBuilder
+    module Build
+      extend self
+
       def build(build)
         project = build.project
         commit = build.pipeline
diff --git a/lib/gitlab/note_data_builder.rb b/lib/gitlab/data_builder/note.rb
similarity index 97%
rename from lib/gitlab/note_data_builder.rb
rename to lib/gitlab/data_builder/note.rb
index 8bdc89a7751f3f77a8a001b51b57dcf2641e9dd1..50fea1232af72daa78b7226bfdff5d8bdafba414 100644
--- a/lib/gitlab/note_data_builder.rb
+++ b/lib/gitlab/data_builder/note.rb
@@ -1,6 +1,8 @@
 module Gitlab
-  class NoteDataBuilder
-    class << self
+  module DataBuilder
+    module Note
+      extend self
+
       # Produce a hash of post-receive data
       #
       # For all notes:
diff --git a/lib/gitlab/data_builder/pipeline.rb b/lib/gitlab/data_builder/pipeline.rb
new file mode 100644
index 0000000000000000000000000000000000000000..06a783ebc1c2636a9ed63512342b601d4dae3210
--- /dev/null
+++ b/lib/gitlab/data_builder/pipeline.rb
@@ -0,0 +1,62 @@
+module Gitlab
+  module DataBuilder
+    module Pipeline
+      extend self
+
+      def build(pipeline)
+        {
+          object_kind: 'pipeline',
+          object_attributes: hook_attrs(pipeline),
+          user: pipeline.user.try(:hook_attrs),
+          project: pipeline.project.hook_attrs(backward: false),
+          commit: pipeline.commit.try(:hook_attrs),
+          builds: pipeline.builds.map(&method(:build_hook_attrs))
+        }
+      end
+
+      def hook_attrs(pipeline)
+        {
+          id: pipeline.id,
+          ref: pipeline.ref,
+          tag: pipeline.tag,
+          sha: pipeline.sha,
+          before_sha: pipeline.before_sha,
+          status: pipeline.status,
+          stages: pipeline.stages,
+          created_at: pipeline.created_at,
+          finished_at: pipeline.finished_at,
+          duration: pipeline.duration
+        }
+      end
+
+      def build_hook_attrs(build)
+        {
+          id: build.id,
+          stage: build.stage,
+          name: build.name,
+          status: build.status,
+          created_at: build.created_at,
+          started_at: build.started_at,
+          finished_at: build.finished_at,
+          when: build.when,
+          manual: build.manual?,
+          user: build.user.try(:hook_attrs),
+          runner: build.runner && runner_hook_attrs(build.runner),
+          artifacts_file: {
+            filename: build.artifacts_file.filename,
+            size: build.artifacts_size
+          }
+        }
+      end
+
+      def runner_hook_attrs(runner)
+        {
+          id: runner.id,
+          description: runner.description,
+          active: runner.active?,
+          is_shared: runner.is_shared?
+        }
+      end
+    end
+  end
+end
diff --git a/lib/gitlab/push_data_builder.rb b/lib/gitlab/data_builder/push.rb
similarity index 98%
rename from lib/gitlab/push_data_builder.rb
rename to lib/gitlab/data_builder/push.rb
index c8f12577112eaed1562bb49cc7b31b0a0b477090..4f81863da35a917b6976991bb14b9aed87887090 100644
--- a/lib/gitlab/push_data_builder.rb
+++ b/lib/gitlab/data_builder/push.rb
@@ -1,6 +1,8 @@
 module Gitlab
-  class PushDataBuilder
-    class << self
+  module DataBuilder
+    module Push
+      extend self
+
       # Produce a hash of post-receive data
       #
       # data = {
diff --git a/lib/gitlab/diff/file.rb b/lib/gitlab/diff/file.rb
index b09ca1fb8b0c785f4d6cf78683b78137706d54cb..e47df508ca29fa570bbc699893c0d4219591b775 100644
--- a/lib/gitlab/diff/file.rb
+++ b/lib/gitlab/diff/file.rb
@@ -63,15 +63,18 @@ module Gitlab
         diff_refs.try(:head_sha)
       end
 
+      attr_writer :highlighted_diff_lines
+
       # Array of Gitlab::Diff::Line objects
       def diff_lines
-        @lines ||= Gitlab::Diff::Parser.new.parse(raw_diff.each_line).to_a
+        @diff_lines ||= Gitlab::Diff::Parser.new.parse(raw_diff.each_line).to_a
       end
 
       def highlighted_diff_lines
         @highlighted_diff_lines ||= Gitlab::Diff::Highlight.new(self, repository: self.repository).highlight
       end
 
+      # Array[<Hash>] with right/left keys that contains Gitlab::Diff::Line objects which text is hightlighted
       def parallel_diff_lines
         @parallel_diff_lines ||= Gitlab::Diff::ParallelDiff.new(self).parallelize
       end
diff --git a/lib/gitlab/diff/file_collection/base.rb b/lib/gitlab/diff/file_collection/base.rb
new file mode 100644
index 0000000000000000000000000000000000000000..2b9fc65b9858397976eeb90aa071f89599083381
--- /dev/null
+++ b/lib/gitlab/diff/file_collection/base.rb
@@ -0,0 +1,35 @@
+module Gitlab
+  module Diff
+    module FileCollection
+      class Base
+        attr_reader :project, :diff_options, :diff_view, :diff_refs
+
+        delegate :count, :size, :real_size, to: :diff_files
+
+        def self.default_options
+          ::Commit.max_diff_options.merge(ignore_whitespace_change: false, no_collapse: false)
+        end
+
+        def initialize(diffable, project:, diff_options: nil, diff_refs: nil)
+          diff_options = self.class.default_options.merge(diff_options || {})
+
+          @diffable     = diffable
+          @diffs        = diffable.raw_diffs(diff_options)
+          @project      = project
+          @diff_options = diff_options
+          @diff_refs    = diff_refs
+        end
+
+        def diff_files
+          @diff_files ||= @diffs.decorate! { |diff| decorate_diff!(diff) }
+        end
+
+        private
+
+        def decorate_diff!(diff)
+          Gitlab::Diff::File.new(diff, repository: project.repository, diff_refs: diff_refs)
+        end
+      end
+    end
+  end
+end
diff --git a/lib/gitlab/diff/file_collection/commit.rb b/lib/gitlab/diff/file_collection/commit.rb
new file mode 100644
index 0000000000000000000000000000000000000000..4dc297ec036aa6801d4459e25df9cdbd5d2a1973
--- /dev/null
+++ b/lib/gitlab/diff/file_collection/commit.rb
@@ -0,0 +1,14 @@
+module Gitlab
+  module Diff
+    module FileCollection
+      class Commit < Base
+        def initialize(commit, diff_options:)
+          super(commit,
+            project: commit.project,
+            diff_options: diff_options,
+            diff_refs: commit.diff_refs)
+        end
+      end
+    end
+  end
+end
diff --git a/lib/gitlab/diff/file_collection/compare.rb b/lib/gitlab/diff/file_collection/compare.rb
new file mode 100644
index 0000000000000000000000000000000000000000..20d8f891cc333f581c91b1ab36c25606401c4e28
--- /dev/null
+++ b/lib/gitlab/diff/file_collection/compare.rb
@@ -0,0 +1,14 @@
+module Gitlab
+  module Diff
+    module FileCollection
+      class Compare < Base
+        def initialize(compare, project:, diff_options:, diff_refs: nil)
+          super(compare,
+            project:      project,
+            diff_options: diff_options,
+            diff_refs:    diff_refs)
+        end
+      end
+    end
+  end
+end
diff --git a/lib/gitlab/diff/file_collection/merge_request.rb b/lib/gitlab/diff/file_collection/merge_request.rb
new file mode 100644
index 0000000000000000000000000000000000000000..4f946908e2f9c63eb42ad71030d0511ad71402f3
--- /dev/null
+++ b/lib/gitlab/diff/file_collection/merge_request.rb
@@ -0,0 +1,73 @@
+module Gitlab
+  module Diff
+    module FileCollection
+      class MergeRequest < Base
+        def initialize(merge_request, diff_options:)
+          @merge_request = merge_request
+
+          super(merge_request,
+            project: merge_request.project,
+            diff_options: diff_options,
+            diff_refs: merge_request.diff_refs)
+        end
+
+        def diff_files
+          super.tap { |_| store_highlight_cache }
+        end
+
+        private
+
+        # Extracted method to highlight in the same iteration to the diff_collection.
+        def decorate_diff!(diff)
+          diff_file = super
+          cache_highlight!(diff_file) if cacheable?
+          diff_file
+        end
+
+        def highlight_diff_file_from_cache!(diff_file, cache_diff_lines)
+          diff_file.highlighted_diff_lines = cache_diff_lines.map do |line|
+            Gitlab::Diff::Line.init_from_hash(line)
+          end
+        end
+
+        #
+        # If we find the highlighted diff files lines on the cache we replace existing diff_files lines (no highlighted)
+        # 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
+        # hashes that represent serialized diff lines.
+        #
+        def cache_highlight!(diff_file)
+          file_path = diff_file.file_path
+
+          if highlight_cache[file_path]
+            highlight_diff_file_from_cache!(diff_file, highlight_cache[file_path])
+          else
+            highlight_cache[file_path] = diff_file.highlighted_diff_lines.map(&:to_hash)
+          end
+        end
+
+        def highlight_cache
+          return @highlight_cache if defined?(@highlight_cache)
+
+          @highlight_cache = Rails.cache.read(cache_key) || {}
+          @highlight_cache_was_empty = @highlight_cache.empty?
+          @highlight_cache
+        end
+
+        def store_highlight_cache
+          Rails.cache.write(cache_key, highlight_cache) if @highlight_cache_was_empty
+        end
+
+        def cacheable?
+          @merge_request.merge_request_diff.present?
+        end
+
+        def cache_key
+          [@merge_request.merge_request_diff, 'highlighted-diff-files', diff_options]
+        end
+      end
+    end
+  end
+end
diff --git a/lib/gitlab/diff/highlight.rb b/lib/gitlab/diff/highlight.rb
index 649a265a02c34e5130aeafd70617d80141dfeb9d..9ea976e18fae04401dd3d26b4303289843bb0201 100644
--- a/lib/gitlab/diff/highlight.rb
+++ b/lib/gitlab/diff/highlight.rb
@@ -40,8 +40,6 @@ module Gitlab
       def highlight_line(diff_line)
         return unless diff_file && diff_file.diff_refs
 
-        line_prefix = diff_line.text.match(/\A(.)/) ? $1 : ' '
-
         rich_line =
           if diff_line.unchanged? || diff_line.added?
             new_lines[diff_line.new_pos - 1]
@@ -51,7 +49,10 @@ module Gitlab
 
         # Only update text if line is found. This will prevent
         # issues with submodules given the line only exists in diff content.
-        "#{line_prefix}#{rich_line}".html_safe if rich_line
+        if rich_line
+          line_prefix = diff_line.text.match(/\A(.)/) ? $1 : ' '
+          "#{line_prefix}#{rich_line}".html_safe
+        end
       end
 
       def inline_diffs
diff --git a/lib/gitlab/diff/line.rb b/lib/gitlab/diff/line.rb
index c6189d660c2b6304eab2899abab26702da9e4b4e..80a146b4a5a96f490b0252b5404232486ad8a4aa 100644
--- a/lib/gitlab/diff/line.rb
+++ b/lib/gitlab/diff/line.rb
@@ -2,11 +2,27 @@ 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)
+        new(hash[:text], hash[:type], hash[:index], hash[:old_pos], hash[:new_pos])
+      end
+
+      def serialize_keys
+        @serialize_keys ||= %i(text type index old_pos new_pos)
+      end
+
+      def to_hash
+        hash = {}
+        serialize_keys.each { |key| hash[key] = send(key) }
+        hash
       end
 
       def old_line
@@ -29,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/email/handler.rb b/lib/gitlab/email/handler.rb
index bd3267e2a80ba54f31753d732b664b19e8838583..5cf9d5ebe28d67be964a8fc4cd9beac8e106dac6 100644
--- a/lib/gitlab/email/handler.rb
+++ b/lib/gitlab/email/handler.rb
@@ -4,7 +4,8 @@ require 'gitlab/email/handler/create_issue_handler'
 module Gitlab
   module Email
     module Handler
-      HANDLERS = [CreateNoteHandler, CreateIssueHandler]
+      # The `CreateIssueHandler` feature is disabled for the time being.
+      HANDLERS = [CreateNoteHandler]
 
       def self.for(mail, mail_key)
         HANDLERS.find do |klass|
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/message/repository_push.rb b/lib/gitlab/email/message/repository_push.rb
index 97701b0cd42cf7831ec3e0afa4bb078cab763603..0e3b65fceb4fe7b31ad1b9e9c6990ecc3e98d21e 100644
--- a/lib/gitlab/email/message/repository_push.rb
+++ b/lib/gitlab/email/message/repository_push.rb
@@ -35,21 +35,22 @@ module Gitlab
         def commits
           return unless compare
 
-          @commits ||= Commit.decorate(compare.commits, project)
+          @commits ||= compare.commits
         end
 
         def diffs
           return unless compare
-          
-          @diffs ||= safe_diff_files(compare.diffs(max_files: 30), diff_refs: diff_refs, repository: project.repository)
+
+          # This diff is more moderated in number of files and lines
+          @diffs ||= compare.diffs(max_files: 30, max_lines: 5000, no_collapse: true).diff_files
         end
 
         def diffs_count
-          diffs.count if diffs
+          diffs.size if diffs
         end
 
         def compare
-          @opts[:compare]
+          @opts[:compare] if @opts[:compare]
         end
 
         def diff_refs
@@ -97,16 +98,18 @@ module Gitlab
             if commits.length > 1
               namespace_project_compare_url(project_namespace,
                                             project,
-                                            from: Commit.new(compare.base, project),
-                                            to:   Commit.new(compare.head, project))
+                                            from: compare.start_commit,
+                                            to:   compare.head_commit)
             else
               namespace_project_commit_url(project_namespace,
-                                           project, commits.first)
+                                           project,
+                                           commits.first)
             end
           else
             unless @action == :delete
               namespace_project_tree_url(project_namespace,
-                                         project, ref_name)
+                                         project,
+                                         ref_name)
             end
           end
         end
diff --git a/lib/gitlab/email/receiver.rb b/lib/gitlab/email/receiver.rb
index 9213cfb51e8354ed99768f80f65aec717738981f..a40c44eb1bc5fe1557f68e7848c24167759233b9 100644
--- a/lib/gitlab/email/receiver.rb
+++ b/lib/gitlab/email/receiver.rb
@@ -1,5 +1,5 @@
 
-require 'gitlab/email/handler'
+require_dependency 'gitlab/email/handler'
 
 # Inspired in great part by Discourse's Email::Receiver
 module Gitlab
diff --git a/lib/gitlab/git.rb b/lib/gitlab/git.rb
index 191bea86ac378d1be090c02cb1399e55946a38a7..7584efe4fa864dde1a23c253dde68037841dff58 100644
--- a/lib/gitlab/git.rb
+++ b/lib/gitlab/git.rb
@@ -9,6 +9,24 @@ module Gitlab
         ref.gsub(/\Arefs\/(tags|heads)\//, '')
       end
 
+      def branch_name(ref)
+        ref = ref.to_s
+        if self.branch_ref?(ref)
+          self.ref_name(ref)
+        else
+          nil
+        end
+      end
+
+      def tag_name(ref)
+        ref = ref.to_s
+        if self.tag_ref?(ref)
+          self.ref_name(ref)
+        else
+          nil
+        end
+      end
+
       def tag_ref?(ref)
         ref.start_with?(TAG_REF_PREFIX)
       end
diff --git a/lib/gitlab/git_access.rb b/lib/gitlab/git_access.rb
index 8e8f39d9cb25435ee7a8bb9faafda59b8e3eeb79..1882eb8d0508c34b98096a99ea3553682c6df963 100644
--- a/lib/gitlab/git_access.rb
+++ b/lib/gitlab/git_access.rb
@@ -14,7 +14,7 @@ module Gitlab
       @user_access = UserAccess.new(user, project: project)
     end
 
-    def check(cmd, changes = nil)
+    def check(cmd, changes)
       return build_status_object(false, "Git access over #{protocol.upcase} is not allowed") unless protocol_allowed?
 
       unless actor
@@ -76,10 +76,10 @@ module Gitlab
         return build_status_object(false, "A repository for this project does not exist yet.")
       end
 
-      changes = changes.lines if changes.kind_of?(String)
+      changes_list = Gitlab::ChangesList.new(changes)
 
       # Iterate over all changes to find if user allowed all of them to be applied
-      changes.map(&:strip).reject(&:blank?).each do |change|
+      changes_list.each do |change|
         status = change_access_check(change)
         unless status.allowed?
           # If user does not have access to make at least one change - cancel all push
@@ -134,7 +134,7 @@ module Gitlab
     end
 
     def build_status_object(status, message = '')
-      GitAccessStatus.new(status, message)
+      Gitlab::GitAccessStatus.new(status, message)
     end
   end
 end
diff --git a/lib/gitlab/git_post_receive.rb b/lib/gitlab/git_post_receive.rb
index a088e19d1e7959d228df335b73a398df2d40ba9a..d32bdd86427cf963d17b23fd45a06e2b5ac931f9 100644
--- a/lib/gitlab/git_post_receive.rb
+++ b/lib/gitlab/git_post_receive.rb
@@ -39,7 +39,6 @@ module Gitlab
     end
 
     def deserialize_changes(changes)
-      changes = Base64.decode64(changes) unless changes.include?(' ')
       changes = utf8_encode_changes(changes)
       changes.lines
     end
diff --git a/lib/gitlab/github_import/branch_formatter.rb b/lib/gitlab/github_import/branch_formatter.rb
index 7d2d545b84e50130920a27ec5245375ecda99da0..4750675ae9ddf75afb954de6da314c393c40f18a 100644
--- a/lib/gitlab/github_import/branch_formatter.rb
+++ b/lib/gitlab/github_import/branch_formatter.rb
@@ -7,10 +7,6 @@ module Gitlab
         branch_exists? && commit_exists?
       end
 
-      def name
-        @name ||= exists? ? ref : "#{ref}-#{short_id}"
-      end
-
       def valid?
         repo.present?
       end
diff --git a/lib/gitlab/github_import/hook_formatter.rb b/lib/gitlab/github_import/hook_formatter.rb
deleted file mode 100644
index db1fabaa18af50ed0f1d241ac8395361e7d21cc5..0000000000000000000000000000000000000000
--- a/lib/gitlab/github_import/hook_formatter.rb
+++ /dev/null
@@ -1,23 +0,0 @@
-module Gitlab
-  module GithubImport
-    class HookFormatter
-      EVENTS = %w[* create delete pull_request push].freeze
-
-      attr_reader :raw
-
-      delegate :id, :name, :active, to: :raw
-
-      def initialize(raw)
-        @raw = raw
-      end
-
-      def config
-        raw.config.attrs
-      end
-
-      def valid?
-        (EVENTS & raw.events).any? && active
-      end
-    end
-  end
-end
diff --git a/lib/gitlab/github_import/importer.rb b/lib/gitlab/github_import/importer.rb
index 3932fcb1eda26f05459a1e13cfa3b8f2506c5e9f..9ddc8905bd6149f32bf58fbb896afc45787613c5 100644
--- a/lib/gitlab/github_import/importer.rb
+++ b/lib/gitlab/github_import/importer.rb
@@ -12,7 +12,6 @@ module Gitlab
 
         if credentials
           @client = Client.new(credentials[:user])
-          @formatter = Gitlab::ImportFormatter.new
         else
           raise Projects::ImportService::Error, "Unable to find project import data credentials for project ID: #{@project.id}"
         end
@@ -66,73 +65,45 @@ module Gitlab
       end
 
       def import_pull_requests
-        disable_webhooks
-
         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?)
 
-        source_branches_removed = pull_requests.reject(&:source_branch_exists?).map { |pr| [pr.source_branch_name, pr.source_branch_sha] }
-        target_branches_removed = pull_requests.reject(&:target_branch_exists?).map { |pr| [pr.target_branch_name, pr.target_branch_sha] }
-        branches_removed = source_branches_removed | target_branches_removed
-
-        restore_branches(branches_removed)
-
         pull_requests.each do |pull_request|
-          merge_request = pull_request.create!
-          apply_labels(merge_request)
-          import_comments(merge_request)
-          import_comments_on_diff(merge_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)
+          end
         end
 
         true
-      rescue ActiveRecord::RecordInvalid => e
-        raise Projects::ImportService::Error, e.message
-      ensure
-        clean_up_restored_branches(branches_removed)
-        clean_up_disabled_webhooks
       end
 
-      def disable_webhooks
-        update_webhooks(hooks, active: false)
+      def restore_source_branch(pull_request)
+        project.repository.fetch_ref(repo_url, "pull/#{pull_request.number}/head", pull_request.source_branch_name)
       end
 
-      def clean_up_disabled_webhooks
-        update_webhooks(hooks, active: true)
+      def restore_target_branch(pull_request)
+        project.repository.create_branch(pull_request.target_branch_name, pull_request.target_branch_sha)
       end
 
-      def update_webhooks(hooks, options)
-        hooks.each do |hook|
-          client.edit_hook(repo, hook.id, hook.name, hook.config, options)
-        end
+      def remove_branch(name)
+        project.repository.delete_branch(name)
+      rescue Rugged::ReferenceError
+        nil
       end
 
-      def hooks
-        @hooks ||=
-          begin
-            client.hooks(repo).map { |raw| HookFormatter.new(raw) }.select(&:valid?)
-
-          # The GitHub Repository Webhooks API returns 404 for users
-          # without admin access to the repository when listing hooks.
-          # In this case we just want to return gracefully instead of
-          # spitting out an error and stop the import process.
-          rescue Octokit::NotFound
-            []
-          end
-      end
-
-      def restore_branches(branches)
-        branches.each do |name, sha|
-          client.create_ref(repo, "refs/heads/#{name}", sha)
-        end
-
-        project.repository.fetch_ref(repo_url, '+refs/heads/*', 'refs/heads/*')
-      end
-
-      def clean_up_restored_branches(branches)
-        branches.each do |name, _|
-          client.delete_ref(repo, "heads/#{name}")
-          project.repository.delete_branch(name) rescue Rugged::ReferenceError
-        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
diff --git a/lib/gitlab/github_import/pull_request_formatter.rb b/lib/gitlab/github_import/pull_request_formatter.rb
index a4ea2210abdba794f13aa4a869972645229f7712..b84538a090a268a0e97af332dd2528579941ecb1 100644
--- a/lib/gitlab/github_import/pull_request_formatter.rb
+++ b/lib/gitlab/github_import/pull_request_formatter.rb
@@ -1,8 +1,8 @@
 module Gitlab
   module GithubImport
     class PullRequestFormatter < BaseFormatter
-      delegate :exists?, :name, :project, :repo, :sha, to: :source_branch, prefix: true
-      delegate :exists?, :name, :project, :repo, :sha, to: :target_branch, prefix: true
+      delegate :exists?, :project, :ref, :repo, :sha, to: :source_branch, prefix: true
+      delegate :exists?, :project, :ref, :repo, :sha, to: :target_branch, prefix: true
 
       def attributes
         {
@@ -33,17 +33,29 @@ module Gitlab
       end
 
       def valid?
-        source_branch.valid? && target_branch.valid? && !cross_project?
+        source_branch.valid? && target_branch.valid?
       end
 
       def source_branch
         @source_branch ||= BranchFormatter.new(project, raw_data.head)
       end
 
+      def source_branch_name
+        @source_branch_name ||= begin
+          source_branch_exists? ? source_branch_ref : "pull/#{number}/#{source_branch_ref}"
+        end
+      end
+
       def target_branch
         @target_branch ||= BranchFormatter.new(project, raw_data.base)
       end
 
+      def target_branch_name
+        @target_branch_name ||= begin
+          target_branch_exists? ? target_branch_ref : "pull/#{number}/#{target_branch_ref}"
+        end
+      end
+
       private
 
       def assigned?
@@ -68,10 +80,6 @@ module Gitlab
         raw_data.body || ""
       end
 
-      def cross_project?
-        source_branch_repo.id != target_branch_repo.id
-      end
-
       def description
         formatter.author_line(author) + body
       end
diff --git a/lib/gitlab/import_export.rb b/lib/gitlab/import_export.rb
index 48b2c43ac21cee166310d10c870920054b5a5365..bb562bdcd2c0fb8df35d3b3a98c4d18058c1e27a 100644
--- a/lib/gitlab/import_export.rb
+++ b/lib/gitlab/import_export.rb
@@ -13,6 +13,10 @@ module Gitlab
       File.join(Settings.shared['path'], 'tmp/project_exports')
     end
 
+    def import_upload_path(filename:)
+      File.join(storage_path, 'uploads', filename)
+    end
+
     def project_filename
       "project.json"
     end
diff --git a/lib/gitlab/import_export/avatar_restorer.rb b/lib/gitlab/import_export/avatar_restorer.rb
index 352539eb594ead70aec67accc7d348ba31776f15..cfa595629f4758fd8ee6bd6d04d0f37d311fdb5b 100644
--- a/lib/gitlab/import_export/avatar_restorer.rb
+++ b/lib/gitlab/import_export/avatar_restorer.rb
@@ -1,7 +1,6 @@
 module Gitlab
   module ImportExport
     class AvatarRestorer
-
       def initialize(project:, shared:)
         @project = project
         @shared = shared
diff --git a/lib/gitlab/import_export/json_hash_builder.rb b/lib/gitlab/import_export/json_hash_builder.rb
index 008300bde4557bb3c4e9de762a0187606704b1c2..0cc10f4008712da59483bea5ce4efba8922a8cf6 100644
--- a/lib/gitlab/import_export/json_hash_builder.rb
+++ b/lib/gitlab/import_export/json_hash_builder.rb
@@ -57,19 +57,16 @@ module Gitlab
       # +value+ existing model to be included in the hash
       # +json_config_hash+ the original hash containing the root model
       def create_model_value(current_key, value, json_config_hash)
-        parsed_hash = { include: value }
-        parse_hash(value, parsed_hash)
-
-        json_config_hash[current_key] = parsed_hash
+        json_config_hash[current_key] = parse_hash(value) || { include: value }
       end
 
       # Calls attributes finder to parse the hash and add any attributes to it
       #
       # +value+ existing model to be included in the hash
       # +parsed_hash+ the original hash
-      def parse_hash(value, parsed_hash)
+      def parse_hash(value)
         @attributes_finder.parse(value) do |hash|
-          parsed_hash = { include: hash_or_merge(value, hash) }
+          { include: hash_or_merge(value, hash) }
         end
       end
 
diff --git a/lib/gitlab/import_export/members_mapper.rb b/lib/gitlab/import_export/members_mapper.rb
index b459054c198a0c826f457bdfbebb7d934a2337a4..36c4cf6efa0493deef2d098cdb3f625bf96902e1 100644
--- a/lib/gitlab/import_export/members_mapper.rb
+++ b/lib/gitlab/import_export/members_mapper.rb
@@ -18,11 +18,14 @@ module Gitlab
         @map ||=
           begin
             @exported_members.inject(missing_keys_tracking_hash) do |hash, member|
-              existing_user = User.where(find_project_user_query(member)).first
-              old_user_id = member['user']['id']
-              if existing_user && add_user_as_team_member(existing_user, member)
-                hash[old_user_id] = existing_user.id
+              if member['user']
+                old_user_id = member['user']['id']
+                existing_user = User.where(find_project_user_query(member)).first
+                hash[old_user_id] = existing_user.id if existing_user && add_team_member(member, existing_user)
+              else
+                add_team_member(member)
               end
+
               hash
             end
           end
@@ -45,7 +48,7 @@ module Gitlab
         ProjectMember.create!(user: @user, access_level: ProjectMember::MASTER, source_id: @project.id, importing: true)
       end
 
-      def add_user_as_team_member(existing_user, member)
+      def add_team_member(member, existing_user = nil)
         member['user'] = existing_user
 
         ProjectMember.create(member_hash(member)).persisted?
diff --git a/lib/gitlab/import_export/relation_factory.rb b/lib/gitlab/import_export/relation_factory.rb
index e9c1b79fa45573a901b861c7641ac92d35c99a2b..b0726268ca617ce6f6807eeb5afef80d38f8da10 100644
--- a/lib/gitlab/import_export/relation_factory.rb
+++ b/lib/gitlab/import_export/relation_factory.rb
@@ -44,7 +44,7 @@ module Gitlab
         if @relation_name == :notes
           set_note_author
 
-          # TODO: note attatchments not supported yet
+          # attachment is deprecated and note uploads are handled by Markdown uploader
           @relation_hash['attachment'] = nil
         end
 
@@ -102,17 +102,19 @@ module Gitlab
       def update_project_references
         project_id = @relation_hash.delete('project_id')
 
+        # If source and target are the same, populate them with the new project ID.
+        if @relation_hash['source_project_id']
+          @relation_hash['source_project_id'] = same_source_and_target? ? project_id : -1
+        end
+
         # project_id may not be part of the export, but we always need to populate it if required.
         @relation_hash['project_id'] = project_id
         @relation_hash['gl_project_id'] = project_id if @relation_hash['gl_project_id']
         @relation_hash['target_project_id'] = project_id if @relation_hash['target_project_id']
-        @relation_hash['source_project_id'] = -1 if @relation_hash['source_project_id']
+      end
 
-        # If source and target are the same, populate them with the new project ID.
-        if @relation_hash['source_project_id'] && @relation_hash['target_project_id'] &&
-          @relation_hash['target_project_id'] == @relation_hash['source_project_id']
-          @relation_hash['source_project_id'] = project_id
-        end
+      def same_source_and_target?
+        @relation_hash['target_project_id'] && @relation_hash['target_project_id'] == @relation_hash['source_project_id']
       end
 
       def reset_ci_tokens
diff --git a/lib/gitlab/import_export/version_checker.rb b/lib/gitlab/import_export/version_checker.rb
index abfc694b87947ec165b4189b988209719bf98f42..de3fe6d822e0273ac15d4295be47245365247635 100644
--- a/lib/gitlab/import_export/version_checker.rb
+++ b/lib/gitlab/import_export/version_checker.rb
@@ -25,7 +25,7 @@ module Gitlab
 
       def verify_version!(version)
         if Gem::Version.new(version) > Gem::Version.new(Gitlab::ImportExport.version)
-          raise Gitlab::ImportExport::Error("Import version mismatch: Required <= #{Gitlab::ImportExport.version} but was #{version}")
+          raise Gitlab::ImportExport::Error.new("Import version mismatch: Required <= #{Gitlab::ImportExport.version} but was #{version}")
         else
           true
         end
diff --git a/lib/gitlab/ldap/access.rb b/lib/gitlab/ldap/access.rb
index f2b649e50a20bc1805d9b917dd25f6dd0784d2af..2f326d00a2f4b37188509e077cc9a18270fc70ff 100644
--- a/lib/gitlab/ldap/access.rb
+++ b/lib/gitlab/ldap/access.rb
@@ -25,7 +25,7 @@ module Gitlab
         end
       end
 
-      def initialize(user, adapter=nil)
+      def initialize(user, adapter = nil)
         @adapter = adapter
         @user = user
         @provider = user.ldap_identity.provider
diff --git a/lib/gitlab/ldap/adapter.rb b/lib/gitlab/ldap/adapter.rb
index df65179bfeadedb6d611c51dabb54b972eeca6e8..9a5bcfb5c9bab9f8df436219f30f8a95a58b903a 100644
--- a/lib/gitlab/ldap/adapter.rb
+++ b/lib/gitlab/ldap/adapter.rb
@@ -13,7 +13,7 @@ module Gitlab
         Gitlab::LDAP::Config.new(provider)
       end
 
-      def initialize(provider, ldap=nil)
+      def initialize(provider, ldap = nil)
         @provider = provider
         @ldap = ldap || Net::LDAP.new(config.adapter_options)
       end
diff --git a/lib/gitlab/lfs/response.rb b/lib/gitlab/lfs/response.rb
deleted file mode 100644
index a1ee1aa81ff090de51b6c1db73076ed5261a5dca..0000000000000000000000000000000000000000
--- a/lib/gitlab/lfs/response.rb
+++ /dev/null
@@ -1,329 +0,0 @@
-module Gitlab
-  module Lfs
-    class Response
-      def initialize(project, user, ci, request)
-        @origin_project = project
-        @project = storage_project(project)
-        @user = user
-        @ci = ci
-        @env = request.env
-        @request = request
-      end
-
-      def render_download_object_response(oid)
-        render_response_to_download do
-          if check_download_sendfile_header?
-            render_lfs_sendfile(oid)
-          else
-            render_not_found
-          end
-        end
-      end
-
-      def render_batch_operation_response
-        request_body = JSON.parse(@request.body.read)
-        case request_body["operation"]
-        when "download"
-          render_batch_download(request_body)
-        when "upload"
-          render_batch_upload(request_body)
-        else
-          render_not_found
-        end
-      end
-
-      def render_storage_upload_authorize_response(oid, size)
-        render_response_to_push do
-          [
-            200,
-            { "Content-Type" => "application/json; charset=utf-8" },
-            [JSON.dump({
-              'StoreLFSPath' => "#{Gitlab.config.lfs.storage_path}/tmp/upload",
-              'LfsOid' => oid,
-              'LfsSize' => size
-            })]
-          ]
-        end
-      end
-
-      def render_storage_upload_store_response(oid, size, tmp_file_name)
-        return render_forbidden unless tmp_file_name
-
-        render_response_to_push do
-          render_lfs_upload_ok(oid, size, tmp_file_name)
-        end
-      end
-
-      def render_unsupported_deprecated_api
-        [
-          501,
-          { "Content-Type" => "application/json; charset=utf-8" },
-          [JSON.dump({
-            'message' => 'Server supports batch API only, please update your Git LFS client to version 1.0.1 and up.',
-            'documentation_url' => "#{Gitlab.config.gitlab.url}/help",
-          })]
-        ]
-      end
-
-      private
-
-      def render_not_enabled
-        [
-          501,
-          {
-            "Content-Type" => "application/json; charset=utf-8",
-          },
-          [JSON.dump({
-            'message' => 'Git LFS is not enabled on this GitLab server, contact your admin.',
-            'documentation_url' => "#{Gitlab.config.gitlab.url}/help",
-          })]
-        ]
-      end
-
-      def render_unauthorized
-        [
-          401,
-          {
-            'Content-Type' => 'text/plain'
-          },
-          ['Unauthorized']
-        ]
-      end
-
-      def render_not_found
-        [
-          404,
-          {
-            "Content-Type" => "application/vnd.git-lfs+json"
-          },
-          [JSON.dump({
-            'message' => 'Not found.',
-            'documentation_url' => "#{Gitlab.config.gitlab.url}/help",
-          })]
-        ]
-      end
-
-      def render_forbidden
-        [
-          403,
-          {
-            "Content-Type" => "application/vnd.git-lfs+json"
-          },
-          [JSON.dump({
-            'message' => 'Access forbidden. Check your access level.',
-            'documentation_url' => "#{Gitlab.config.gitlab.url}/help",
-          })]
-        ]
-      end
-
-      def render_lfs_sendfile(oid)
-        return render_not_found unless oid.present?
-
-        lfs_object = object_for_download(oid)
-
-        if lfs_object && lfs_object.file.exists?
-          [
-            200,
-            {
-              # GitLab-workhorse will forward Content-Type header
-              "Content-Type" => "application/octet-stream",
-              "X-Sendfile" => lfs_object.file.path
-            },
-            []
-          ]
-        else
-          render_not_found
-        end
-      end
-
-      def render_batch_upload(body)
-        return render_not_found if body.empty? || body['objects'].nil?
-
-        render_response_to_push do
-          response = build_upload_batch_response(body['objects'])
-          [
-            200,
-            {
-              "Content-Type" => "application/json; charset=utf-8",
-              "Cache-Control" => "private",
-            },
-            [JSON.dump(response)]
-          ]
-        end
-      end
-
-      def render_batch_download(body)
-        return render_not_found if body.empty? || body['objects'].nil?
-
-        render_response_to_download do
-          response = build_download_batch_response(body['objects'])
-          [
-            200,
-            {
-              "Content-Type" => "application/json; charset=utf-8",
-              "Cache-Control" => "private",
-            },
-            [JSON.dump(response)]
-          ]
-        end
-      end
-
-      def render_lfs_upload_ok(oid, size, tmp_file)
-        if store_file(oid, size, tmp_file)
-          [
-            200,
-            {
-              'Content-Type' => 'text/plain',
-              'Content-Length' => 0
-            },
-            []
-          ]
-        else
-          [
-            422,
-            { 'Content-Type' => 'text/plain' },
-            ["Unprocessable entity"]
-          ]
-        end
-      end
-
-      def render_response_to_download
-        return render_not_enabled unless Gitlab.config.lfs.enabled
-
-        unless @project.public?
-          return render_unauthorized unless @user || @ci
-          return render_forbidden unless user_can_fetch?
-        end
-
-        yield
-      end
-
-      def render_response_to_push
-        return render_not_enabled unless Gitlab.config.lfs.enabled
-        return render_unauthorized unless @user
-        return render_forbidden unless user_can_push?
-
-        yield
-      end
-
-      def check_download_sendfile_header?
-        @env['HTTP_X_SENDFILE_TYPE'].to_s == "X-Sendfile"
-      end
-
-      def user_can_fetch?
-        # Check user access against the project they used to initiate the pull
-        @ci || @user.can?(:download_code, @origin_project)
-      end
-
-      def user_can_push?
-        # Check user access against the project they used to initiate the push
-        @user.can?(:push_code, @origin_project)
-      end
-
-      def storage_project(project)
-        if project.forked?
-          storage_project(project.forked_from_project)
-        else
-          project
-        end
-      end
-
-      def store_file(oid, size, tmp_file)
-        tmp_file_path = File.join("#{Gitlab.config.lfs.storage_path}/tmp/upload", tmp_file)
-
-        object = LfsObject.find_or_create_by(oid: oid, size: size)
-        if object.file.exists?
-          success = true
-        else
-          success = move_tmp_file_to_storage(object, tmp_file_path)
-        end
-
-        if success
-          success = link_to_project(object)
-        end
-
-        success
-      ensure
-        # Ensure that the tmp file is removed
-        FileUtils.rm_f(tmp_file_path)
-      end
-
-      def object_for_download(oid)
-        @project.lfs_objects.find_by(oid: oid)
-      end
-
-      def move_tmp_file_to_storage(object, path)
-        File.open(path) do |f|
-          object.file = f
-        end
-
-        object.file.store!
-        object.save
-      end
-
-      def link_to_project(object)
-        if object && !object.projects.exists?(@project.id)
-          object.projects << @project
-          object.save
-        end
-      end
-
-      def select_existing_objects(objects)
-        objects_oids = objects.map { |o| o['oid'] }
-        @project.lfs_objects.where(oid: objects_oids).pluck(:oid).to_set
-      end
-
-      def build_upload_batch_response(objects)
-        selected_objects = select_existing_objects(objects)
-
-        upload_hypermedia_links(objects, selected_objects)
-      end
-
-      def build_download_batch_response(objects)
-        selected_objects = select_existing_objects(objects)
-
-        download_hypermedia_links(objects, selected_objects)
-      end
-
-      def download_hypermedia_links(all_objects, existing_objects)
-        all_objects.each do |object|
-          if existing_objects.include?(object['oid'])
-            object['actions'] = {
-              'download' => {
-                'href' => "#{@origin_project.http_url_to_repo}/gitlab-lfs/objects/#{object['oid']}",
-                'header' => {
-                  'Authorization' => @env['HTTP_AUTHORIZATION']
-                }.compact
-              }
-            }
-          else
-            object['error'] = {
-              'code' => 404,
-              'message' => "Object does not exist on the server or you don't have permissions to access it",
-            }
-          end
-        end
-
-        { 'objects' => all_objects }
-      end
-
-      def upload_hypermedia_links(all_objects, existing_objects)
-        all_objects.each do |object|
-          # generate actions only for non-existing objects
-          next if existing_objects.include?(object['oid'])
-
-          object['actions'] = {
-            'upload' => {
-              'href' => "#{@origin_project.http_url_to_repo}/gitlab-lfs/objects/#{object['oid']}/#{object['size']}",
-              'header' => {
-                'Authorization' => @env['HTTP_AUTHORIZATION']
-              }.compact
-            }
-          }
-        end
-
-        { 'objects' => all_objects }
-      end
-    end
-  end
-end
diff --git a/lib/gitlab/lfs/router.rb b/lib/gitlab/lfs/router.rb
deleted file mode 100644
index f2a76a56b8f2452ea1a789e90feb20a548830060..0000000000000000000000000000000000000000
--- a/lib/gitlab/lfs/router.rb
+++ /dev/null
@@ -1,98 +0,0 @@
-module Gitlab
-  module Lfs
-    class Router
-      attr_reader :project, :user, :ci, :request
-
-      def initialize(project, user, ci, request)
-        @project = project
-        @user = user
-        @ci = ci
-        @env = request.env
-        @request = request
-      end
-
-      def try_call
-        return unless @request && @request.path.present?
-
-        case @request.request_method
-        when 'GET'
-          get_response
-        when 'POST'
-          post_response
-        when 'PUT'
-          put_response
-        else
-          nil
-        end
-      end
-
-      private
-
-      def get_response
-        path_match = @request.path.match(/\/(info\/lfs|gitlab-lfs)\/objects\/([0-9a-f]{64})$/)
-        return nil unless path_match
-
-        oid = path_match[2]
-        return nil unless oid
-
-        case path_match[1]
-        when "info/lfs"
-          lfs.render_unsupported_deprecated_api
-        when "gitlab-lfs"
-          lfs.render_download_object_response(oid)
-        else
-          nil
-        end
-      end
-
-      def post_response
-        post_path = @request.path.match(/\/info\/lfs\/objects(\/batch)?$/)
-        return nil unless post_path
-
-        # Check for Batch API
-        if post_path[0].ends_with?("/info/lfs/objects/batch")
-          lfs.render_batch_operation_response
-        elsif post_path[0].ends_with?("/info/lfs/objects")
-          lfs.render_unsupported_deprecated_api
-        else
-          nil
-        end
-      end
-
-      def put_response
-        object_match = @request.path.match(/\/gitlab-lfs\/objects\/([0-9a-f]{64})\/([0-9]+)(|\/authorize){1}$/)
-        return nil if object_match.nil?
-
-        oid = object_match[1]
-        size = object_match[2].try(:to_i)
-        return nil if oid.nil? || size.nil?
-
-        # GitLab-workhorse requests
-        # 1. Try to authorize the request
-        # 2. send a request with a header containing the name of the temporary file
-        if object_match[3] && object_match[3] == '/authorize'
-          lfs.render_storage_upload_authorize_response(oid, size)
-        else
-          tmp_file_name = sanitize_tmp_filename(@request.env['HTTP_X_GITLAB_LFS_TMP'])
-          lfs.render_storage_upload_store_response(oid, size, tmp_file_name)
-        end
-      end
-
-      def lfs
-        return unless @project
-
-        Gitlab::Lfs::Response.new(@project, @user, @ci, @request)
-      end
-
-      def sanitize_tmp_filename(name)
-        if name.present?
-          name.gsub!(/^.*(\\|\/)/, '')
-          name = name.match(/[0-9a-f]{73}/)
-          name[0] if name
-        else
-          nil
-        end
-      end
-    end
-  end
-end
diff --git a/lib/gitlab/mail_room.rb b/lib/gitlab/mail_room.rb
new file mode 100644
index 0000000000000000000000000000000000000000..12999a90a298fa78ef7c31ca5c9a1c6398199641
--- /dev/null
+++ b/lib/gitlab/mail_room.rb
@@ -0,0 +1,47 @@
+require 'yaml'
+require 'json'
+require_relative 'redis' unless defined?(Gitlab::Redis)
+
+module Gitlab
+  module MailRoom
+    class << self
+      def enabled?
+        config[:enabled] && config[:address]
+      end
+
+      def config
+        @config ||= fetch_config
+      end
+
+      def reset_config!
+        @config = nil
+      end
+
+      private
+
+      def fetch_config
+        return {} unless File.exist?(config_file)
+
+        rails_env = ENV['RAILS_ENV'] || ENV['RACK_ENV'] || 'development'
+        all_config = YAML.load_file(config_file)[rails_env].deep_symbolize_keys
+
+        config = all_config[:incoming_email] || {}
+        config[:enabled] = false if config[:enabled].nil?
+        config[:port] = 143 if config[:port].nil?
+        config[:ssl] = false if config[:ssl].nil?
+        config[:start_tls] = false if config[:start_tls].nil?
+        config[:mailbox] = 'inbox' if config[:mailbox].nil?
+
+        if config[:enabled] && config[:address]
+          config[:redis_url] = Gitlab::Redis.new(rails_env).url
+        end
+
+        config
+      end
+
+      def config_file
+        ENV['MAIL_ROOM_GITLAB_CONFIG_FILE'] || File.expand_path('../../../config/gitlab.yml', __FILE__)
+      end
+    end
+  end
+end
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..b4493bf44d28eacde9d5828ab30db6598aa48ed7 100644
--- a/lib/gitlab/metrics/rack_middleware.rb
+++ b/lib/gitlab/metrics/rack_middleware.rb
@@ -17,6 +17,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
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/popen.rb b/lib/gitlab/popen.rb
index 43e07e0916045d84d4587cfa35b7ec34aa9ba9db..ca23ccef25bb23ce236764a3463951c6d3494b9c 100644
--- a/lib/gitlab/popen.rb
+++ b/lib/gitlab/popen.rb
@@ -5,7 +5,7 @@ module Gitlab
   module Popen
     extend self
 
-    def popen(cmd, path=nil)
+    def popen(cmd, path = nil)
       unless cmd.is_a?(Array)
         raise "System commands must be given as an array of strings"
       end
diff --git a/lib/gitlab/redis.rb b/lib/gitlab/redis.rb
index 40766f35f779846d93a7d4b3e0a94d38df93b429..9376b54f43bc102cf1c5494659332adda38d515e 100644
--- a/lib/gitlab/redis.rb
+++ b/lib/gitlab/redis.rb
@@ -1,50 +1,94 @@
+# This file should not have any direct dependency on Rails environment
+# please require all dependencies below:
+require 'active_support/core_ext/hash/keys'
+
 module Gitlab
   class Redis
     CACHE_NAMESPACE = 'cache:gitlab'
     SESSION_NAMESPACE = 'session:gitlab'
     SIDEKIQ_NAMESPACE = 'resque:gitlab'
-
-    attr_reader :url
+    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.
-    URL_MUTEX = Mutex.new
+    PARAMS_MUTEX = Mutex.new
     POOL_MUTEX = Mutex.new
-    private_constant :URL_MUTEX, :POOL_MUTEX
+    private_constant :PARAMS_MUTEX, :POOL_MUTEX
 
-    def self.url
-      @url || URL_MUTEX.synchronize { @url = new.url }
-    end
+    class << self
+      def params
+        @params || PARAMS_MUTEX.synchronize { @params = new.params }
+      end
+
+      # @deprecated Use .params instead to get sentinel support
+      def url
+        new.url
+      end
 
-    def self.with
-      if @pool.nil?
-        POOL_MUTEX.synchronize do
-          @pool = ConnectionPool.new { ::Redis.new(url: url) }
+      def with
+        if @pool.nil?
+          POOL_MUTEX.synchronize do
+            @pool = ConnectionPool.new { ::Redis.new(params) }
+          end
         end
+        @pool.with { |redis| yield redis }
+      end
+
+      def reset_params!
+        @params = nil
       end
-      @pool.with { |redis| yield redis }
     end
 
-    def self.redis_store_options
-      url = new.url
-      redis_config_hash = ::Redis::Store::Factory.extract_host_options_from_uri(url)
-      # Redis::Store does not handle Unix sockets well, so let's do it for them
-      redis_uri = URI.parse(url)
+    def initialize(rails_env = nil)
+      @rails_env = rails_env || ::Rails.env
+    end
+
+    def params
+      redis_store_options
+    end
+
+    def url
+      raw_config_hash[:url]
+    end
+
+    private
+
+    def redis_store_options
+      config = raw_config_hash
+      redis_url = config.delete(:url)
+      redis_uri = URI.parse(redis_url)
+
       if redis_uri.scheme == 'unix'
-        redis_config_hash[:path] = redis_uri.path
+        # Redis::Store does not handle Unix sockets well, so let's do it for them
+        config[:path] = redis_uri.path
+        config
+      else
+        redis_hash = ::Redis::Store::Factory.extract_host_options_from_uri(redis_url)
+        # order is important here, sentinels must be after the connection keys.
+        # {url: ..., port: ..., sentinels: [...]}
+        redis_hash.merge(config)
       end
-      redis_config_hash
     end
 
-    def initialize(rails_env=nil)
-      rails_env ||= Rails.env
-      config_file = File.expand_path('../../../config/resque.yml', __FILE__)
+    def raw_config_hash
+      config_data = fetch_config
 
-      @url = "redis://localhost:6379"
-      if File.exist?(config_file)
-        @url = YAML.load_file(config_file)[rails_env]
+      if config_data
+        config_data.is_a?(String) ? { url: config_data } : config_data.deep_symbolize_keys
+      else
+        { url: DEFAULT_REDIS_URL }
       end
     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__)
+    end
   end
 end
diff --git a/lib/gitlab/request_profiler/middleware.rb b/lib/gitlab/request_profiler/middleware.rb
index 4e787dc0656c6888f9798a8a1ef17faabf321909..786e1d49f5e048da7fa2a79df2abdf3f36d314aa 100644
--- a/lib/gitlab/request_profiler/middleware.rb
+++ b/lib/gitlab/request_profiler/middleware.rb
@@ -1,5 +1,5 @@
 require 'ruby-prof'
-require 'gitlab/request_profiler'
+require_dependency 'gitlab/request_profiler'
 
 module Gitlab
   module RequestProfiler
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/template/base_template.rb b/lib/gitlab/template/base_template.rb
index 760ff3e614a699b6055fb7573add67df79fca1e5..7ebec8e2cff245ee8e12b6512225243d6e3a246b 100644
--- a/lib/gitlab/template/base_template.rb
+++ b/lib/gitlab/template/base_template.rb
@@ -1,8 +1,9 @@
 module Gitlab
   module Template
     class BaseTemplate
-      def initialize(path)
+      def initialize(path, project = nil)
         @path = path
+        @finder = self.class.finder(project)
       end
 
       def name
@@ -10,23 +11,32 @@ module Gitlab
       end
 
       def content
-        File.read(@path)
+        @finder.read(@path)
+      end
+
+      def to_json
+        { name: name, content: content }
       end
 
       class << self
-        def all
-          self.categories.keys.flat_map { |cat| by_category(cat) }
+        def all(project = nil)
+          if categories.any?
+            categories.keys.flat_map { |cat| by_category(cat, project) }
+          else
+            by_category("", project)
+          end
         end
 
-        def find(key)
-          file_name = "#{key}#{self.extension}"
-
-          directory = select_directory(file_name)
-          directory ? new(File.join(category_directory(directory), file_name)) : nil
+        def find(key, project = nil)
+          path = self.finder(project).find(key)
+          path.present? ? new(path, project) : nil
         end
 
+        # Set categories as sub directories
+        # Example: { "category_name_1" => "directory_path_1", "category_name_2" => "directory_name_2" }
+        # Default is no category with all files in base dir of each class
         def categories
-          raise NotImplementedError
+          {}
         end
 
         def extension
@@ -37,29 +47,40 @@ module Gitlab
           raise NotImplementedError
         end
 
-        def by_category(category)
-          templates_for_directory(category_directory(category))
+        # Defines which strategy will be used to get templates files
+        # RepoTemplateFinder - Finds templates on project repository, templates are filtered perproject
+        # GlobalTemplateFinder - Finds templates on gitlab installation source, templates can be used in all projects
+        def finder(project = nil)
+          raise NotImplementedError
         end
 
-        def category_directory(category)
-          File.join(base_dir, categories[category])
+        def by_category(category, project = nil)
+          directory = category_directory(category)
+          files = finder(project).list_files_for(directory)
+
+          files.map { |f| new(f, project) }
         end
 
-        private
+        def category_directory(category)
+          return base_dir unless category.present?
 
-        def select_directory(file_name)
-          categories.keys.find do |category|
-            File.exist?(File.join(category_directory(category), file_name))
-          end
+          File.join(base_dir, categories[category])
         end
 
-        def templates_for_directory(dir)
-          dir << '/' unless dir.end_with?('/')
-          Dir.glob(File.join(dir, "*#{self.extension}")).select { |f| f =~ filter_regex }.map { |f| new(f) }
-        end
+        # If template is organized by category it returns { category_name: [{ name: template_name }, { name: template2_name }] }
+        # If no category is present returns [{ name: template_name }, { name: template2_name}]
+        def dropdown_names(project = nil)
+          return [] if project && !project.repository.exists?
 
-        def filter_regex
-          @filter_reges ||= /#{Regexp.escape(extension)}\z/
+          if categories.any?
+            categories.keys.map do |category|
+              files = self.by_category(category, project)
+              [category, files.map { |t| { name: t.name } }]
+            end.to_h
+          else
+            files = self.all(project)
+            files.map { |t| { name: t.name } }
+          end
         end
       end
     end
diff --git a/lib/gitlab/template/finders/base_template_finder.rb b/lib/gitlab/template/finders/base_template_finder.rb
new file mode 100644
index 0000000000000000000000000000000000000000..473b05257c6256cdeeebdf40aeeeaadb1f4320a7
--- /dev/null
+++ b/lib/gitlab/template/finders/base_template_finder.rb
@@ -0,0 +1,35 @@
+module Gitlab
+  module Template
+    module Finders
+      class BaseTemplateFinder
+        def initialize(base_dir)
+          @base_dir = base_dir
+        end
+
+        def list_files_for
+          raise NotImplementedError
+        end
+
+        def read
+          raise NotImplementedError
+        end
+
+        def find
+          raise NotImplementedError
+        end
+
+        def category_directory(category)
+          return @base_dir unless category.present?
+
+          @base_dir + @categories[category]
+        end
+
+        class << self
+          def filter_regex(extension)
+            /#{Regexp.escape(extension)}\z/
+          end
+        end
+      end
+    end
+  end
+end
diff --git a/lib/gitlab/template/finders/global_template_finder.rb b/lib/gitlab/template/finders/global_template_finder.rb
new file mode 100644
index 0000000000000000000000000000000000000000..831da45191f80dc1f4ebd1b05db48421f034551c
--- /dev/null
+++ b/lib/gitlab/template/finders/global_template_finder.rb
@@ -0,0 +1,38 @@
+# Searches and reads file present on Gitlab installation directory
+module Gitlab
+  module Template
+    module Finders
+      class GlobalTemplateFinder < BaseTemplateFinder
+        def initialize(base_dir, extension, categories = {})
+          @categories = categories
+          @extension  = extension
+          super(base_dir)
+        end
+
+        def read(path)
+          File.read(path)
+        end
+
+        def find(key)
+          file_name = "#{key}#{@extension}"
+
+          directory = select_directory(file_name)
+          directory ? File.join(category_directory(directory), file_name) : nil
+        end
+
+        def list_files_for(dir)
+          dir << '/' unless dir.end_with?('/')
+          Dir.glob(File.join(dir, "*#{@extension}")).select { |f| f =~ self.class.filter_regex(@extension) }
+        end
+
+        private
+
+        def select_directory(file_name)
+          @categories.keys.find do |category|
+            File.exist?(File.join(category_directory(category), file_name))
+          end
+        end
+      end
+    end
+  end
+end
diff --git a/lib/gitlab/template/finders/repo_template_finder.rb b/lib/gitlab/template/finders/repo_template_finder.rb
new file mode 100644
index 0000000000000000000000000000000000000000..22c39436cb2465dc290a0b85d6717f31b92eea26
--- /dev/null
+++ b/lib/gitlab/template/finders/repo_template_finder.rb
@@ -0,0 +1,59 @@
+# Searches and reads files present on each Gitlab project repository
+module Gitlab
+  module Template
+    module Finders
+      class RepoTemplateFinder < BaseTemplateFinder
+        # Raised when file is not found
+        class FileNotFoundError < StandardError; end
+
+        def initialize(project, base_dir, extension, categories = {})
+          @categories     = categories
+          @extension      = extension
+          @repository     = project.repository
+          @commit         = @repository.head_commit if @repository.exists?
+
+          super(base_dir)
+        end
+
+        def read(path)
+          blob = @repository.blob_at(@commit.id, path) if @commit
+          raise FileNotFoundError if blob.nil?
+          blob.data
+        end
+
+        def find(key)
+          file_name = "#{key}#{@extension}"
+          directory = select_directory(file_name)
+          raise FileNotFoundError if directory.nil?
+
+          category_directory(directory) + file_name
+        end
+
+        def list_files_for(dir)
+          return [] unless @commit
+
+          dir << '/' unless dir.end_with?('/')
+
+          entries = @repository.tree(:head, dir).entries
+
+          names = entries.map(&:name)
+          names.select { |f| f =~ self.class.filter_regex(@extension) }
+        end
+
+        private
+
+        def select_directory(file_name)
+          return [] unless @commit
+
+          # Insert root as directory
+          directories = ["", @categories.keys]
+
+          directories.find do |category|
+            path = category_directory(category) + file_name
+            @repository.blob_at(@commit.id, path)
+          end
+        end
+      end
+    end
+  end
+end
diff --git a/lib/gitlab/template/gitignore.rb b/lib/gitlab/template/gitignore_template.rb
similarity index 63%
rename from lib/gitlab/template/gitignore.rb
rename to lib/gitlab/template/gitignore_template.rb
index 964fbfd4de330ef82019a615d67be438dcb2eedf..8d2a9d2305ca7f24a427cae8ff677761a29efe96 100644
--- a/lib/gitlab/template/gitignore.rb
+++ b/lib/gitlab/template/gitignore_template.rb
@@ -1,6 +1,6 @@
 module Gitlab
   module Template
-    class Gitignore < BaseTemplate
+    class GitignoreTemplate < BaseTemplate
       class << self
         def extension
           '.gitignore'
@@ -16,6 +16,10 @@ module Gitlab
         def base_dir
           Rails.root.join('vendor/gitignore')
         end
+
+        def finder(project = nil)
+          Gitlab::Template::Finders::GlobalTemplateFinder.new(self.base_dir, self.extension, self.categories)
+        end
       end
     end
   end
diff --git a/lib/gitlab/template/gitlab_ci_yml.rb b/lib/gitlab/template/gitlab_ci_yml_template.rb
similarity index 72%
rename from lib/gitlab/template/gitlab_ci_yml.rb
rename to lib/gitlab/template/gitlab_ci_yml_template.rb
index 7f480fe33c0f51aeb653313517c2829035f542bd..8d1a1ed54c9db399cc4c64f7a608baf9bf1ab36a 100644
--- a/lib/gitlab/template/gitlab_ci_yml.rb
+++ b/lib/gitlab/template/gitlab_ci_yml_template.rb
@@ -1,6 +1,6 @@
 module Gitlab
   module Template
-    class GitlabCiYml < BaseTemplate
+    class GitlabCiYmlTemplate < BaseTemplate
       def content
         explanation = "# This file is a template, and might need editing before it works on your project."
         [explanation, super].join("\n")
@@ -21,6 +21,10 @@ module Gitlab
         def base_dir
           Rails.root.join('vendor/gitlab-ci-yml')
         end
+
+        def finder(project = nil)
+          Gitlab::Template::Finders::GlobalTemplateFinder.new(self.base_dir, self.extension, self.categories)
+        end
       end
     end
   end
diff --git a/lib/gitlab/template/issue_template.rb b/lib/gitlab/template/issue_template.rb
new file mode 100644
index 0000000000000000000000000000000000000000..c6fa8d3eafcc913003b17f7dad0e60fdaf1b6d16
--- /dev/null
+++ b/lib/gitlab/template/issue_template.rb
@@ -0,0 +1,19 @@
+module Gitlab
+  module Template
+    class IssueTemplate < BaseTemplate
+      class << self
+        def extension
+          '.md'
+        end
+
+        def base_dir
+          '.gitlab/issue_templates/'
+        end
+
+        def finder(project)
+          Gitlab::Template::Finders::RepoTemplateFinder.new(project, self.base_dir, self.extension, self.categories)
+        end
+      end
+    end
+  end
+end
diff --git a/lib/gitlab/template/merge_request_template.rb b/lib/gitlab/template/merge_request_template.rb
new file mode 100644
index 0000000000000000000000000000000000000000..f826c02f3b53ad9f07e20277929a8d8dca9c0391
--- /dev/null
+++ b/lib/gitlab/template/merge_request_template.rb
@@ -0,0 +1,19 @@
+module Gitlab
+  module Template
+    class MergeRequestTemplate < BaseTemplate
+      class << self
+        def extension
+          '.md'
+        end
+
+        def base_dir
+          '.gitlab/merge_request_templates/'
+        end
+
+        def finder(project)
+          Gitlab::Template::Finders::RepoTemplateFinder.new(project, self.base_dir, self.extension, self.categories)
+        end
+      end
+    end
+  end
+end
diff --git a/lib/gitlab/user_access.rb b/lib/gitlab/user_access.rb
index 3a69027368fdbd2c32c08f905381e6f9ee31e4e2..9858d2e7d83b64404a5a8de49a7b74fab0a54821 100644
--- a/lib/gitlab/user_access.rb
+++ b/lib/gitlab/user_access.rb
@@ -30,7 +30,9 @@ module Gitlab
       return false unless user
 
       if project.protected_branch?(ref)
-        access_levels = project.protected_branches.matching(ref).map(&:push_access_level)
+        return true if project.empty_repo? && project.user_can_push_to_empty_repo?(user)
+
+        access_levels = project.protected_branches.matching(ref).map(&:push_access_levels).flatten
         access_levels.any? { |access_level| access_level.check_access(user) }
       else
         user.can?(:push_code, project)
@@ -41,7 +43,7 @@ module Gitlab
       return false unless user
 
       if project.protected_branch?(ref)
-        access_levels = project.protected_branches.matching(ref).map(&:merge_access_level)
+        access_levels = project.protected_branches.matching(ref).map(&:merge_access_levels).flatten
         access_levels.any? { |access_level| access_level.check_access(user) }
       else
         user.can?(:push_code, project)
diff --git a/lib/gitlab/utils.rb b/lib/gitlab/utils.rb
index d13fe0ef8a9df2c8ac823caa97d0e5d363ae0f9d..e59ead5d76c6c45cdb91a4c1719073b157d1a9c5 100644
--- a/lib/gitlab/utils.rb
+++ b/lib/gitlab/utils.rb
@@ -7,7 +7,7 @@ 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)
diff --git a/lib/support/nginx/gitlab b/lib/support/nginx/gitlab
index 4a4892a2e07ac5a93f4dfcf0ae85614b9a52c3a8..d521de28e8a293a943f5bfa57f54e1cf6ed2f9f0 100644
--- a/lib/support/nginx/gitlab
+++ b/lib/support/nginx/gitlab
@@ -49,12 +49,7 @@ server {
 
     proxy_http_version 1.1;
 
-    ## By overwriting Host and clearing X-Forwarded-Host we ensure that
-    ## internal HTTP redirects generated by GitLab always send users to
-    ## YOUR_SERVER_FQDN.
-    proxy_set_header    Host                YOUR_SERVER_FQDN;
-    proxy_set_header    X-Forwarded-Host    "";
-
+    proxy_set_header    Host                $http_host;
     proxy_set_header    X-Real-IP           $remote_addr;
     proxy_set_header    X-Forwarded-For     $proxy_add_x_forwarded_for;
     proxy_set_header    X-Forwarded-Proto   $scheme;
diff --git a/lib/support/nginx/gitlab-ssl b/lib/support/nginx/gitlab-ssl
index 0b93d7f292f12b6f6303dd3ab06bc6083d9e3af4..bf014b56cf68062d3da68c1416aea7e7d09c83de 100644
--- a/lib/support/nginx/gitlab-ssl
+++ b/lib/support/nginx/gitlab-ssl
@@ -93,12 +93,7 @@ server {
 
     proxy_http_version 1.1;
 
-    ## By overwriting Host and clearing X-Forwarded-Host we ensure that
-    ## internal HTTP redirects generated by GitLab always send users to
-    ## YOUR_SERVER_FQDN.
-    proxy_set_header    Host                YOUR_SERVER_FQDN;
-    proxy_set_header    X-Forwarded-Host    "";
-
+    proxy_set_header    Host                $http_host;
     proxy_set_header    X-Real-IP           $remote_addr;
     proxy_set_header    X-Forwarded-Ssl     on;
     proxy_set_header    X-Forwarded-For     $proxy_add_x_forwarded_for;
diff --git a/lib/tasks/downtime_check.rake b/lib/tasks/downtime_check.rake
index 30a2e9be5ce7415bf3b888e6461dcd5bfec374a7..afe5d42910c281140719c5f9809f003a8f5c56a6 100644
--- a/lib/tasks/downtime_check.rake
+++ b/lib/tasks/downtime_check.rake
@@ -1,26 +1,12 @@
 desc 'Checks if migrations in a branch require downtime'
 task downtime_check: :environment do
-  # First we'll want to make sure we're comparing with the right upstream
-  # repository/branch.
-  current_branch = `git rev-parse --abbrev-ref HEAD`.strip
-
-  # Either the developer ran this task directly on the master branch, or they're
-  # making changes directly on the master branch.
-  if current_branch == 'master'
-    if defined?(Gitlab::License)
-      repo = 'gitlab-ee'
-    else
-      repo = 'gitlab-ce'
-    end
-
-    `git fetch https://gitlab.com/gitlab-org/#{repo}.git --depth 1`
-
-    compare_with = 'FETCH_HEAD'
-  # The developer is working on a different branch, in this case we can just
-  # compare with the master branch.
+  if defined?(Gitlab::License)
+    repo = 'gitlab-ee'
   else
-    compare_with = 'master'
+    repo = 'gitlab-ce'
   end
 
-  Rake::Task['gitlab:db:downtime_check'].invoke(compare_with)
+  `git fetch https://gitlab.com/gitlab-org/#{repo}.git --depth 1`
+
+  Rake::Task['gitlab:db:downtime_check'].invoke('FETCH_HEAD')
 end
diff --git a/lib/tasks/gitlab/bulk_add_permission.rake b/lib/tasks/gitlab/bulk_add_permission.rake
index 5dbf7d61e0662387a6c1ef14929eaf544bac3e40..83dd870fa31334871b22ae6daa3b253c259c5f5d 100644
--- a/lib/tasks/gitlab/bulk_add_permission.rake
+++ b/lib/tasks/gitlab/bulk_add_permission.rake
@@ -4,13 +4,13 @@ namespace :gitlab do
     task all_users_to_all_projects: :environment  do |t, args|
       user_ids = User.where(admin: false).pluck(:id)
       admin_ids = User.where(admin: true).pluck(:id)
-      projects_ids = Project.pluck(:id)
+      project_ids = Project.pluck(:id)
 
-      puts "Importing #{user_ids.size} users into #{projects_ids.size} projects"
-      ProjectMember.add_users_into_projects(projects_ids, user_ids, ProjectMember::DEVELOPER)
+      puts "Importing #{user_ids.size} users into #{project_ids.size} projects"
+      ProjectMember.add_users_to_projects(project_ids, user_ids, ProjectMember::DEVELOPER)
 
-      puts "Importing #{admin_ids.size} admins into #{projects_ids.size} projects"
-      ProjectMember.add_users_into_projects(projects_ids, admin_ids, ProjectMember::MASTER)
+      puts "Importing #{admin_ids.size} admins into #{project_ids.size} projects"
+      ProjectMember.add_users_to_projects(project_ids, admin_ids, ProjectMember::MASTER)
     end
 
     desc "GitLab | Add a specific user to all projects (as a developer)"
@@ -18,7 +18,7 @@ namespace :gitlab do
       user = User.find_by(email: args.email)
       project_ids = Project.pluck(:id)
       puts "Importing #{user.email} users into #{project_ids.size} projects"
-      ProjectMember.add_users_into_projects(project_ids, Array.wrap(user.id), ProjectMember::DEVELOPER)
+      ProjectMember.add_users_to_projects(project_ids, Array.wrap(user.id), ProjectMember::DEVELOPER)
     end
 
     desc "GitLab | Add all users to all groups (admin users are added as owners)"
diff --git a/lib/tasks/gitlab/check.rake b/lib/tasks/gitlab/check.rake
index 60f4636e7379bedfec3f859502ce9b169c3de3ba..5f4a6bbfa353271840c7c3f60beae2bcff47b75c 100644
--- a/lib/tasks/gitlab/check.rake
+++ b/lib/tasks/gitlab/check.rake
@@ -46,7 +46,7 @@ namespace :gitlab do
       }
 
       correct_options = options.map do |name, value|
-        run(%W(#{Gitlab.config.git.bin_path} config --global --get #{name})).try(:squish) == value
+        run_command(%W(#{Gitlab.config.git.bin_path} config --global --get #{name})).try(:squish) == value
       end
 
       if correct_options.all?
@@ -64,7 +64,7 @@ namespace :gitlab do
           for_more_information(
             see_installation_guide_section "GitLab"
           )
-       end
+        end
       end
     end
 
@@ -73,7 +73,7 @@ namespace :gitlab do
 
       database_config_file = Rails.root.join("config", "database.yml")
 
-      if File.exists?(database_config_file)
+      if File.exist?(database_config_file)
         puts "yes".color(:green)
       else
         puts "no".color(:red)
@@ -94,7 +94,7 @@ namespace :gitlab do
 
       gitlab_config_file = Rails.root.join("config", "gitlab.yml")
 
-      if File.exists?(gitlab_config_file)
+      if File.exist?(gitlab_config_file)
         puts "yes".color(:green)
       else
         puts "no".color(:red)
@@ -113,7 +113,7 @@ namespace :gitlab do
       print "GitLab config outdated? ... "
 
       gitlab_config_file = Rails.root.join("config", "gitlab.yml")
-      unless File.exists?(gitlab_config_file)
+      unless File.exist?(gitlab_config_file)
         puts "can't check because of previous errors".color(:magenta)
       end
 
@@ -144,7 +144,7 @@ namespace :gitlab do
 
       script_path = "/etc/init.d/gitlab"
 
-      if File.exists?(script_path)
+      if File.exist?(script_path)
         puts "yes".color(:green)
       else
         puts "no".color(:red)
@@ -169,7 +169,7 @@ namespace :gitlab do
       recipe_path = Rails.root.join("lib/support/init.d/", "gitlab")
       script_path = "/etc/init.d/gitlab"
 
-      unless File.exists?(script_path)
+      unless File.exist?(script_path)
         puts "can't check because of previous errors".color(:magenta)
         return
       end
@@ -316,7 +316,7 @@ namespace :gitlab do
       min_redis_version = "2.8.0"
       print "Redis version >= #{min_redis_version}? ... "
 
-      redis_version = run(%W(redis-cli --version))
+      redis_version = run_command(%W(redis-cli --version))
       redis_version = redis_version.try(:match, /redis-cli (\d+\.\d+\.\d+)/)
       if redis_version &&
           (Gem::Version.new(redis_version[1]) > Gem::Version.new(min_redis_version))
@@ -361,7 +361,7 @@ namespace :gitlab do
       Gitlab.config.repositories.storages.each do |name, repo_base_path|
         print "#{name}... "
 
-        if File.exists?(repo_base_path)
+        if File.exist?(repo_base_path)
           puts "yes".color(:green)
         else
           puts "no".color(:red)
@@ -385,7 +385,7 @@ namespace :gitlab do
       Gitlab.config.repositories.storages.each do |name, repo_base_path|
         print "#{name}... "
 
-        unless File.exists?(repo_base_path)
+        unless File.exist?(repo_base_path)
           puts "can't check because of previous errors".color(:magenta)
           return
         end
@@ -408,7 +408,7 @@ namespace :gitlab do
       Gitlab.config.repositories.storages.each do |name, repo_base_path|
         print "#{name}... "
 
-        unless File.exists?(repo_base_path)
+        unless File.exist?(repo_base_path)
           puts "can't check because of previous errors".color(:magenta)
           return
         end
@@ -438,7 +438,7 @@ namespace :gitlab do
       Gitlab.config.repositories.storages.each do |name, repo_base_path|
         print "#{name}... "
 
-        unless File.exists?(repo_base_path)
+        unless File.exist?(repo_base_path)
           puts "can't check because of previous errors".color(:magenta)
           return
         end
@@ -893,7 +893,7 @@ namespace :gitlab do
 
   def check_ruby_version
     required_version = Gitlab::VersionInfo.new(2, 1, 0)
-    current_version = Gitlab::VersionInfo.parse(run(%W(ruby --version)))
+    current_version = Gitlab::VersionInfo.parse(run_command(%W(ruby --version)))
 
     print "Ruby version >= #{required_version} ? ... "
 
@@ -910,7 +910,7 @@ namespace :gitlab do
 
   def check_git_version
     required_version = Gitlab::VersionInfo.new(2, 7, 3)
-    current_version = Gitlab::VersionInfo.parse(run(%W(#{Gitlab.config.git.bin_path} --version)))
+    current_version = Gitlab::VersionInfo.parse(run_command(%W(#{Gitlab.config.git.bin_path} --version)))
 
     puts "Your git bin path is \"#{Gitlab.config.git.bin_path}\""
     print "Git version >= #{required_version} ? ... "
diff --git a/lib/tasks/gitlab/info.rake b/lib/tasks/gitlab/info.rake
index fe43d40e6d23dac528ca1b3dc8f8185a049f7e00..dffea8ed155e635e51abf339c695570931f52b8a 100644
--- a/lib/tasks/gitlab/info.rake
+++ b/lib/tasks/gitlab/info.rake
@@ -8,7 +8,7 @@ namespace :gitlab do
       # check Ruby version
       ruby_version = run_and_match(%W(ruby --version), /[\d\.p]+/).try(:to_s)
       # check Gem version
-      gem_version = run(%W(gem --version))
+      gem_version = run_command(%W(gem --version))
       # check Bundler version
       bunder_version = run_and_match(%W(bundle --version), /[\d\.]+/).try(:to_s)
       # check Bundler version
@@ -17,7 +17,7 @@ namespace :gitlab do
       puts ""
       puts "System information".color(:yellow)
       puts "System:\t\t#{os_name || "unknown".color(:red)}"
-      puts "Current User:\t#{run(%W(whoami))}"
+      puts "Current User:\t#{run_command(%W(whoami))}"
       puts "Using RVM:\t#{rvm_version.present? ? "yes".color(:green) : "no"}"
       puts "RVM Version:\t#{rvm_version}" if rvm_version.present?
       puts "Ruby Version:\t#{ruby_version || "unknown".color(:red)}"
diff --git a/lib/tasks/gitlab/shell.rake b/lib/tasks/gitlab/shell.rake
index c85ebdf8619b0072f1264b211a4119df3c8119d0..bb7eb852f1b8603e7a6aced3ce550edebf86e2ea 100644
--- a/lib/tasks/gitlab/shell.rake
+++ b/lib/tasks/gitlab/shell.rake
@@ -5,7 +5,8 @@ namespace :gitlab do
       warn_user_is_not_gitlab
 
       default_version = Gitlab::Shell.version_required
-      args.with_defaults(tag: 'v' + default_version, repo: "https://gitlab.com/gitlab-org/gitlab-shell.git")
+      default_version_tag = 'v' + default_version
+      args.with_defaults(tag: default_version_tag, repo: "https://gitlab.com/gitlab-org/gitlab-shell.git")
 
       user = Gitlab.config.gitlab.user
       home_dir = Rails.env.test? ? Rails.root.join('tmp/tests') : Gitlab.config.gitlab.user_home
@@ -15,7 +16,12 @@ namespace :gitlab do
       target_dir = Gitlab.config.gitlab_shell.path
 
       # Clone if needed
-      unless File.directory?(target_dir)
+      if File.directory?(target_dir)
+        Dir.chdir(target_dir) do
+          system(*%W(Gitlab.config.git.bin_path} fetch --tags --quiet))
+          system(*%W(Gitlab.config.git.bin_path} checkout --quiet #{default_version_tag}))
+        end
+      else
         system(*%W(#{Gitlab.config.git.bin_path} clone -- #{args.repo} #{target_dir}))
       end
 
@@ -84,7 +90,7 @@ namespace :gitlab do
     task build_missing_projects: :environment do
       Project.find_each(batch_size: 1000) do |project|
         path_to_repo = project.repository.path_to_repo
-        if File.exists?(path_to_repo)
+        if File.exist?(path_to_repo)
           print '-'
         else
           if Gitlab::Shell.new.add_repository(project.repository_storage_path,
diff --git a/lib/tasks/gitlab/task_helpers.rake b/lib/tasks/gitlab/task_helpers.rake
index ab96b1d35932c9802787ef65e6db2f189961993e..74be413423aa9ece819d105ff0d4b09d153ac63a 100644
--- a/lib/tasks/gitlab/task_helpers.rake
+++ b/lib/tasks/gitlab/task_helpers.rake
@@ -23,7 +23,7 @@ namespace :gitlab do
   # It will primarily use lsb_relase to determine the OS.
   # It has fallbacks to Debian, SuSE, OS X and systems running systemd.
   def os_name
-    os_name = run(%W(lsb_release -irs))
+    os_name = run_command(%W(lsb_release -irs))
     os_name ||= if File.readable?('/etc/system-release')
                   File.read('/etc/system-release')
                 end
@@ -34,7 +34,7 @@ namespace :gitlab do
     os_name ||= if File.readable?('/etc/SuSE-release')
                   File.read('/etc/SuSE-release')
                 end
-    os_name ||= if os_x_version = run(%W(sw_vers -productVersion))
+    os_name ||= if os_x_version = run_command(%W(sw_vers -productVersion))
                   "Mac OS X #{os_x_version}"
                 end
     os_name ||= if File.readable?('/etc/os-release')
@@ -62,10 +62,10 @@ namespace :gitlab do
   # Returns nil if nothing matched
   # Returns the MatchData if the pattern matched
   #
-  # see also #run
+  # see also #run_command
   # see also String#match
   def run_and_match(command, regexp)
-    run(command).try(:match, regexp)
+    run_command(command).try(:match, regexp)
   end
 
   # Runs the given command
@@ -74,7 +74,7 @@ namespace :gitlab do
   # Returns the output of the command otherwise
   #
   # see also #run_and_match
-  def run(command)
+  def run_command(command)
     output, _ = Gitlab::Popen.popen(command)
     output
   rescue Errno::ENOENT
@@ -82,7 +82,7 @@ namespace :gitlab do
   end
 
   def uid_for(user_name)
-    run(%W(id -u #{user_name})).chomp.to_i
+    run_command(%W(id -u #{user_name})).chomp.to_i
   end
 
   def gid_for(group_name)
@@ -96,7 +96,7 @@ namespace :gitlab do
   def warn_user_is_not_gitlab
     unless @warned_user_not_gitlab
       gitlab_user = Gitlab.config.gitlab.user
-      current_user = run(%W(whoami)).chomp
+      current_user = run_command(%W(whoami)).chomp
       unless current_user == gitlab_user
         puts " Warning ".color(:black).background(:yellow)
         puts "  You are running as user #{current_user.color(:magenta)}, we hope you know what you are doing."
diff --git a/lib/tasks/gitlab/web_hook.rake b/lib/tasks/gitlab/web_hook.rake
index f467cc0ee29f53419b40f8ebe334fb4abbcc045d..49530e7a37202fcc6144e0c3adbd5cfa94376c93 100644
--- a/lib/tasks/gitlab/web_hook.rake
+++ b/lib/tasks/gitlab/web_hook.rake
@@ -26,10 +26,10 @@ namespace :gitlab do
       namespace_path = ENV['NAMESPACE']
 
       projects = find_projects(namespace_path)
-      projects_ids = projects.pluck(:id)
+      project_ids = projects.pluck(:id)
 
       puts "Removing webhooks with the url '#{web_hook_url}' ... "
-      count = WebHook.where(url: web_hook_url, project_id: projects_ids, type: 'ProjectHook').delete_all
+      count = WebHook.where(url: web_hook_url, project_id: project_ids, type: 'ProjectHook').delete_all
       puts "#{count} webhooks were removed."
     end
 
diff --git a/lib/tasks/spinach.rake b/lib/tasks/spinach.rake
index da255f5464b04f77be7b1cca3d95904f9102b989..8dbfa7751dcf252c4f7541091c5fffce6e38a470 100644
--- a/lib/tasks/spinach.rake
+++ b/lib/tasks/spinach.rake
@@ -34,21 +34,19 @@ task :spinach do
   run_spinach_tests(nil)
 end
 
-def run_command(cmd)
+def run_system_command(cmd)
   system({'RAILS_ENV' => 'test', 'force' => 'yes'}, *cmd)
 end
 
 def run_spinach_command(args)
-  run_command(%w(spinach -r rerun) + args)
+  run_system_command(%w(spinach -r rerun) + args)
 end
 
 def run_spinach_tests(tags)
-  #run_command(%w(rake gitlab:setup)) or raise('gitlab:setup failed!')
-
   success = run_spinach_command(%W(--tags #{tags}))
   3.times do |_|
     break if success
-    break unless File.exists?('tmp/spinach-rerun.txt')
+    break unless File.exist?('tmp/spinach-rerun.txt')
 
     tests = File.foreach('tmp/spinach-rerun.txt').map(&:chomp)
     puts ''
diff --git a/scripts/lint-doc.sh b/scripts/lint-doc.sh
new file mode 100755
index 0000000000000000000000000000000000000000..bc6e4d940611423a83dc9b664099dc11b4c1fa99
--- /dev/null
+++ b/scripts/lint-doc.sh
@@ -0,0 +1,15 @@
+#!/usr/bin/env bash
+
+cd "$(dirname "$0")/.."
+
+# Use long options (e.g. --header instead of -H) for curl examples in documentation.
+grep --perl-regexp --recursive --color=auto 'curl (.+ )?-[^- ].*' doc/
+if [ $? == 0 ]
+then
+  echo '✖ ERROR: Short options should not be used in documentation!' >&2
+  exit 1
+fi
+
+echo "✔ Linting passed"
+exit 0
+
diff --git a/scripts/prepare_build.sh b/scripts/prepare_build.sh
index 7e71a0309014943b24ee986ac9a2785d72fb0a5e..76b2178c79c346281e03582b558bdcc3c535976c 100755
--- a/scripts/prepare_build.sh
+++ b/scripts/prepare_build.sh
@@ -20,10 +20,11 @@ if [ -f /.dockerenv ] || [ -f ./dockerinit ]; then
 
     # Install phantomjs package
     pushd vendor/apt
-    if [ ! -e phantomjs_1.9.8-0jessie_amd64.deb ]; then
-        wget -q https://gitlab.com/axil/phantomjs-debian/raw/master/phantomjs_1.9.8-0jessie_amd64.deb
+    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
-    dpkg -i phantomjs_1.9.8-0jessie_amd64.deb
+    cp "$PHANTOMJS_FILE/bin/phantomjs" "/usr/bin/"
     popd
 
     # Try to install packages
diff --git a/spec/config/mail_room_spec.rb b/spec/config/mail_room_spec.rb
index 6fad7e2b9e7700b493f01f4b87d29c5c62b89d73..c5d3cd70acc87deddf951ec56f26d9cb9cd2f7c8 100644
--- a/spec/config/mail_room_spec.rb
+++ b/spec/config/mail_room_spec.rb
@@ -1,53 +1,48 @@
-require "spec_helper"
+require 'spec_helper'
 
-describe "mail_room.yml" do
-  let(:config_path)   { "config/mail_room.yml" }
+describe 'mail_room.yml' do
+  let(:config_path)   { 'config/mail_room.yml' }
   let(:configuration) { YAML.load(ERB.new(File.read(config_path)).result) }
 
-  context "when incoming email is disabled" do
+  context 'when incoming email is disabled' do
     before do
-      ENV["MAIL_ROOM_GITLAB_CONFIG_FILE"] = Rails.root.join("spec/fixtures/mail_room_disabled.yml").to_s
+      ENV['MAIL_ROOM_GITLAB_CONFIG_FILE'] = Rails.root.join('spec/fixtures/mail_room_disabled.yml').to_s
+      Gitlab::MailRoom.reset_config!
     end
 
     after do
-      ENV["MAIL_ROOM_GITLAB_CONFIG_FILE"] = nil
+      ENV['MAIL_ROOM_GITLAB_CONFIG_FILE'] = nil
     end
 
-    it "contains no configuration" do
+    it 'contains no configuration' do
       expect(configuration[:mailboxes]).to be_nil
     end
   end
 
-  context "when incoming email is enabled" do
+  context 'when incoming email is enabled' do
     before do
-      ENV["MAIL_ROOM_GITLAB_CONFIG_FILE"] = Rails.root.join("spec/fixtures/mail_room_enabled.yml").to_s
+      ENV['MAIL_ROOM_GITLAB_CONFIG_FILE'] = Rails.root.join('spec/fixtures/mail_room_enabled.yml').to_s
+      Gitlab::MailRoom.reset_config!
     end
 
     after do
-      ENV["MAIL_ROOM_GITLAB_CONFIG_FILE"] = nil
+      ENV['MAIL_ROOM_GITLAB_CONFIG_FILE'] = nil
     end
 
-    it "contains the intended configuration" do
+    it 'contains the intended configuration' do
       expect(configuration[:mailboxes].length).to eq(1)
 
       mailbox = configuration[:mailboxes].first
 
-      expect(mailbox[:host]).to eq("imap.gmail.com")
+      expect(mailbox[:host]).to eq('imap.gmail.com')
       expect(mailbox[:port]).to eq(993)
       expect(mailbox[:ssl]).to eq(true)
       expect(mailbox[:start_tls]).to eq(false)
-      expect(mailbox[:email]).to eq("gitlab-incoming@gmail.com")
-      expect(mailbox[:password]).to eq("[REDACTED]")
-      expect(mailbox[:name]).to eq("inbox")
-
-      redis_config_file = Rails.root.join('config', 'resque.yml')
-
-      redis_url =
-        if File.exist?(redis_config_file)
-          YAML.load_file(redis_config_file)[Rails.env]
-        else
-          "redis://localhost:6379"
-        end
+      expect(mailbox[:email]).to eq('gitlab-incoming@gmail.com')
+      expect(mailbox[:password]).to eq('[REDACTED]')
+      expect(mailbox[:name]).to eq('inbox')
+
+      redis_url = Gitlab::Redis.url
 
       expect(mailbox[:delivery_options][:redis_url]).to eq(redis_url)
       expect(mailbox[:arbitration_options][:redis_url]).to eq(redis_url)
diff --git a/spec/controllers/admin/groups_controller_spec.rb b/spec/controllers/admin/groups_controller_spec.rb
new file mode 100644
index 0000000000000000000000000000000000000000..602de72d23f23daafbc85bf6268da7cf97f76d89
--- /dev/null
+++ b/spec/controllers/admin/groups_controller_spec.rb
@@ -0,0 +1,25 @@
+require 'spec_helper'
+
+describe Admin::GroupsController do
+  let(:group) { create(:group) }
+  let(:project) { create(:project, namespace: group) }
+  let(:admin) { create(:admin) }
+
+  before do
+    sign_in(admin)
+  end
+
+  describe 'DELETE #destroy' do
+    it 'schedules a group destroy' do
+      Sidekiq::Testing.fake! do
+        expect { delete :destroy, id: project.group.path }.to change(GroupDestroyWorker.jobs, :size).by(1)
+      end
+    end
+
+    it 'redirects to the admin group path' do
+      delete :destroy, id: project.group.path
+
+      expect(response).to redirect_to(admin_groups_path)
+    end
+  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/spam_logs_controller_spec.rb b/spec/controllers/admin/spam_logs_controller_spec.rb
index 520a4f6f9c594f4a3197a59da40f247390cc52a6..585ca31389dcd57d39d633e2ef90264156fab3cd 100644
--- a/spec/controllers/admin/spam_logs_controller_spec.rb
+++ b/spec/controllers/admin/spam_logs_controller_spec.rb
@@ -34,4 +34,16 @@ describe Admin::SpamLogsController do
       expect { User.find(user.id) }.to raise_error(ActiveRecord::RecordNotFound)
     end
   end
+
+  describe '#mark_as_ham' do
+    before do
+      allow_any_instance_of(AkismetService).to receive(:submit_ham).and_return(true)
+    end
+    it 'submits the log as ham' do
+      post :mark_as_ham, id: first_spam.id
+
+      expect(response).to have_http_status(302)
+      expect(SpamLog.find(first_spam.id).submitted_as_ham).to be_truthy
+    end
+  end
 end
diff --git a/spec/controllers/admin/users_controller_spec.rb b/spec/controllers/admin/users_controller_spec.rb
index ab9aa65f7b98aa88b14bc1213917831815d1b0f6..33fe3c73822673c9503595d48124e88036647afc 100644
--- a/spec/controllers/admin/users_controller_spec.rb
+++ b/spec/controllers/admin/users_controller_spec.rb
@@ -39,7 +39,7 @@ describe Admin::UsersController do
         user.ldap_block
       end
 
-      it 'will not unblock user' do
+      it 'does not unblock user' do
         put :unblock, id: user.username
         user.reload
         expect(user.blocked?).to be_truthy
diff --git a/spec/controllers/application_controller_spec.rb b/spec/controllers/application_controller_spec.rb
index 8bd210cbc3dd4a5a4c095d681fd8f5070737e739..98e912f000cffea9e041a8695f9fc12f20984b38 100644
--- a/spec/controllers/application_controller_spec.rb
+++ b/spec/controllers/application_controller_spec.rb
@@ -5,7 +5,7 @@ describe ApplicationController do
     let(:user) { create(:user) }
     let(:controller) { ApplicationController.new }
 
-    it 'should redirect if the user is over their password expiry' do
+    it 'redirects if the user is over their password expiry' do
       user.password_expires_at = Time.new(2002)
       expect(user.ldap_user?).to be_falsey
       allow(controller).to receive(:current_user).and_return(user)
@@ -14,7 +14,7 @@ describe ApplicationController do
       controller.send(:check_password_expiration)
     end
 
-    it 'should not redirect if the user is under their password expiry' do
+    it 'does not redirect if the user is under their password expiry' do
       user.password_expires_at = Time.now + 20010101
       expect(user.ldap_user?).to be_falsey
       allow(controller).to receive(:current_user).and_return(user)
@@ -22,7 +22,7 @@ describe ApplicationController do
       controller.send(:check_password_expiration)
     end
 
-    it 'should not redirect if the user is over their password expiry but they are an ldap user' do
+    it 'does not redirect if the user is over their password expiry but they are an ldap user' do
       user.password_expires_at = Time.new(2002)
       allow(user).to receive(:ldap_user?).and_return(true)
       allow(controller).to receive(:current_user).and_return(user)
diff --git a/spec/controllers/autocomplete_controller_spec.rb b/spec/controllers/autocomplete_controller_spec.rb
index ed0b7f9e240d07f8f6f4e446e12cb641cb0dd1ec..a121cb2fc97d3e58960e660073c595ab9fe1f741 100644
--- a/spec/controllers/autocomplete_controller_spec.rb
+++ b/spec/controllers/autocomplete_controller_spec.rb
@@ -2,178 +2,312 @@ require 'spec_helper'
 
 describe AutocompleteController do
   let!(:project) { create(:project) }
-  let!(:user)    { create(:user) }
-  let!(:user2)   { create(:user) }
-  let!(:non_member)   { create(:user) }
+  let!(:user) { create(:user) }
 
-  context 'project members' do
-    before do
-      sign_in(user)
-      project.team << [user, :master]
-    end
+  context 'users and members' do
+    let!(:user2) { create(:user) }
+    let!(:non_member) { create(:user) }
 
-    describe 'GET #users with project ID' do
+    context 'project members' do
       before do
-        get(:users, project_id: project.id)
+        sign_in(user)
+        project.team << [user, :master]
       end
 
-      let(:body) { JSON.parse(response.body) }
+      describe 'GET #users with project ID' do
+        before do
+          get(:users, project_id: project.id)
+        end
 
-      it { expect(body).to be_kind_of(Array) }
-      it { expect(body.size).to eq 1 }
-      it { expect(body.map { |u| u["username"] }).to include(user.username) }
+        let(:body) { JSON.parse(response.body) }
+
+        it { expect(body).to be_kind_of(Array) }
+        it { expect(body.size).to eq 1 }
+        it { expect(body.map { |u| u["username"] }).to include(user.username) }
+      end
+
+      describe 'GET #users with unknown project' do
+        before do
+          get(:users, project_id: 'unknown')
+        end
+
+        it { expect(response).to have_http_status(404) }
+      end
     end
 
-    describe 'GET #users with unknown project' do
+    context 'group members' do
+      let(:group) { create(:group) }
+
       before do
-        get(:users, project_id: 'unknown')
+        sign_in(user)
+        group.add_owner(user)
       end
 
-      it { expect(response).to have_http_status(404) }
-    end
-  end
+      let(:body) { JSON.parse(response.body) }
 
-  context 'group members' do
-    let(:group) { create(:group) }
+      describe 'GET #users with group ID' do
+        before do
+          get(:users, group_id: group.id)
+        end
 
-    before do
-      sign_in(user)
-      group.add_owner(user)
+        it { expect(body).to be_kind_of(Array) }
+        it { expect(body.size).to eq 1 }
+        it { expect(body.first["username"]).to eq user.username }
+      end
+
+      describe 'GET #users with unknown group ID' do
+        before do
+          get(:users, group_id: 'unknown')
+        end
+
+        it { expect(response).to have_http_status(404) }
+      end
     end
 
-    let(:body) { JSON.parse(response.body) }
+    context 'non-member login for public project' do
+      let!(:project) { create(:project, :public) }
 
-    describe 'GET #users with group ID' do
       before do
-        get(:users, group_id: group.id)
+        sign_in(non_member)
+        project.team << [user, :master]
       end
 
-      it { expect(body).to be_kind_of(Array) }
-      it { expect(body.size).to eq 1 }
-      it { expect(body.first["username"]).to eq user.username }
+      let(:body) { JSON.parse(response.body) }
+
+      describe 'GET #users with project ID' do
+        before do
+          get(:users, project_id: project.id, current_user: true)
+        end
+
+        it { expect(body).to be_kind_of(Array) }
+        it { expect(body.size).to eq 2 }
+        it { expect(body.map { |u| u['username'] }).to match_array([user.username, non_member.username]) }
+      end
     end
 
-    describe 'GET #users with unknown group ID' do
+    context 'all users' do
       before do
-        get(:users, group_id: 'unknown')
+        sign_in(user)
+        get(:users)
       end
 
-      it { expect(response).to have_http_status(404) }
+      let(:body) { JSON.parse(response.body) }
+
+      it { expect(body).to be_kind_of(Array) }
+      it { expect(body.size).to eq User.count }
     end
-  end
 
-  context 'non-member login for public project' do
-    let!(:project) { create(:project, :public) }
+    context 'unauthenticated user' do
+      let(:public_project) { create(:project, :public) }
+      let(:body) { JSON.parse(response.body) }
 
-    before do
-      sign_in(non_member)
-      project.team << [user, :master]
-    end
+      describe 'GET #users with public project' do
+        before do
+          public_project.team << [user, :guest]
+          get(:users, project_id: public_project.id)
+        end
+
+        it { expect(body).to be_kind_of(Array) }
+        it { expect(body.size).to eq 1 }
+      end
 
-    let(:body) { JSON.parse(response.body) }
+      describe 'GET #users with project' do
+        before do
+          get(:users, project_id: project.id)
+        end
 
-    describe 'GET #users with project ID' do
-      before do
-        get(:users, project_id: project.id, current_user: true)
+        it { expect(response).to have_http_status(404) }
       end
 
-      it { expect(body).to be_kind_of(Array) }
-      it { expect(body.size).to eq 2 }
-      it { expect(body.map { |u| u['username'] }).to match_array([user.username, non_member.username]) }
-    end
-  end
+      describe 'GET #users with unknown project' do
+        before do
+          get(:users, project_id: 'unknown')
+        end
 
-  context 'all users' do
-    before do
-      sign_in(user)
-      get(:users)
-    end
+        it { expect(response).to have_http_status(404) }
+      end
 
-    let(:body) { JSON.parse(response.body) }
+      describe 'GET #users with inaccessible group' do
+        before do
+          project.team << [user, :guest]
+          get(:users, group_id: user.namespace.id)
+        end
 
-    it { expect(body).to be_kind_of(Array) }
-    it { expect(body.size).to eq User.count }
-  end
+        it { expect(response).to have_http_status(404) }
+      end
+
+      describe 'GET #users with no project' do
+        before do
+          get(:users)
+        end
 
-  context 'unauthenticated user' do
-    let(:public_project) { create(:project, :public) }
-    let(:body) { JSON.parse(response.body) }
+        it { expect(body).to be_kind_of(Array) }
+        it { expect(body.size).to eq 0 }
+      end
+    end
 
-    describe 'GET #users with public project' do
+    context 'author of issuable included' do
       before do
-        public_project.team << [user, :guest]
-        get(:users, project_id: public_project.id)
+        sign_in(user)
       end
 
-      it { expect(body).to be_kind_of(Array) }
-      it { expect(body.size).to eq 1 }
+      let(:body) { JSON.parse(response.body) }
+
+      it 'includes the author' do
+        get(:users, author_id: non_member.id)
+
+        expect(body.first["username"]).to eq non_member.username
+      end
+
+      it 'rejects non existent user ids' do
+        get(:users, author_id: 99999)
+
+        expect(body.collect { |u| u['id'] }).not_to include(99999)
+      end
     end
 
-    describe 'GET #users with project' do
-      before do
-        get(:users, project_id: project.id)
+    context 'skip_users parameter included' do
+      before { sign_in(user) }
+
+      it 'skips the user IDs passed' do
+        get(:users, skip_users: [user, user2].map(&:id))
+
+        other_user_ids    = [non_member, project.owner, project.creator].map(&:id)
+        response_user_ids = JSON.parse(response.body).map { |user| user['id'] }
+
+        expect(response_user_ids).to contain_exactly(*other_user_ids)
       end
+    end
+  end
+
+  context 'projects' do
+    let(:authorized_project) { create(:project) }
+    let(:authorized_search_project) { create(:project, name: 'rugged') }
 
-      it { expect(response).to have_http_status(404) }
+    before do
+      sign_in(user)
+      project.team << [user, :master]
     end
 
-    describe 'GET #users with unknown project' do
+    context 'authorized projects' do
       before do
-        get(:users, project_id: 'unknown')
+        authorized_project.team << [user, :master]
       end
 
-      it { expect(response).to have_http_status(404) }
+      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 2
+
+          expect(body.first['id']).to eq 0
+          expect(body.first['name_with_namespace']).to eq 'No project'
+
+          expect(body.last['id']).to eq authorized_project.id
+          expect(body.last['name_with_namespace']).to eq authorized_project.name_with_namespace
+        end
+      end
     end
 
-    describe 'GET #users with inaccessible group' do
+    context 'authorized projects and search' do
       before do
-        project.team << [user, :guest]
-        get(:users, group_id: user.namespace.id)
+        authorized_project.team << [user, :master]
+        authorized_search_project.team << [user, :master]
       end
 
-      it { expect(response).to have_http_status(404) }
+      describe 'GET #projects with project ID and search' do
+        before do
+          get(:projects, project_id: project.id, search: 'rugged')
+        end
+
+        let(:body) { JSON.parse(response.body) }
+
+        it do
+          expect(body).to be_kind_of(Array)
+          expect(body.size).to eq 2
+
+          expect(body.last['id']).to eq authorized_search_project.id
+          expect(body.last['name_with_namespace']).to eq authorized_search_project.name_with_namespace
+        end
+      end
     end
 
-    describe 'GET #users with no project' do
+    context 'authorized projects apply limit' do
       before do
-        get(:users)
+        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
 
-      it { expect(body).to be_kind_of(Array) }
-      it { expect(body.size).to eq 0 }
-    end
-  end
+      describe 'GET #projects with project ID' do
+        before do
+          get(:projects, project_id: project.id)
+        end
 
-  context 'author of issuable included' do
-    before do
-      sign_in(user)
+        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
 
-    let(:body) { JSON.parse(response.body) }
+    context 'authorized projects with offset' do
+      before do
+        authorized_project2 = create(:project)
+        authorized_project3 = create(:project)
 
-    it 'includes the author' do
-      get(:users, author_id: non_member.id)
+        authorized_project.team << [user, :master]
+        authorized_project2.team << [user, :master]
+        authorized_project3.team << [user, :master]
+      end
 
-      expect(body.first["username"]).to eq non_member.username
-    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
 
-    it 'rejects non existent user ids' do
-      get(:users, author_id: 99999)
+        let(:body) { JSON.parse(response.body) }
 
-      expect(body.collect { |u| u['id'] }).not_to include(99999)
+        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
-  end
 
-  context 'skip_users parameter included' do
-    before { sign_in(user) }
+    context 'authorized projects without admin_issue ability' do
+      before(:each) do
+        authorized_project.team << [user, :guest]
+
+        expect(user.can?(:admin_issue, authorized_project)).to eq(false)
+      end
+
+      describe 'GET #projects with project ID' do
+        before do
+          get(:projects, project_id: project.id)
+        end
 
-    it 'skips the user IDs passed' do
-      get(:users, skip_users: [user, user2].map(&:id))
+        let(:body) { JSON.parse(response.body) }
 
-      other_user_ids = [non_member, project.owner, project.creator].map(&:id)
-      response_user_ids = JSON.parse(response.body).map { |user| user['id'] }
+        it do
+          expect(body).to be_kind_of(Array)
+          expect(body.size).to eq 1 # 'No project'
 
-      expect(response_user_ids).to contain_exactly(*other_user_ids)
+          expect(body.first['id']).to eq 0
+        end
+      end
     end
   end
 end
diff --git a/spec/controllers/groups/avatars_controller_spec.rb b/spec/controllers/groups/avatars_controller_spec.rb
index 91d639218e59e263e378fb9ecca081ec5b23229d..506aeee7d2a34e7918a8e0d8e2ecbeadb764f164 100644
--- a/spec/controllers/groups/avatars_controller_spec.rb
+++ b/spec/controllers/groups/avatars_controller_spec.rb
@@ -9,7 +9,7 @@ describe Groups::AvatarsController do
     sign_in(user)
   end
 
-  it 'destroy should remove avatar from DB' do
+  it 'removes avatar from DB calling destroy' do
     delete :destroy, group_id: group.path
     @group = assigns(:group)
     expect(@group.avatar.present?).to be_falsey
diff --git a/spec/controllers/groups/milestones_controller_spec.rb b/spec/controllers/groups/milestones_controller_spec.rb
index b0793cb1655280f3c8b22cf93f87dd180b1766b2..8c52f615b8ba06076e11ba44d56882bbe39a47ae 100644
--- a/spec/controllers/groups/milestones_controller_spec.rb
+++ b/spec/controllers/groups/milestones_controller_spec.rb
@@ -15,7 +15,7 @@ describe Groups::MilestonesController do
   end
 
   describe "#create" do
-    it "should create group milestone with Chinese title" do
+    it "creates group milestone with Chinese title" do
       post :create,
            group_id: group.id,
            milestone: { project_ids: [project.id, project2.id], title: title }
diff --git a/spec/controllers/groups_controller_spec.rb b/spec/controllers/groups_controller_spec.rb
index cd98fecd0c7fdc8bd3c6b54741daed81bfa5c987..a763e2c5ba852a31fc3c114fbdf81aaadb46bc86 100644
--- a/spec/controllers/groups_controller_spec.rb
+++ b/spec/controllers/groups_controller_spec.rb
@@ -75,4 +75,34 @@ describe GroupsController do
       end
     end
   end
+
+  describe 'DELETE #destroy' do
+    context 'as another user' do
+      it 'returns 404' do
+        sign_in(create(:user))
+
+        delete :destroy, id: group.path
+
+        expect(response.status).to eq(404)
+      end
+    end
+
+    context 'as the group owner' do
+      before do
+        sign_in(user)
+      end
+
+      it 'schedules a group destroy' do
+        Sidekiq::Testing.fake! do
+          expect { delete :destroy, id: group.path }.to change(GroupDestroyWorker.jobs, :size).by(1)
+        end
+      end
+
+      it 'redirects to the root path' do
+        delete :destroy, id: group.path
+
+        expect(response).to redirect_to(root_path)
+      end
+    end
+  end
 end
diff --git a/spec/controllers/profiles/avatars_controller_spec.rb b/spec/controllers/profiles/avatars_controller_spec.rb
index ad5855df0a433eadd70ae0a74db6435a1cb0cc6d..4fa0462ccdfbfdf18b3146f1f7dbc3161255fbe3 100644
--- a/spec/controllers/profiles/avatars_controller_spec.rb
+++ b/spec/controllers/profiles/avatars_controller_spec.rb
@@ -8,7 +8,7 @@ describe Profiles::AvatarsController do
     controller.instance_variable_set(:@user, user)
   end
 
-  it 'destroy should remove avatar from DB' do
+  it 'removes avatar from DB by calling destroy' do
     delete :destroy
     @user = assigns(:user)
     expect(@user.avatar.present?).to be_falsey
diff --git a/spec/controllers/profiles/keys_controller_spec.rb b/spec/controllers/profiles/keys_controller_spec.rb
index 3a82083717ff07833ba8d954bde6cd24f1bcd76f..6bcfae0fc13d912c96ebd76d0562bf4bfad4aae5 100644
--- a/spec/controllers/profiles/keys_controller_spec.rb
+++ b/spec/controllers/profiles/keys_controller_spec.rb
@@ -6,7 +6,7 @@ describe Profiles::KeysController do
   describe '#new' do
     before { sign_in(user) }
 
-    it 'redirect to #index' do
+    it 'redirects to #index' do
       get :new
 
       expect(response).to redirect_to(profile_keys_path)
@@ -15,7 +15,7 @@ describe Profiles::KeysController do
 
   describe "#get_keys" do
     describe "non existant user" do
-      it "should generally not work" do
+      it "does not generally work" do
         get :get_keys, username: 'not-existent'
 
         expect(response).not_to be_success
@@ -23,19 +23,19 @@ describe Profiles::KeysController do
     end
 
     describe "user with no keys" do
-      it "should generally work" do
+      it "does generally work" do
         get :get_keys, username: user.username
 
         expect(response).to be_success
       end
 
-      it "should render all keys separated with a new line" do
+      it "renders all keys separated with a new line" do
         get :get_keys, username: user.username
 
         expect(response.body).to eq("")
       end
 
-      it "should respond with text/plain content type" do
+      it "responds with text/plain content type" do
         get :get_keys, username: user.username
         expect(response.content_type).to eq("text/plain")
       end
@@ -47,13 +47,13 @@ describe Profiles::KeysController do
         user.keys << create(:another_key)
       end
 
-      it "should generally work" do
+      it "does generally work" do
         get :get_keys, username: user.username
 
         expect(response).to be_success
       end
 
-      it "should render all keys separated with a new line" do
+      it "renders all keys separated with a new line" do
         get :get_keys, username: user.username
 
         expect(response.body).not_to eq("")
@@ -65,13 +65,13 @@ describe Profiles::KeysController do
         expect(response.body).to match(/AQDmTillFzNTrrGgwaCKaSj/)
       end
 
-      it "should not render the comment of the key" do
+      it "does not render the comment of the key" do
         get :get_keys, username: user.username
 
         expect(response.body).not_to match(/dummy@gitlab.com/)
       end
 
-      it "should respond with text/plain content type" do
+      it "responds with text/plain content type" do
         get :get_keys, username: user.username
         expect(response.content_type).to eq("text/plain")
       end
diff --git a/spec/controllers/projects/avatars_controller_spec.rb b/spec/controllers/projects/avatars_controller_spec.rb
index 4d724ca9ed0175624b18a7460df5487147493bb1..f5ea097af8b5a22fb67690d93a4150c3da0ce4f2 100644
--- a/spec/controllers/projects/avatars_controller_spec.rb
+++ b/spec/controllers/projects/avatars_controller_spec.rb
@@ -10,7 +10,7 @@ describe Projects::AvatarsController do
     controller.instance_variable_set(:@project, project)
   end
 
-  it 'destroy should remove avatar from DB' do
+  it 'removes avatar from DB by calling destroy' do
     delete :destroy, namespace_id: project.namespace.id, project_id: project.id
     expect(project.avatar.present?).to be_falsey
     expect(project).to be_valid
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..d0ad5e26dbd077725cfc8bf28d546a9483191998
--- /dev/null
+++ b/spec/controllers/projects/boards/issues_controller_spec.rb
@@ -0,0 +1,120 @@
+require 'spec_helper'
+
+describe Projects::Boards::IssuesController do
+  let(:project) { create(:project_with_board) }
+  let(:user)    { create(:user) }
+
+  let(:planning)    { create(:label, project: project, name: 'Planning') }
+  let(:development) { create(:label, project: project, name: 'Development') }
+
+  let!(:list1) { create(:list, board: project.board, label: planning, position: 0) }
+  let!(:list2) { create(:list, board: project.board, label: development, position: 1) }
+
+  before do
+    project.team << [user, :master]
+  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')))
+        create(:labeled_issue, project: project, labels: [planning])
+        create(:labeled_issue, project: project, labels: [development])
+        create(:labeled_issue, project: project, labels: [development], assignee: johndoe)
+
+        list_issues user: user, list_id: 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 list id' do
+      it 'returns a not found 404 response' do
+        list_issues user: user, list_id: 999
+
+        expect(response).to have_http_status(404)
+      end
+    end
+
+    context 'with unauthorized user' do
+      before do
+        allow(Ability.abilities).to receive(:allowed?).with(user, :read_project, project).and_return(true)
+        allow(Ability.abilities).to receive(:allowed?).with(user, :read_issue, project).and_return(false)
+      end
+
+      it 'returns a successful 403 response' do
+        list_issues user: user, list_id: list2
+
+        expect(response).to have_http_status(403)
+      end
+    end
+
+    def list_issues(user:, list_id:)
+      sign_in(user)
+
+      get :index, namespace_id: project.namespace.to_param,
+                  project_id: project.to_param,
+                  list_id: list_id.to_param
+    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, 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, 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, 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 issue id' do
+        move user: user, 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 successful 403 response' do
+        move user: guest, issue: issue, from_list_id: list1.id, to_list_id: list2.id
+
+        expect(response).to have_http_status(403)
+      end
+    end
+
+    def move(user:, issue:, from_list_id:, to_list_id:)
+      sign_in(user)
+
+      patch :update, namespace_id: project.namespace.to_param,
+                     project_id: project.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..9496636e3cc4c58d77d4b09aa4882c33bd1f5f71
--- /dev/null
+++ b/spec/controllers/projects/boards/lists_controller_spec.rb
@@ -0,0 +1,241 @@
+require 'spec_helper'
+
+describe Projects::Boards::ListsController do
+  let(:project) { create(:project_with_board) }
+  let(:board)   { project.board }
+  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
+
+      expect(response).to have_http_status(200)
+      expect(response.content_type).to eq 'application/json'
+    end
+
+    it 'returns a list of board lists' do
+      board = project.create_board
+      create(:backlog_list, board: board)
+      create(:list, board: board)
+      create(:done_list, board: board)
+
+      read_board_list user: user
+
+      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.abilities).to receive(:allowed?).with(user, :read_project, project).and_return(true)
+        allow(Ability.abilities).to receive(:allowed?).with(user, :read_list, project).and_return(false)
+      end
+
+      it 'returns a successful 403 response' do
+        read_board_list user: user
+
+        expect(response).to have_http_status(403)
+      end
+    end
+
+    def read_board_list(user:)
+      sign_in(user)
+
+      get :index, namespace_id: project.namespace.to_param,
+                  project_id: project.to_param,
+                  format: :json
+    end
+  end
+
+  describe 'POST create' do
+    let(:label) { create(:label, project: project, name: 'Development') }
+
+    context 'with valid params' do
+      it 'returns a successful 200 response' do
+        create_board_list user: user, label_id: label.id
+
+        expect(response).to have_http_status(200)
+      end
+
+      it 'returns the created list' do
+        create_board_list user: user, label_id: label.id
+
+        expect(response).to match_response_schema('list')
+      end
+    end
+
+    context 'with invalid params' do
+      it 'returns an error' do
+        create_board_list user: user, label_id: nil
+
+        parsed_response = JSON.parse(response.body)
+
+        expect(parsed_response['label']).to contain_exactly "can't be blank"
+        expect(response).to have_http_status(422)
+      end
+    end
+
+    context 'with unauthorized user' do
+      let(:label) { create(:label, project: project, name: 'Development') }
+
+      it 'returns a successful 403 response' do
+        create_board_list user: guest, label_id: label.id
+
+        expect(response).to have_http_status(403)
+      end
+    end
+
+    def create_board_list(user:, label_id:)
+      sign_in(user)
+
+      post :create, namespace_id: project.namespace.to_param,
+                    project_id: project.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, list: planning, position: 1
+
+        expect(response).to have_http_status(200)
+      end
+
+      it 'moves the list to the desired position' do
+        move user: user, list: planning, position: 1
+
+        expect(planning.reload.position).to eq 1
+      end
+    end
+
+    context 'with invalid position' do
+      it 'returns a unprocessable entity 422 response' do
+        move user: user, 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, list: 999, position: 1
+
+        expect(response).to have_http_status(404)
+      end
+    end
+
+    context 'with unauthorized user' do
+      it 'returns a successful 403 response' do
+        move user: guest, list: planning, position: 6
+
+        expect(response).to have_http_status(403)
+      end
+    end
+
+    def move(user:, list:, position:)
+      sign_in(user)
+
+      patch :update, namespace_id: project.namespace.to_param,
+                     project_id: project.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, list: planning
+
+        expect(response).to have_http_status(200)
+      end
+
+      it 'removes list from board' do
+        expect { remove_board_list user: user, 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, list: 999
+
+        expect(response).to have_http_status(404)
+      end
+    end
+
+    context 'with unauthorized user' do
+      it 'returns a successful 403 response' do
+        remove_board_list user: guest, list: planning
+
+        expect(response).to have_http_status(403)
+      end
+    end
+
+    def remove_board_list(user:, list:)
+      sign_in(user)
+
+      delete :destroy, namespace_id: project.namespace.to_param,
+                       project_id: project.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_board_lists user: user
+
+        expect(response).to have_http_status(200)
+      end
+
+      it 'returns the defaults lists' do
+        generate_default_board_lists user: user
+
+        expect(response).to match_response_schema('lists')
+      end
+    end
+
+    context 'when board lists is not empty' do
+      it 'returns a unprocessable entity 422 response' do
+        create(:list, board: board)
+
+        generate_default_board_lists user: user
+
+        expect(response).to have_http_status(422)
+      end
+    end
+
+    context 'with unauthorized user' do
+      it 'returns a successful 403 response' do
+        generate_default_board_lists user: guest
+
+        expect(response).to have_http_status(403)
+      end
+    end
+
+    def generate_default_board_lists(user:)
+      sign_in(user)
+
+      post :generate, namespace_id: project.namespace.to_param,
+                      project_id: project.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..75a6d39e82c1a84652e750d76dbdfe858e88d99b
--- /dev/null
+++ b/spec/controllers/projects/boards_controller_spec.rb
@@ -0,0 +1,43 @@
+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 show' do
+    it 'creates a new board when project does not have one' do
+      expect { read_board }.to change(Board, :count).by(1)
+    end
+
+    it 'renders HTML template' do
+      read_board
+
+      expect(response).to render_template :show
+      expect(response.content_type).to eq 'text/html'
+    end
+
+    context 'with unauthorized user' do
+      before do
+        allow(Ability.abilities).to receive(:allowed?).with(user, :read_project, project).and_return(true)
+        allow(Ability.abilities).to receive(:allowed?).with(user, :read_board, project).and_return(false)
+      end
+
+      it 'returns a successful 404 response' do
+        read_board
+
+        expect(response).to have_http_status(404)
+      end
+    end
+
+    def read_board(format: :html)
+      get :show, namespace_id: project.namespace.to_param,
+                 project_id: project.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 df902da86f8f765bb7fc32ec2046583ba92034bd..7e440193d7be1e7a463117b5850a1383d201f02e 100644
--- a/spec/controllers/projects/commit_controller_spec.rb
+++ b/spec/controllers/projects/commit_controller_spec.rb
@@ -47,25 +47,25 @@ describe Projects::CommitController do
     end
 
     shared_examples "export as" do |format|
-      it "should generally work" do
+      it "does generally work" do
         go(id: commit.id, format: format)
 
         expect(response).to be_success
       end
 
-      it "should generate it" do
+      it "generates it" do
         expect_any_instance_of(Commit).to receive(:"to_#{format}")
 
         go(id: commit.id, format: format)
       end
 
-      it "should render it" do
+      it "renders it" do
         go(id: commit.id, format: format)
 
         expect(response.body).to eq(commit.send(:"to_#{format}"))
       end
 
-      it "should not escape Html" do
+      it "does not escape Html" do
         allow_any_instance_of(Commit).to receive(:"to_#{format}").
           and_return('HTML entities &<>" ')
 
@@ -83,17 +83,18 @@ describe Projects::CommitController do
       let(:format) { :diff }
 
       it "should really only be a git diff" do
-        go(id: commit.id, format: format)
+        go(id: '66eceea0db202bb39c4e445e8ca28689645366c5', format: format)
 
         expect(response.body).to start_with("diff --git")
       end
 
-      it "should really only be a git diff without whitespace changes" do
+      it "is only be a git diff without whitespace changes" do
         go(id: '66eceea0db202bb39c4e445e8ca28689645366c5', format: format, w: 1)
 
         expect(response.body).to start_with("diff --git")
-        # without whitespace option, there are more than 2 diff_splits
-        diff_splits = assigns(:diffs).first.diff.split("\n")
+
+        # without whitespace option, there are more than 2 diff_splits for other formats
+        diff_splits = assigns(:diffs).diff_files.first.diff.diff.split("\n")
         expect(diff_splits.length).to be <= 2
       end
     end
@@ -102,13 +103,13 @@ describe Projects::CommitController do
       include_examples "export as", :patch
       let(:format) { :patch }
 
-      it "should really be a git email patch" do
+      it "is a git email patch" do
         go(id: commit.id, format: format)
 
         expect(response.body).to start_with("From #{commit.id}")
       end
 
-      it "should contain a git diff" do
+      it "contains a git diff" do
         go(id: commit.id, format: format)
 
         expect(response.body).to match(/^diff --git/)
@@ -146,7 +147,7 @@ describe Projects::CommitController do
 
   describe 'POST revert' do
     context 'when target branch is not provided' do
-      it 'should render the 404 page' do
+      it 'renders the 404 page' do
         post(:revert,
             namespace_id: project.namespace.to_param,
             project_id: project.to_param,
@@ -158,7 +159,7 @@ describe Projects::CommitController do
     end
 
     context 'when the revert was successful' do
-      it 'should redirect to the commits page' do
+      it 'redirects to the commits page' do
         post(:revert,
             namespace_id: project.namespace.to_param,
             project_id: project.to_param,
@@ -179,7 +180,7 @@ describe Projects::CommitController do
             id: commit.id)
       end
 
-      it 'should redirect to the commit page' do
+      it 'redirects to the commit page' do
         # Reverting a commit that has been already reverted.
         post(:revert,
             namespace_id: project.namespace.to_param,
@@ -195,7 +196,7 @@ describe Projects::CommitController do
 
   describe 'POST cherry_pick' do
     context 'when target branch is not provided' do
-      it 'should render the 404 page' do
+      it 'renders the 404 page' do
         post(:cherry_pick,
             namespace_id: project.namespace.to_param,
             project_id: project.to_param,
@@ -207,7 +208,7 @@ describe Projects::CommitController do
     end
 
     context 'when the cherry-pick was successful' do
-      it 'should redirect to the commits page' do
+      it 'redirects to the commits page' do
         post(:cherry_pick,
             namespace_id: project.namespace.to_param,
             project_id: project.to_param,
@@ -228,7 +229,7 @@ describe Projects::CommitController do
             id: master_pickable_commit.id)
       end
 
-      it 'should redirect to the commit page' do
+      it 'redirects to the commit page' do
         # Cherry-picking a commit that has been already cherry-picked.
         post(:cherry_pick,
             namespace_id: project.namespace.to_param,
@@ -266,9 +267,9 @@ describe Projects::CommitController do
           end
 
           it 'only renders the diffs for the path given' do
-            expect(controller).to receive(:render_diff_for_path).and_wrap_original do |meth, diffs, diff_refs, project|
-              expect(diffs.map(&:new_path)).to contain_exactly(existing_path)
-              meth.call(diffs, diff_refs, project)
+            expect(controller).to receive(:render_diff_for_path).and_wrap_original do |meth, diffs|
+              expect(diffs.diff_files.map(&:new_path)).to contain_exactly(existing_path)
+              meth.call(diffs)
             end
 
             diff_for_path(id: commit.id, old_path: existing_path, new_path: existing_path)
diff --git a/spec/controllers/projects/commits_controller_spec.rb b/spec/controllers/projects/commits_controller_spec.rb
index 7d8089c4bc60dc5970e327f480bb077802578b9a..2518a48e336429b268f65d82bdabb3177db3ada1 100644
--- a/spec/controllers/projects/commits_controller_spec.rb
+++ b/spec/controllers/projects/commits_controller_spec.rb
@@ -11,7 +11,7 @@ describe Projects::CommitsController do
 
   describe "GET show" do
     context "as atom feed" do
-      it "should render as atom" do
+      it "renders as atom" do
         get(:show,
             namespace_id: project.namespace.to_param,
             project_id: project.to_param,
diff --git a/spec/controllers/projects/compare_controller_spec.rb b/spec/controllers/projects/compare_controller_spec.rb
index 4058d5e2453cd207464698189f3c8ab73ebd3d79..7a57801c437c0e02ac4db39d356ddd8131ea33ab 100644
--- a/spec/controllers/projects/compare_controller_spec.rb
+++ b/spec/controllers/projects/compare_controller_spec.rb
@@ -11,7 +11,7 @@ describe Projects::CompareController do
     project.team << [user, :master]
   end
 
-  it 'compare should show some diffs' do
+  it 'compare shows some diffs' do
     get(:show,
         namespace_id: project.namespace.to_param,
         project_id: project.to_param,
@@ -19,11 +19,11 @@ describe Projects::CompareController do
         to: ref_to)
 
     expect(response).to be_success
-    expect(assigns(:diffs).first).not_to be_nil
+    expect(assigns(:diffs).diff_files.first).not_to be_nil
     expect(assigns(:commits).length).to be >= 1
   end
 
-  it 'compare should show some diffs with ignore whitespace change option' do
+  it 'compare shows some diffs with ignore whitespace change option' do
     get(:show,
         namespace_id: project.namespace.to_param,
         project_id: project.to_param,
@@ -32,15 +32,16 @@ describe Projects::CompareController do
         w: 1)
 
     expect(response).to be_success
-    expect(assigns(:diffs).first).not_to be_nil
+    diff_file = assigns(:diffs).diff_files.first
+    expect(diff_file).not_to be_nil
     expect(assigns(:commits).length).to be >= 1
     # without whitespace option, there are more than 2 diff_splits
-    diff_splits = assigns(:diffs).first.diff.split("\n")
+    diff_splits = diff_file.diff.diff.split("\n")
     expect(diff_splits.length).to be <= 2
   end
 
   describe 'non-existent refs' do
-    it 'invalid source ref' do
+    it 'uses invalid source ref' do
       get(:show,
           namespace_id: project.namespace.to_param,
           project_id: project.to_param,
@@ -48,11 +49,11 @@ describe Projects::CompareController do
           to: ref_to)
 
       expect(response).to be_success
-      expect(assigns(:diffs).to_a).to eq([])
+      expect(assigns(:diffs).diff_files.to_a).to eq([])
       expect(assigns(:commits)).to eq([])
     end
 
-    it 'invalid target ref' do
+    it 'uses invalid target ref' do
       get(:show,
           namespace_id: project.namespace.to_param,
           project_id: project.to_param,
@@ -87,9 +88,9 @@ describe Projects::CompareController do
           end
 
           it 'only renders the diffs for the path given' do
-            expect(controller).to receive(:render_diff_for_path).and_wrap_original do |meth, diffs, diff_refs, project|
-              expect(diffs.map(&:new_path)).to contain_exactly(existing_path)
-              meth.call(diffs, diff_refs, project)
+            expect(controller).to receive(:render_diff_for_path).and_wrap_original do |meth, diffs|
+              expect(diffs.diff_files.map(&:new_path)).to contain_exactly(existing_path)
+              meth.call(diffs)
             end
 
             diff_for_path(from: ref_from, to: ref_to, old_path: existing_path, new_path: existing_path)
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/forks_controller_spec.rb b/spec/controllers/projects/forks_controller_spec.rb
index f66bcb8099cb37b8c00fa4f825c2f15177a66bad..ac3469cb8a9da2e4039450810000f87d8d0a8b6d 100644
--- a/spec/controllers/projects/forks_controller_spec.rb
+++ b/spec/controllers/projects/forks_controller_spec.rb
@@ -16,7 +16,7 @@ describe Projects::ForksController do
     context 'when fork is public' do
       before { forked_project.update_attribute(:visibility_level, Project::PUBLIC) }
 
-      it 'should be visible for non logged in users' do
+      it 'is visible for non logged in users' do
         get_forks
 
         expect(assigns[:forks]).to be_present
@@ -28,7 +28,7 @@ describe Projects::ForksController do
         forked_project.update_attributes(visibility_level: Project::PRIVATE, group: group)
       end
 
-      it 'should not be visible for non logged in users' do
+      it 'is not be visible for non logged in users' do
         get_forks
 
         expect(assigns[:forks]).to be_blank
@@ -38,7 +38,7 @@ describe Projects::ForksController do
         before { sign_in(project.creator) }
 
         context 'when user is not a Project member neither a group member' do
-          it 'should not see the Project listed' do
+          it 'does not see the Project listed' do
             get_forks
 
             expect(assigns[:forks]).to be_blank
@@ -48,7 +48,7 @@ describe Projects::ForksController do
         context 'when user is a member of the Project' do
           before { forked_project.team << [project.creator, :developer] }
 
-          it 'should see the project listed' do
+          it 'sees the project listed' do
             get_forks
 
             expect(assigns[:forks]).to be_present
@@ -58,7 +58,7 @@ describe Projects::ForksController do
         context 'when user is a member of the Group' do
           before { forked_project.group.add_developer(project.creator) }
 
-          it 'should see the project listed' do
+          it 'sees the project listed' do
             get_forks
 
             expect(assigns[:forks]).to be_present
diff --git a/spec/controllers/projects/issues_controller_spec.rb b/spec/controllers/projects/issues_controller_spec.rb
index 77f65057f711bed129cffbef43900c54d0baa4a2..0836b71056c7a7d0e44266b3dbb61ba991e494f0 100644
--- a/spec/controllers/projects/issues_controller_spec.rb
+++ b/spec/controllers/projects/issues_controller_spec.rb
@@ -6,37 +6,65 @@ describe Projects::IssuesController do
   let(:issue)   { create(:issue, project: project) }
 
   describe "GET #index" do
-    before do
-      sign_in(user)
-      project.team << [user, :developer]
-    end
+    context 'external issue tracker' do
+      it 'redirects to the external issue tracker' do
+        external = double(issues_url: 'https://example.com/issues')
+        allow(project).to receive(:external_issue_tracker).and_return(external)
+        controller.instance_variable_set(:@project, project)
 
-    it "returns index" do
-      get :index, namespace_id: project.namespace.path, project_id: project.path
+        get :index, namespace_id: project.namespace.path, project_id: project
 
-      expect(response).to have_http_status(200)
+        expect(response).to redirect_to('https://example.com/issues')
+      end
     end
 
-    it "return 301 if request path doesn't match project path" do
-      get :index, namespace_id: project.namespace.path, project_id: project.path.upcase
+    context 'internal issue tracker' do
+      before do
+        sign_in(user)
+        project.team << [user, :developer]
+      end
 
-      expect(response).to redirect_to(namespace_project_issues_path(project.namespace, project))
-    end
+      it "returns index" do
+        get :index, namespace_id: project.namespace.path, project_id: project.path
+
+        expect(response).to have_http_status(200)
+      end
 
-    it "returns 404 when issues are disabled" do
-      project.issues_enabled = false
-      project.save
+      it "returns 301 if request path doesn't match project path" do
+        get :index, namespace_id: project.namespace.path, project_id: project.path.upcase
 
-      get :index, namespace_id: project.namespace.path, project_id: project.path
-      expect(response).to have_http_status(404)
+        expect(response).to redirect_to(namespace_project_issues_path(project.namespace, project))
+      end
+
+      it "returns 404 when issues are disabled" do
+        project.issues_enabled = false
+        project.save
+
+        get :index, namespace_id: project.namespace.path, project_id: project.path
+        expect(response).to have_http_status(404)
+      end
+
+      it "returns 404 when external issue tracker is enabled" do
+        controller.instance_variable_set(:@project, project)
+        allow(project).to receive(:default_issues_tracker?).and_return(false)
+
+        get :index, namespace_id: project.namespace.path, project_id: project.path
+        expect(response).to have_http_status(404)
+      end
     end
+  end
+
+  describe 'GET #new' do
+    context 'external issue tracker' do
+      it 'redirects to the external issue tracker' do
+        external = double(new_issue_path: 'https://example.com/issues/new')
+        allow(project).to receive(:external_issue_tracker).and_return(external)
+        controller.instance_variable_set(:@project, project)
 
-    it "returns 404 when external issue tracker is enabled" do
-      controller.instance_variable_set(:@project, project)
-      allow(project).to receive(:default_issues_tracker?).and_return(false)
+        get :new, namespace_id: project.namespace.path, project_id: project
 
-      get :index, namespace_id: project.namespace.path, project_id: project.path
-      expect(response).to have_http_status(404)
+        expect(response).to redirect_to('https://example.com/issues/new')
+      end
     end
   end
 
@@ -91,21 +119,21 @@ describe Projects::IssuesController do
     let!(:request_forgery_timing_attack) { create(:issue, :confidential, project: project, assignee: assignee) }
 
     describe 'GET #index' do
-      it 'should not list confidential issues for guests' do
+      it 'does not list confidential issues for guests' do
         sign_out(:user)
         get_issues
 
         expect(assigns(:issues)).to eq [issue]
       end
 
-      it 'should not list confidential issues for non project members' do
+      it 'does not list confidential issues for non project members' do
         sign_in(non_member)
         get_issues
 
         expect(assigns(:issues)).to eq [issue]
       end
 
-      it 'should not list confidential issues for project members with guest role' do
+      it 'does not list confidential issues for project members with guest role' do
         sign_in(member)
         project.team << [member, :guest]
 
@@ -114,7 +142,7 @@ describe Projects::IssuesController do
         expect(assigns(:issues)).to eq [issue]
       end
 
-      it 'should list confidential issues for author' do
+      it 'lists confidential issues for author' do
         sign_in(author)
         get_issues
 
@@ -122,7 +150,7 @@ describe Projects::IssuesController do
         expect(assigns(:issues)).not_to include request_forgery_timing_attack
       end
 
-      it 'should list confidential issues for assignee' do
+      it 'lists confidential issues for assignee' do
         sign_in(assignee)
         get_issues
 
@@ -130,7 +158,7 @@ describe Projects::IssuesController do
         expect(assigns(:issues)).to include request_forgery_timing_attack
       end
 
-      it 'should list confidential issues for project members' do
+      it 'lists confidential issues for project members' do
         sign_in(member)
         project.team << [member, :developer]
 
@@ -140,7 +168,7 @@ describe Projects::IssuesController do
         expect(assigns(:issues)).to include request_forgery_timing_attack
       end
 
-      it 'should list confidential issues for admin' do
+      it 'lists confidential issues for admin' do
         sign_in(admin)
         get_issues
 
@@ -246,8 +274,8 @@ describe Projects::IssuesController do
   describe 'POST #create' do
     context 'Akismet is enabled' do
       before do
-        allow_any_instance_of(Gitlab::AkismetHelper).to receive(:check_for_spam?).and_return(true)
-        allow_any_instance_of(Gitlab::AkismetHelper).to receive(:is_spam?).and_return(true)
+        allow_any_instance_of(SpamService).to receive(:check_for_spam?).and_return(true)
+        allow_any_instance_of(AkismetService).to receive(:is_spam?).and_return(true)
       end
 
       def post_spam_issue
@@ -272,6 +300,52 @@ describe Projects::IssuesController do
         expect(spam_logs[0].title).to eq('Spam Title')
       end
     end
+
+    context 'user agent details are saved' do
+      before do
+        request.env['action_dispatch.remote_ip'] = '127.0.0.1'
+      end
+
+      def post_new_issue
+        sign_in(user)
+        project = create(:empty_project, :public)
+        post :create, {
+          namespace_id: project.namespace.to_param,
+          project_id: project.to_param,
+          issue: { title: 'Title', description: 'Description' }
+        }
+      end
+
+      it 'creates a user agent detail' do
+        expect{ post_new_issue }.to change(UserAgentDetail, :count).by(1)
+      end
+    end
+  end
+
+  describe 'POST #mark_as_spam' do
+    context 'properly submits to Akismet' do
+      before do
+        allow_any_instance_of(AkismetService).to receive_messages(submit_spam: true)
+        allow_any_instance_of(ApplicationSetting).to receive_messages(akismet_enabled: true)
+      end
+
+      def post_spam
+        admin = create(:admin)
+        create(:user_agent_detail, subject: issue)
+        project.team << [admin, :master]
+        sign_in(admin)
+        post :mark_as_spam, {
+          namespace_id: project.namespace.path,
+          project_id: project.path,
+          id: issue.iid
+        }
+      end
+
+      it 'updates issue' do
+        post_spam
+        expect(issue.submittable_as_spam?).to be_falsey
+      end
+    end
   end
 
   describe "DELETE #destroy" do
diff --git a/spec/controllers/projects/merge_requests_controller_spec.rb b/spec/controllers/projects/merge_requests_controller_spec.rb
index 210085e3b1a199e99fa647cfb9172847b4909198..c64c2b075c53c81adac2617da089badcb47953ca 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)
@@ -36,7 +41,7 @@ describe Projects::MergeRequestsController do
 
   describe "GET show" do
     shared_examples "export merge as" do |format|
-      it "should generally work" do
+      it "does generally work" do
         get(:show,
             namespace_id: project.namespace.to_param,
             project_id: project.to_param,
@@ -46,7 +51,7 @@ describe Projects::MergeRequestsController do
         expect(response).to be_success
       end
 
-      it "should generate it" do
+      it "generates it" do
         expect_any_instance_of(MergeRequest).to receive(:"to_#{format}")
 
         get(:show,
@@ -56,7 +61,7 @@ describe Projects::MergeRequestsController do
             format: format)
       end
 
-      it "should render it" do
+      it "renders it" do
         get(:show,
             namespace_id: project.namespace.to_param,
             project_id: project.to_param,
@@ -66,7 +71,7 @@ describe Projects::MergeRequestsController do
         expect(response.body).to eq(merge_request.send(:"to_#{format}").to_s)
       end
 
-      it "should not escape Html" do
+      it "does not escape Html" do
         allow_any_instance_of(MergeRequest).to receive(:"to_#{format}").
           and_return('HTML entities &<>" ')
 
@@ -118,7 +123,7 @@ describe Projects::MergeRequestsController do
 
     context 'when filtering by opened state' do
       context 'with opened merge requests' do
-        it 'should list those merge requests' do
+        it 'lists those merge requests' do
           get_merge_requests
 
           expect(assigns(:merge_requests)).to include(merge_request)
@@ -131,7 +136,7 @@ describe Projects::MergeRequestsController do
           merge_request.reopen!
         end
 
-        it 'should list those merge requests' do
+        it 'lists those merge requests' do
           get_merge_requests
 
           expect(assigns(:merge_requests)).to include(merge_request)
@@ -392,9 +397,9 @@ describe Projects::MergeRequestsController do
             end
 
             it 'only renders the diffs for the path given' do
-              expect(controller).to receive(:render_diff_for_path).and_wrap_original do |meth, diffs, diff_refs, project|
-                expect(diffs.map(&:new_path)).to contain_exactly(existing_path)
-                meth.call(diffs, diff_refs, project)
+              expect(controller).to receive(:render_diff_for_path).and_wrap_original do |meth, diffs|
+                expect(diffs.diff_files.map(&:new_path)).to contain_exactly(existing_path)
+                meth.call(diffs)
               end
 
               diff_for_path(id: merge_request.iid, old_path: existing_path, new_path: existing_path)
@@ -455,9 +460,9 @@ describe Projects::MergeRequestsController do
         end
 
         it 'only renders the diffs for the path given' do
-          expect(controller).to receive(:render_diff_for_path).and_wrap_original do |meth, diffs, diff_refs, project|
-            expect(diffs.map(&:new_path)).to contain_exactly(existing_path)
-            meth.call(diffs, diff_refs, project)
+          expect(controller).to receive(:render_diff_for_path).and_wrap_original do |meth, diffs|
+            expect(diffs.diff_files.map(&:new_path)).to contain_exactly(existing_path)
+            meth.call(diffs)
           end
 
           diff_for_path(old_path: existing_path, new_path: existing_path, merge_request: { source_branch: 'feature', target_branch: 'master' })
@@ -477,9 +482,9 @@ describe Projects::MergeRequestsController do
           end
 
           it 'only renders the diffs for the path given' do
-            expect(controller).to receive(:render_diff_for_path).and_wrap_original do |meth, diffs, diff_refs, project|
-              expect(diffs.map(&:new_path)).to contain_exactly(existing_path)
-              meth.call(diffs, diff_refs, project)
+            expect(controller).to receive(:render_diff_for_path).and_wrap_original do |meth, diffs|
+              expect(diffs.diff_files.map(&:new_path)).to contain_exactly(existing_path)
+              meth.call(diffs)
             end
 
             diff_for_path(old_path: existing_path, new_path: existing_path, merge_request: { source_project: other_project, source_branch: 'feature', target_branch: 'master' })
@@ -523,4 +528,135 @@ 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::UnexpectedDelimiter)
+
+        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 '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 resolve_conflicts' do
+    let(:json_response) { JSON.parse(response.body) }
+    let!(:original_head_sha) { merge_request_with_conflicts.diff_head_sha }
+
+    def resolve_conflicts(sections)
+      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',
+           sections: sections,
+           commit_message: 'Commit message'
+    end
+
+    context 'with valid params' do
+      before do
+        resolve_conflicts('2f6fcd96b88b36ce98c38da085c795a27d92a3dd_14_14' => 'head',
+                          '6eb14e00385d2fb284765eb1cd8d420d33d63fc9_9_9' => 'head',
+                          '6eb14e00385d2fb284765eb1cd8d420d33d63fc9_21_21' => 'origin',
+                          '6eb14e00385d2fb284765eb1cd8d420d33d63fc9_49_49' => 'origin')
+      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
+        resolve_conflicts('2f6fcd96b88b36ce98c38da085c795a27d92a3dd_14_14' => 'head')
+      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_9_9')
+      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
 end
diff --git a/spec/controllers/projects/milestones_controller_spec.rb b/spec/controllers/projects/milestones_controller_spec.rb
index d173bb350f1c66c0615d3114ffa5a4290e37ce87..4e3ef5dc6fa7f303c1c5c1558cf69f14a9562006 100644
--- a/spec/controllers/projects/milestones_controller_spec.rb
+++ b/spec/controllers/projects/milestones_controller_spec.rb
@@ -14,7 +14,7 @@ describe Projects::MilestonesController do
   end
 
   describe "#destroy" do
-    it "should remove milestone" do
+    it "removes milestone" do
       expect(issue.milestone_id).to eq(milestone.id)
 
       delete :destroy, namespace_id: project.namespace.id, project_id: project.id, id: milestone.iid, format: :js
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/protected_branches_controller_spec.rb b/spec/controllers/projects/protected_branches_controller_spec.rb
index 596d8d34b7c9a69045afda7cbcee09635c230eed..da6112a13f7587ab365ef38b770dd1bfbd5ff12c 100644
--- a/spec/controllers/projects/protected_branches_controller_spec.rb
+++ b/spec/controllers/projects/protected_branches_controller_spec.rb
@@ -3,7 +3,7 @@ require('spec_helper')
 describe Projects::ProtectedBranchesController do
   describe "GET #index" do
     let(:project) { create(:project_empty_repo, :public) }
-    it "redirect empty repo to projects page" do
+    it "redirects empty repo to projects page" do
       get(:index, namespace_id: project.namespace.to_param, project_id: project.to_param)
     end
   end
diff --git a/spec/controllers/projects/raw_controller_spec.rb b/spec/controllers/projects/raw_controller_spec.rb
index 48f799d8ca1f4cd1cb0579ace17e0d5a3ed437f3..04bd9a01f7b71f259082f7569072b90a4fe544a6 100644
--- a/spec/controllers/projects/raw_controller_spec.rb
+++ b/spec/controllers/projects/raw_controller_spec.rb
@@ -24,7 +24,7 @@ describe Projects::RawController do
     context 'image header' do
       let(:id) { 'master/files/images/6049019_460s.jpg' }
 
-      it 'set image content type header' do
+      it 'sets image content type header' do
         get(:show,
             namespace_id: public_project.namespace.to_param,
             project_id: public_project.to_param,
diff --git a/spec/controllers/projects/services_controller_spec.rb b/spec/controllers/projects/services_controller_spec.rb
index ccd8c741c832427ade04df1b95499031c2ba8700..cccd492ef0672cd75508fc4024f5edab57657937 100644
--- a/spec/controllers/projects/services_controller_spec.rb
+++ b/spec/controllers/projects/services_controller_spec.rb
@@ -19,7 +19,7 @@ describe Projects::ServicesController do
 
     describe "#test" do
       context 'success' do
-        it "should redirect and show success message" do
+        it "redirects and show success message" do
           expect(service).to receive(:test).and_return({ success: true, result: 'done' })
           get :test, namespace_id: project.namespace.id, project_id: project.id, id: service.id, format: :html
           expect(response.status).to redirect_to('/')
@@ -28,7 +28,7 @@ describe Projects::ServicesController do
       end
 
       context 'failure' do
-        it "should redirect and show failure message" do
+        it "redirects and show failure message" do
           expect(service).to receive(:test).and_return({ success: false, result: 'Bad test' })
           get :test, namespace_id: project.namespace.id, project_id: project.id, id: service.id, format: :html
           expect(response.status).to redirect_to('/')
diff --git a/spec/controllers/projects/templates_controller_spec.rb b/spec/controllers/projects/templates_controller_spec.rb
new file mode 100644
index 0000000000000000000000000000000000000000..7b3a26d7ca773695830c09c0d26a323d954d799a
--- /dev/null
+++ b/spec/controllers/projects/templates_controller_spec.rb
@@ -0,0 +1,48 @@
+require 'spec_helper'
+
+describe Projects::TemplatesController do
+  let(:project) { create(:project) }
+  let(:user) { create(:user) }
+  let(:user2) { create(:user) }
+  let(:file_path_1) { '.gitlab/issue_templates/bug.md' }
+  let(:body) { JSON.parse(response.body) }
+
+  before do
+    project.team << [user, :developer]
+    sign_in(user)
+  end
+
+  before do
+    project.team.add_user(user, Gitlab::Access::MASTER)
+    project.repository.commit_file(user, file_path_1, "something valid", "test 3", "master", false)
+  end
+
+  describe '#show' do
+    it 'renders template name and content as json' do
+      get(:show, namespace_id: project.namespace.to_param, template_type: "issue", key: "bug", project_id: project.path, format: :json)
+
+      expect(response.status).to eq(200)
+      expect(body["name"]).to eq("bug")
+      expect(body["content"]).to eq("something valid")
+    end
+
+    it 'renders 404 when unauthorized' do
+      sign_in(user2)
+      get(:show, namespace_id: project.namespace.to_param, template_type: "issue", key: "bug", project_id: project.path, format: :json)
+
+      expect(response.status).to eq(404)
+    end
+
+    it 'renders 404 when template type is not found' do
+      sign_in(user)
+      get(:show, namespace_id: project.namespace.to_param, template_type: "dont_exist", key: "bug", project_id: project.path, format: :json)
+
+      expect(response.status).to eq(404)
+    end
+
+    it 'renders 404 without errors' do
+      sign_in(user)
+      expect { get(:show, namespace_id: project.namespace.to_param, template_type: "dont_exist", key: "bug", project_id: project.path, format: :json) }.not_to raise_error
+    end
+  end
+end
diff --git a/spec/controllers/projects_controller_spec.rb b/spec/controllers/projects_controller_spec.rb
index 3edce4d339c53979daf98e29fe7cdca0f4208f34..ffe0641ddd78932daed3f78012dfc1094c4af861 100644
--- a/spec/controllers/projects_controller_spec.rb
+++ b/spec/controllers/projects_controller_spec.rb
@@ -128,7 +128,7 @@ describe ProjectsController do
     context "when the url contains .atom" do
       let(:public_project_with_dot_atom) { build(:project, :public, name: 'my.atom', path: 'my.atom') }
 
-      it 'expect an error creating the project' do
+      it 'expects an error creating the project' do
         expect(public_project_with_dot_atom).not_to be_valid
       end
     end
@@ -222,7 +222,7 @@ describe ProjectsController do
           create(:forked_project_link, forked_to_project: project_fork)
         end
 
-        it 'should remove fork from project' do
+        it 'removes fork from project' do
           delete(:remove_fork,
               namespace_id: project_fork.namespace.to_param,
               id: project_fork.to_param, format: :js)
@@ -236,7 +236,7 @@ describe ProjectsController do
       context 'when project not forked' do
         let(:unforked_project) { create(:project, namespace: user.namespace) }
 
-        it 'should do nothing if project was not forked' do
+        it 'does nothing if project was not forked' do
           delete(:remove_fork,
               namespace_id: unforked_project.namespace.to_param,
               id: unforked_project.to_param, format: :js)
@@ -256,7 +256,7 @@ describe ProjectsController do
   end
 
   describe "GET refs" do
-    it "should get a list of branches and tags" do
+    it "gets a list of branches and tags" do
       get :refs, namespace_id: public_project.namespace.path, id: public_project.path
 
       parsed_body = JSON.parse(response.body)
@@ -265,7 +265,7 @@ describe ProjectsController do
       expect(parsed_body["Commits"]).to be_nil
     end
 
-    it "should get a list of branches, tags and commits" do
+    it "gets a list of branches, tags and commits" do
       get :refs, namespace_id: public_project.namespace.path, id: public_project.path, ref: "123456"
 
       parsed_body = JSON.parse(response.body)
diff --git a/spec/factories/boards.rb b/spec/factories/boards.rb
new file mode 100644
index 0000000000000000000000000000000000000000..35c4a0b6f080502011c34605eac3f2386a6211eb
--- /dev/null
+++ b/spec/factories/boards.rb
@@ -0,0 +1,5 @@
+FactoryGirl.define do
+  factory :board do
+    project factory: :empty_project
+  end
+end
diff --git a/spec/factories/broadcast_messages.rb b/spec/factories/broadcast_messages.rb
index efe9803b1a7159fc40debcb74442d7b51880fa17..c2fdf89213a614377edc126ae36c5494cfcd4a24 100644
--- a/spec/factories/broadcast_messages.rb
+++ b/spec/factories/broadcast_messages.rb
@@ -1,8 +1,8 @@
 FactoryGirl.define do
   factory :broadcast_message do
     message "MyText"
-    starts_at Date.yesterday
-    ends_at Date.tomorrow
+    starts_at 1.day.ago
+    ends_at 1.day.from_now
 
     trait :expired do
       starts_at 5.days.ago
diff --git a/spec/factories/ci/builds.rb b/spec/factories/ci/builds.rb
index 1b32d560b1620e4fb199235570202e5eef6e4a51..0c93bbdfe26ebfb2a0ca980f86700a93aaff09e5 100644
--- a/spec/factories/ci/builds.rb
+++ b/spec/factories/ci/builds.rb
@@ -7,6 +7,7 @@ FactoryGirl.define do
     stage_idx 0
     ref 'master'
     tag false
+    status 'pending'
     created_at 'Di 29. Okt 09:50:00 CET 2013'
     started_at 'Di 29. Okt 09:51:28 CET 2013'
     finished_at 'Di 29. Okt 09:53:28 CET 2013'
@@ -45,6 +46,10 @@ FactoryGirl.define do
       status 'pending'
     end
 
+    trait :created do
+      status 'created'
+    end
+
     trait :manual do
       status 'skipped'
       self.when 'manual'
diff --git a/spec/factories/ci/pipelines.rb b/spec/factories/ci/pipelines.rb
index a039bef6f3c44ec1751c8566713b0d76f5d63e12..ac2a1ba5dffb66bb1f1bffb1d359e5e4377272c8 100644
--- a/spec/factories/ci/pipelines.rb
+++ b/spec/factories/ci/pipelines.rb
@@ -1,24 +1,8 @@
-# == 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'
     sha '97de212e80737a608d939f648d959671fb0a0142'
+    status 'pending'
 
     project factory: :empty_project
 
diff --git a/spec/factories/commit_statuses.rb b/spec/factories/commit_statuses.rb
index 1e5c479616c50b0531cee190035f85d8bbd51757..995f2080f1008e05497b639ec0267972bc90c8f7 100644
--- a/spec/factories/commit_statuses.rb
+++ b/spec/factories/commit_statuses.rb
@@ -7,6 +7,30 @@ FactoryGirl.define do
     started_at 'Tue, 26 Jan 2016 08:21:42 +0100'
     finished_at 'Tue, 26 Jan 2016 08:23:42 +0100'
 
+    trait :success do
+      status 'success'
+    end
+
+    trait :failed do
+      status 'failed'
+    end
+
+    trait :canceled do
+      status 'canceled'
+    end
+
+    trait :running do
+      status 'running'
+    end
+
+    trait :pending do
+      status 'pending'
+    end
+
+    trait :created do
+      status 'created'
+    end
+
     after(:build) do |build, evaluator|
       build.project = build.pipeline.project
     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/project_hooks.rb b/spec/factories/project_hooks.rb
index 3195fb3ddcc90332facafc6ccb5bfd7b80a6d7f1..4fd51a2349060aa4ccc53f96720dbc66deba525e 100644
--- a/spec/factories/project_hooks.rb
+++ b/spec/factories/project_hooks.rb
@@ -5,5 +5,15 @@ FactoryGirl.define do
     trait :token do
       token { SecureRandom.hex(10) }
     end
+
+    trait :all_events_enabled do
+      push_events true
+      merge_requests_events true
+      tag_push_events true
+      issues_events true
+      note_events true
+      build_events true
+      pipeline_events true
+    end
   end
 end
diff --git a/spec/factories/projects.rb b/spec/factories/projects.rb
index b682ced75acf77977d07a662fdc41b76dcb0c063..f82d68a1816eba8380a1029aeb96c6ca883c6dfa 100644
--- a/spec/factories/projects.rb
+++ b/spec/factories/projects.rb
@@ -83,4 +83,10 @@ FactoryGirl.define do
       )
     end
   end
+
+  factory :project_with_board, parent: :empty_project do
+    after(:create) do |project|
+      project.create_board
+    end
+  end
 end
diff --git a/spec/factories/protected_branches.rb b/spec/factories/protected_branches.rb
index 5575852c2d77af2f5105580147d0ff43d9a402b5..b2695e0482a02860dc76f999a377dc0422ab7515 100644
--- a/spec/factories/protected_branches.rb
+++ b/spec/factories/protected_branches.rb
@@ -3,26 +3,26 @@ FactoryGirl.define do
     name
     project
 
-    after(:create) do |protected_branch|
-      protected_branch.create_push_access_level!(access_level: Gitlab::Access::MASTER)
-      protected_branch.create_merge_access_level!(access_level: Gitlab::Access::MASTER)
+    after(:build) do |protected_branch|
+      protected_branch.push_access_levels.new(access_level: Gitlab::Access::MASTER)
+      protected_branch.merge_access_levels.new(access_level: Gitlab::Access::MASTER)
     end
 
     trait :developers_can_push do
       after(:create) do |protected_branch|
-        protected_branch.push_access_level.update!(access_level: Gitlab::Access::DEVELOPER)
+        protected_branch.push_access_levels.first.update!(access_level: Gitlab::Access::DEVELOPER)
       end
     end
 
     trait :developers_can_merge do
       after(:create) do |protected_branch|
-        protected_branch.merge_access_level.update!(access_level: Gitlab::Access::DEVELOPER)
+        protected_branch.merge_access_levels.first.update!(access_level: Gitlab::Access::DEVELOPER)
       end
     end
 
     trait :no_one_can_push do
       after(:create) do |protected_branch|
-        protected_branch.push_access_level.update!(access_level: Gitlab::Access::NO_ACCESS)
+        protected_branch.push_access_levels.first.update!(access_level: Gitlab::Access::NO_ACCESS)
       end
     end
   end
diff --git a/spec/factories/user_agent_details.rb b/spec/factories/user_agent_details.rb
new file mode 100644
index 0000000000000000000000000000000000000000..9763cc0cf15d37d6b493b836c88b946cfd463a1a
--- /dev/null
+++ b/spec/factories/user_agent_details.rb
@@ -0,0 +1,7 @@
+FactoryGirl.define do
+  factory :user_agent_detail do
+    ip_address '127.0.0.1'
+    user_agent 'AppleWebKit/537.36'
+    association :subject, factory: :issue
+  end
+end
diff --git a/spec/factories_spec.rb b/spec/factories_spec.rb
index 675d9bd18b7aa9e1f538112e528e321c8ca5b641..786e1456f5fa7ecb2011cd551c218aace350e939 100644
--- a/spec/factories_spec.rb
+++ b/spec/factories_spec.rb
@@ -9,7 +9,7 @@ describe 'factories' do
         expect { entity }.not_to raise_error
       end
 
-      it 'should be valid', if: factory.build_class < ActiveRecord::Base do
+      it 'is valid', if: factory.build_class < ActiveRecord::Base do
         expect(entity).to be_valid
       end
     end
diff --git a/spec/features/admin/admin_abuse_reports_spec.rb b/spec/features/admin/admin_abuse_reports_spec.rb
index 16baf7e951625f5d0e093fbfc67a8b6722beba3c..c1731e6414a80c2efed91d85f133cf694263e8e3 100644
--- a/spec/features/admin/admin_abuse_reports_spec.rb
+++ b/spec/features/admin/admin_abuse_reports_spec.rb
@@ -11,7 +11,7 @@ describe "Admin::AbuseReports", feature: true, js: true  do
       end
 
       describe 'in the abuse report view' do
-        it "should present a link to the user's profile" do
+        it "presents a link to the user's profile" do
           visit admin_abuse_reports_path
 
           expect(page).to have_link user.name, href: user_path(user)
@@ -19,7 +19,7 @@ describe "Admin::AbuseReports", feature: true, js: true  do
       end
 
       describe 'in the profile page of the user' do
-        it 'should show a link to the admin view of the user' do
+        it 'shows a link to the admin view of the user' do
           visit user_path(user)
 
           expect(page).to have_link '', href: admin_user_path(user)
diff --git a/spec/features/admin/admin_disables_git_access_protocol_spec.rb b/spec/features/admin/admin_disables_git_access_protocol_spec.rb
index 5b1c0460274087b2b7f3fab5ba8849d356c4e39e..66044b444952de468dd1448ae61bbd47d8fcca89 100644
--- a/spec/features/admin/admin_disables_git_access_protocol_spec.rb
+++ b/spec/features/admin/admin_disables_git_access_protocol_spec.rb
@@ -45,7 +45,6 @@ feature 'Admin disables Git access protocol', feature: true do
       expect(page).to have_content("git clone #{project.ssh_url_to_repo}")
       expect(page).to have_selector('#clone-dropdown')
     end
-
   end
 
   def visit_project
diff --git a/spec/features/admin/admin_hooks_spec.rb b/spec/features/admin/admin_hooks_spec.rb
index 7964951ae99c66996d496226ad272124d457167d..b3ce72b1452ebb67b62167ef88ad26377dab6a46 100644
--- a/spec/features/admin/admin_hooks_spec.rb
+++ b/spec/features/admin/admin_hooks_spec.rb
@@ -9,7 +9,7 @@ describe "Admin::Hooks", feature: true do
   end
 
   describe "GET /admin/hooks" do
-    it "should be ok" do
+    it "is ok" do
       visit admin_root_path
 
       page.within ".layout-nav" do
@@ -19,7 +19,7 @@ describe "Admin::Hooks", feature: true do
       expect(current_path).to eq(admin_hooks_path)
     end
 
-    it "should have hooks list" do
+    it "has hooks list" do
       visit admin_hooks_path
       expect(page).to have_content(@system_hook.url)
     end
@@ -33,7 +33,7 @@ describe "Admin::Hooks", feature: true do
       expect { click_button "Add System Hook" }.to change(SystemHook, :count).by(1)
     end
 
-    it "should open new hook popup" do
+    it "opens new hook popup" do
       expect(current_path).to eq(admin_hooks_path)
       expect(page).to have_content(@url)
     end
diff --git a/spec/features/admin/admin_projects_spec.rb b/spec/features/admin/admin_projects_spec.rb
index 101d955d693c74d1ee29497fa0df828b03c37560..30ded9202a4975f16a42a1b0e8503dc192d72517 100644
--- a/spec/features/admin/admin_projects_spec.rb
+++ b/spec/features/admin/admin_projects_spec.rb
@@ -11,11 +11,11 @@ describe "Admin::Projects", feature: true  do
       visit admin_namespaces_projects_path
     end
 
-    it "should be ok" do
+    it "is ok" do
       expect(current_path).to eq(admin_namespaces_projects_path)
     end
 
-    it "should have projects list" do
+    it "has projects list" do
       expect(page).to have_content(@project.name)
     end
   end
@@ -26,7 +26,7 @@ describe "Admin::Projects", feature: true  do
       click_link "#{@project.name}"
     end
 
-    it "should have project info" do
+    it "has project info" do
       expect(page).to have_content(@project.path)
       expect(page).to have_content(@project.name)
     end
diff --git a/spec/features/admin/admin_users_spec.rb b/spec/features/admin/admin_users_spec.rb
index 767504df251e172e583e5bd18fac0d009bedbf04..cb3191dfdde4c83e5bfd5e1daf8814532880564b 100644
--- a/spec/features/admin/admin_users_spec.rb
+++ b/spec/features/admin/admin_users_spec.rb
@@ -8,11 +8,11 @@ describe "Admin::Users", feature: true  do
       visit admin_users_path
     end
 
-    it "should be ok" do
+    it "is ok" do
       expect(current_path).to eq(admin_users_path)
     end
 
-    it "should have users list" do
+    it "has users list" do
       expect(page).to have_content(@user.email)
       expect(page).to have_content(@user.name)
     end
@@ -66,11 +66,11 @@ describe "Admin::Users", feature: true  do
       fill_in "user_email", with: "bigbang@mail.com"
     end
 
-    it "should create new user" do
+    it "creates new user" do
       expect { click_button "Create user" }.to change {User.count}.by(1)
     end
 
-    it "should apply defaults to user" do
+    it "applies defaults to user" do
       click_button "Create user"
       user = User.find_by(username: 'bang')
       expect(user.projects_limit).
@@ -79,20 +79,20 @@ describe "Admin::Users", feature: true  do
         to eq(Gitlab.config.gitlab.default_can_create_group)
     end
 
-    it "should create user with valid data" do
+    it "creates user with valid data" do
       click_button "Create user"
       user = User.find_by(username: 'bang')
       expect(user.name).to eq('Big Bang')
       expect(user.email).to eq('bigbang@mail.com')
     end
 
-    it "should call send mail" do
+    it "calls send mail" do
       expect_any_instance_of(NotificationService).to receive(:new_user)
 
       click_button "Create user"
     end
 
-    it "should send valid email to user with email & password" do
+    it "sends valid email to user with email & password" do
       perform_enqueued_jobs do
         click_button "Create user"
       end
@@ -106,7 +106,7 @@ describe "Admin::Users", feature: true  do
   end
 
   describe "GET /admin/users/:id" do
-    it "should have user info" do
+    it "has user info" do
       visit admin_users_path
       click_link @user.name
 
@@ -123,13 +123,13 @@ describe "Admin::Users", feature: true  do
           expect(page).to have_content('Impersonate')
         end
 
-        it 'should not show impersonate button for admin itself' do
+        it 'does not show impersonate button for admin itself' do
           visit admin_user_path(@user)
 
           expect(page).not_to have_content('Impersonate')
         end
 
-        it 'should not show impersonate button for blocked user' do
+        it 'does not show impersonate button for blocked user' do
           another_user.block
 
           visit admin_user_path(another_user)
@@ -153,7 +153,7 @@ describe "Admin::Users", feature: true  do
           expect(icon).not_to eql nil
         end
 
-        it 'can log out of impersonated user back to original user' do
+        it 'logs out of impersonated user back to original user' do
           find(:css, 'li.impersonation a').click
 
           expect(page.find(:css, '.header-user .profile-link')['data-user']).to eql(@user.username)
@@ -197,7 +197,7 @@ describe "Admin::Users", feature: true  do
       click_link "edit_user_#{@simple_user.id}"
     end
 
-    it "should have user edit page" do
+    it "has user edit page" do
       expect(page).to have_content('Name')
       expect(page).to have_content('Password')
     end
@@ -212,12 +212,12 @@ describe "Admin::Users", feature: true  do
         click_button "Save changes"
       end
 
-      it "should show page with  new data" do
+      it "shows page with  new data" do
         expect(page).to have_content('bigbang@mail.com')
         expect(page).to have_content('Big Bang')
       end
 
-      it "should change user entry" do
+      it "changes user entry" do
         @simple_user.reload
         expect(@simple_user.name).to eq('Big Bang')
         expect(@simple_user.is_admin?).to be_truthy
diff --git a/spec/features/atom/dashboard_spec.rb b/spec/features/atom/dashboard_spec.rb
index f81a3c117ff23df177f768c151f399eb26b2bdf5..746df36bb258992171f480987aaaf76970707bf5 100644
--- a/spec/features/atom/dashboard_spec.rb
+++ b/spec/features/atom/dashboard_spec.rb
@@ -5,7 +5,7 @@ describe "Dashboard Feed", feature: true  do
     let!(:user) { create(:user, name: "Jonh") }
 
     context "projects atom feed via private token" do
-      it "should render projects atom feed" do
+      it "renders projects atom feed" do
         visit dashboard_projects_path(:atom, private_token: user.private_token)
         expect(body).to have_selector('feed title')
       end
@@ -23,11 +23,11 @@ describe "Dashboard Feed", feature: true  do
         visit dashboard_projects_path(:atom, private_token: user.private_token)
       end
 
-      it "should have issue opened event" do
+      it "has issue opened event" do
         expect(body).to have_content("#{user.name} opened issue ##{issue.iid}")
       end
 
-      it "should have issue comment event" do
+      it "has issue comment event" do
         expect(body).
           to have_content("#{user.name} commented on issue ##{issue.iid}")
       end
diff --git a/spec/features/atom/issues_spec.rb b/spec/features/atom/issues_spec.rb
index baa7814e96a017ab5cc74ae3c5a5e12586f14ab3..09c140868fb818bdbc5adb966081040ed09c15fd 100644
--- a/spec/features/atom/issues_spec.rb
+++ b/spec/features/atom/issues_spec.rb
@@ -9,7 +9,7 @@ describe 'Issues Feed', feature: true  do
     before { project.team << [user, :developer] }
 
     context 'when authenticated' do
-      it 'should render atom feed' do
+      it 'renders atom feed' do
         login_with user
         visit namespace_project_issues_path(project.namespace, project, :atom)
 
@@ -22,7 +22,7 @@ describe 'Issues Feed', feature: true  do
     end
 
     context 'when authenticated via private token' do
-      it 'should render atom feed' do
+      it 'renders atom feed' do
         visit namespace_project_issues_path(project.namespace, project, :atom,
                                             private_token: user.private_token)
 
diff --git a/spec/features/atom/users_spec.rb b/spec/features/atom/users_spec.rb
index 91704377a07a47c708fa95c5c7626019be94278f..a8833194421fca4eb355e158ad1c1a2cf8935aa9 100644
--- a/spec/features/atom/users_spec.rb
+++ b/spec/features/atom/users_spec.rb
@@ -5,7 +5,7 @@ describe "User Feed", feature: true  do
     let!(:user) { create(:user) }
 
     context 'user atom feed via private token' do
-      it "should render user atom feed" do
+      it "renders user atom feed" do
         visit user_path(user, :atom, private_token: user.private_token)
         expect(body).to have_selector('feed title')
       end
@@ -43,24 +43,24 @@ describe "User Feed", feature: true  do
         visit user_path(user, :atom, private_token: user.private_token)
       end
 
-      it 'should have issue opened event' do
+      it 'has issue opened event' do
         expect(body).to have_content("#{safe_name} opened issue ##{issue.iid}")
       end
 
-      it 'should have issue comment event' do
+      it 'has issue comment event' do
         expect(body).
           to have_content("#{safe_name} commented on issue ##{issue.iid}")
       end
 
-      it 'should have XHTML summaries in issue descriptions' do
+      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/
       end
 
-      it 'should have XHTML summaries in notes' do
+      it 'has XHTML summaries in notes' do
         expect(body).to match /Bug confirmed <img[^>]*\/>/
       end
 
-      it 'should have XHTML summaries in merge request descriptions' do
+      it 'has XHTML summaries in merge request descriptions' do
         expect(body).to match /Here is the fix: <\/p><div[^>]*><a[^>]*><img[^>]*\/><\/a><\/div>/
       end
     end
diff --git a/spec/features/boards/boards_spec.rb b/spec/features/boards/boards_spec.rb
new file mode 100644
index 0000000000000000000000000000000000000000..5d777895542cc40b0dd7ba464024f76aca56a78d
--- /dev/null
+++ b/spec/features/boards/boards_spec.rb
@@ -0,0 +1,634 @@
+require 'rails_helper'
+
+describe 'Issue Boards', feature: true, js: true do
+  include WaitForAjax
+
+  let(:project) { create(:empty_project, :public) }
+  let(:user)    { create(:user) }
+  let!(:user2)  { create(:user) }
+
+  before do
+    project.create_board
+    project.board.lists.create(list_type: :backlog)
+    project.board.lists.create(list_type: :done)
+
+    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)
+      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', 'Development', 'Testing', 'Production', 'Ready', '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: 6)
+
+      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') }
+    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!(:list1) { create(:list, board: project.board, label: planning, position: 0) }
+    let!(:list2) { create(:list, board: project.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]) }
+
+    before do
+      visit namespace_project_board_path(project.namespace, project)
+
+      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 issues in lists' do
+      page.within(find('.board:nth-child(2)')) do
+        expect(page.find('.board-header')).to have_content('2')
+        expect(page).to have_selector('.card', count: 2)
+      end
+
+      page.within(find('.board:nth-child(3)')) do
+        expect(page.find('.board-header')).to have_content('2')
+        expect(page).to have_selector('.card', count: 2)
+      end
+    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 '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)
+      wait_for_vue_resource
+
+      page.within(find('.board', match: :first)) do
+        expect(page.find('.board-header')).to have_content('20')
+        expect(page).to have_selector('.card', count: 20)
+
+        evaluate_script("document.querySelectorAll('.board .board-list')[0].scrollTop = document.querySelectorAll('.board .board-list')[0].scrollHeight")
+        wait_for_vue_resource(spinner: false)
+
+        expect(page.find('.board-header')).to have_content('40')
+        expect(page).to have_selector('.card', count: 40)
+      end
+    end
+
+    context 'backlog' do
+      it 'shows issues in backlog with no labels' do
+        page.within(find('.board', match: :first)) do
+          expect(page.find('.board-header')).to have_content('6')
+          expect(page).to have_selector('.card', count: 6)
+        end
+      end
+
+      it 'is searchable' do
+        page.within(find('.board', match: :first)) do
+          find('.form-control').set issue1.title
+
+          wait_for_vue_resource(spinner: false)
+
+          expect(page).to have_selector('.card', count: 1)
+        end
+      end
+
+      it 'clears search' do
+        page.within(find('.board', match: :first)) do
+          find('.form-control').set issue1.title
+
+          expect(page).to have_selector('.card', count: 1)
+
+          find('.board-search-clear-btn').click
+        end
+
+        wait_for_vue_resource
+
+        page.within(find('.board', match: :first)) do
+          expect(page).to have_selector('.card', count: 6)
+        end
+      end
+
+      it 'moves issue from backlog into list' do
+        drag_to(list_to_index: 1)
+
+        page.within(find('.board', match: :first)) do
+          expect(page.find('.board-header')).to have_content('5')
+          expect(page).to have_selector('.card', count: 5)
+        end
+
+        wait_for_vue_resource
+
+        page.within(find('.board:nth-child(2)')) do
+          expect(page.find('.board-header')).to have_content('3')
+          expect(page).to have_selector('.card', count: 3)
+        end
+      end
+    end
+
+    context 'done' do
+      it 'shows list of done issues' do
+        expect(find('.board:nth-child(4)')).to have_selector('.card', count: 1)
+      end
+
+      it 'moves issue to done' do
+        drag_to(list_from_index: 0, list_to_index: 3)
+
+        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)
+
+        expect(find('.board:nth-child(2)')).to have_selector('.card', count: 1)
+        expect(find('.board:nth-child(3)')).to have_selector('.card', count: 1)
+        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')
+
+        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)
+
+        expect(find('.board:nth-child(2)')).to have_selector('.card', count: 1)
+        expect(find('.board:nth-child(3)')).to have_selector('.card', count: 3)
+        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)
+
+        expect(find('.board:nth-child(2)')).to have_selector('.card', count: 3)
+        expect(find('.board:nth-child(3)')).to have_selector('.card', count: 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_selector('.card', count: 3)
+        expect(find('.board:nth-child(2)')).to have_content(issue8.title)
+      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 'moves issues from backlog into new list' do
+          page.within(find('.board', match: :first)) do
+            expect(page.find('.board-header')).to have_content('6')
+            expect(page).to have_selector('.card', count: 6)
+          end
+
+          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
+
+          page.within(find('.board', match: :first)) do
+            expect(page.find('.board-header')).to have_content('5')
+            expect(page).to have_selector('.card', count: 5)
+          end
+        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(spinner: false)
+
+          expect(find('.js-author-search')).to have_content(user2.name)
+        end
+
+        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 '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(spinner: false)
+
+          expect(find('.js-assignee-search')).to have_content(user.name)
+        end
+
+        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 '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(spinner: false)
+
+          expect(find('.js-milestone-select')).to have_content(milestone.title)
+        end
+
+        wait_for_vue_resource
+
+        page.within(find('.board', match: :first)) do
+          expect(page.find('.board-header')).to have_content('0')
+          expect(page).to have_selector('.card', count: 0)
+        end
+
+        page.within(find('.board:nth-child(2)')) do
+          expect(page.find('.board-header')).to have_content('1')
+          expect(page).to have_selector('.card', count: 1)
+        end
+      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(spinner: false)
+            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('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 '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(spinner: false)
+            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('20')
+          expect(page).to have_selector('.card', count: 20)
+
+          evaluate_script("document.querySelectorAll('.board .board-list')[0].scrollTop = document.querySelectorAll('.board .board-list')[0].scrollHeight")
+
+          expect(page.find('.board-header')).to have_content('40')
+          expect(page).to have_selector('.card', count: 40)
+        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(spinner: false)
+            click_link(bug.title)
+            wait_for_vue_resource(spinner: false)
+            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('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 '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(spinner: false)
+            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('5')
+          expect(page).to have_selector('.card', count: 5)
+        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 '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(spinner: false)
+        end
+
+        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
+
+        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(spinner: false)
+
+          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)
+      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)
+      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
+
+  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)
+      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_vue_resource(spinner: true)
+    Timeout.timeout(Capybara.default_max_wait_time) do
+      loop until page.evaluate_script('Vue.activeResources').zero?
+    end
+
+    if spinner
+      expect(find('.boards-list')).not_to have_selector('.fa-spinner')
+    end
+  end
+end
diff --git a/spec/features/ci_lint_spec.rb b/spec/features/ci_lint_spec.rb
index 30e29d9d5526545c8483d3fdc455e7f21ab75b7e..81077f4b00547fc52506a4e1f82239907388a3a3 100644
--- a/spec/features/ci_lint_spec.rb
+++ b/spec/features/ci_lint_spec.rb
@@ -17,7 +17,7 @@ describe 'CI Lint' do
         File.read(Rails.root.join('spec/support/gitlab_stubs/gitlab_ci.yml'))
       end
 
-      it 'Yaml parsing' do
+      it 'parses Yaml' do
         within "table" do
           expect(page).to have_content('Job - rspec')
           expect(page).to have_content('Job - spinach')
diff --git a/spec/features/commits_spec.rb b/spec/features/commits_spec.rb
index 45e1a157a1f1bca31e79769d162695050e67d8b3..5910803df51f4f2c6541da76827fbe2f4cb8962f 100644
--- a/spec/features/commits_spec.rb
+++ b/spec/features/commits_spec.rb
@@ -52,7 +52,7 @@ describe 'Commits' do
             visit namespace_project_commits_path(project.namespace, project, :master)
           end
 
-          it 'should show build status' do
+          it 'shows build status' do
             page.within("//li[@id='commit-#{pipeline.short_sha}']") do
               expect(page).to have_css(".ci-status-link")
             end
diff --git a/spec/features/compare_spec.rb b/spec/features/compare_spec.rb
index c62556948e0faadd495b976288131ca1cf89264e..ca7f73e24cc864d9290cc2407c259b59d4ef4eb8 100644
--- a/spec/features/compare_spec.rb
+++ b/spec/features/compare_spec.rb
@@ -11,11 +11,11 @@ describe "Compare", js: true do
   end
 
   describe "branches" do
-    it "should pre-populate fields" do
+    it "pre-populates fields" do
       expect(page.find_field("from").value).to eq("master")
     end
 
-    it "should compare branches" do
+    it "compares branches" do
       fill_in "from", with: "fea"
       find("#from").click
 
@@ -28,7 +28,7 @@ describe "Compare", js: true do
   end
 
   describe "tags" do
-    it "should compare tags" do
+    it "compares tags" do
       fill_in "from", with: "v1.0"
       find("#from").click
 
diff --git a/spec/features/dashboard/label_filter_spec.rb b/spec/features/dashboard/label_filter_spec.rb
index 24e83d44010cb0bc7fdd511e0c30427447c14b91..4cff12de854a2da9a41f22d7a3717ee4653ea5e6 100644
--- a/spec/features/dashboard/label_filter_spec.rb
+++ b/spec/features/dashboard/label_filter_spec.rb
@@ -16,7 +16,7 @@ describe 'Dashboard > label filter', feature: true, js: true do
   end
 
   context 'duplicate labels' do
-    it 'should remove duplicate labels' do
+    it 'removes duplicate labels' do
       page.within('.labels-filter') do
         click_button 'Label'
       end
diff --git a/spec/features/dashboard_issues_spec.rb b/spec/features/dashboard_issues_spec.rb
index 39805da9d0bb3f8a35a6584d50cc34be7a4c0729..3fb1cb37544717b3a1c3747a546c6d056f52979b 100644
--- a/spec/features/dashboard_issues_spec.rb
+++ b/spec/features/dashboard_issues_spec.rb
@@ -16,7 +16,7 @@ describe "Dashboard Issues filtering", feature: true, js: true do
       visit_issues
     end
 
-    it 'should show all issues with no milestone' do
+    it 'shows all issues with no milestone' do
       show_milestone_dropdown
 
       click_link 'No Milestone'
@@ -24,7 +24,7 @@ describe "Dashboard Issues filtering", feature: true, js: true do
       expect(page).to have_selector('.issue', count: 1)
     end
 
-    it 'should show all issues with any milestone' do
+    it 'shows all issues with any milestone' do
       show_milestone_dropdown
 
       click_link 'Any Milestone'
@@ -32,7 +32,7 @@ describe "Dashboard Issues filtering", feature: true, js: true do
       expect(page).to have_selector('.issue', count: 2)
     end
 
-    it 'should show all issues with the selected milestone' do
+    it 'shows all issues with the selected milestone' do
       show_milestone_dropdown
 
       page.within '.dropdown-content' do
diff --git a/spec/features/gitlab_flavored_markdown_spec.rb b/spec/features/gitlab_flavored_markdown_spec.rb
index a89ac09f23692e81866c942674255b137454359a..84d73d693bcfea5611df4683d955896e537d9b1f 100644
--- a/spec/features/gitlab_flavored_markdown_spec.rb
+++ b/spec/features/gitlab_flavored_markdown_spec.rb
@@ -23,25 +23,25 @@ describe "GitLab Flavored Markdown", feature: true do
   end
 
   describe "for commits" do
-    it "should render title in commits#index" do
+    it "renders title in commits#index" do
       visit namespace_project_commits_path(project.namespace, project, 'master', limit: 1)
 
       expect(page).to have_link(issue.to_reference)
     end
 
-    it "should render title in commits#show" do
+    it "renders title in commits#show" do
       visit namespace_project_commit_path(project.namespace, project, commit)
 
       expect(page).to have_link(issue.to_reference)
     end
 
-    it "should render description in commits#show" do
+    it "renders description in commits#show" do
       visit namespace_project_commit_path(project.namespace, project, commit)
 
       expect(page).to have_link(fred.to_reference)
     end
 
-    it "should render title in repositories#branches" do
+    it "renders title in repositories#branches" do
       visit namespace_project_branches_path(project.namespace, project)
 
       expect(page).to have_link(issue.to_reference)
@@ -62,19 +62,19 @@ describe "GitLab Flavored Markdown", feature: true do
                       description: "ask #{fred.to_reference} for details")
     end
 
-    it "should render subject in issues#index" do
+    it "renders subject in issues#index" do
       visit namespace_project_issues_path(project.namespace, project)
 
       expect(page).to have_link(@other_issue.to_reference)
     end
 
-    it "should render subject in issues#show" do
+    it "renders subject in issues#show" do
       visit namespace_project_issue_path(project.namespace, project, @issue)
 
       expect(page).to have_link(@other_issue.to_reference)
     end
 
-    it "should render details in issues#show" do
+    it "renders details in issues#show" do
       visit namespace_project_issue_path(project.namespace, project, @issue)
 
       expect(page).to have_link(fred.to_reference)
@@ -86,13 +86,13 @@ describe "GitLab Flavored Markdown", feature: true do
       @merge_request = create(:merge_request, source_project: project, target_project: project, title: "fix #{issue.to_reference}")
     end
 
-    it "should render title in merge_requests#index" do
+    it "renders title in merge_requests#index" do
       visit namespace_project_merge_requests_path(project.namespace, project)
 
       expect(page).to have_link(issue.to_reference)
     end
 
-    it "should render title in merge_requests#show" do
+    it "renders title in merge_requests#show" do
       visit namespace_project_merge_request_path(project.namespace, project, @merge_request)
 
       expect(page).to have_link(issue.to_reference)
@@ -107,19 +107,19 @@ describe "GitLab Flavored Markdown", feature: true do
                           description: "ask #{fred.to_reference} for details")
     end
 
-    it "should render title in milestones#index" do
+    it "renders title in milestones#index" do
       visit namespace_project_milestones_path(project.namespace, project)
 
       expect(page).to have_link(issue.to_reference)
     end
 
-    it "should render title in milestones#show" do
+    it "renders title in milestones#show" do
       visit namespace_project_milestone_path(project.namespace, project, @milestone)
 
       expect(page).to have_link(issue.to_reference)
     end
 
-    it "should render description in milestones#show" do
+    it "renders description in milestones#show" do
       visit namespace_project_milestone_path(project.namespace, project, @milestone)
 
       expect(page).to have_link(fred.to_reference)
diff --git a/spec/features/help_pages_spec.rb b/spec/features/help_pages_spec.rb
index 1e2306d7f5930b9f7c67ce0094195141b25878e6..e2101b333e23170003718cf079eec9f36bcc8c7d 100644
--- a/spec/features/help_pages_spec.rb
+++ b/spec/features/help_pages_spec.rb
@@ -5,7 +5,7 @@ describe 'Help Pages', feature: true do
     before do
       login_as :user
     end
-    it 'replace the variable $your_email with the email of the user' do
+    it 'replaces the variable $your_email with the email of the user' do
       visit help_page_path('ssh/README')
       expect(page).to have_content("ssh-keygen -t rsa -C \"#{@user.email}\"")
     end
diff --git a/spec/features/issuables/default_sort_order_spec.rb b/spec/features/issuables/default_sort_order_spec.rb
index 0d495cd04aa9cddb86398d8e795fe2e5e1eda8c7..9a2b879e789a97f19bf2b2ef240016428be6b774 100644
--- a/spec/features/issuables/default_sort_order_spec.rb
+++ b/spec/features/issuables/default_sort_order_spec.rb
@@ -55,7 +55,7 @@ describe 'Projects > Issuables > Default sort order', feature: true do
       it 'is "last updated"' do
         visit_merge_requests_with_state(project, 'merged')
 
-        expect(selected_sort_order).to eq('last updated')
+        expect(find('.issues-other-filters')).to have_content('Last updated')
         expect(first_merge_request).to include(last_updated_issuable.title)
         expect(last_merge_request).to include(first_updated_issuable.title)
       end
@@ -67,7 +67,7 @@ describe 'Projects > Issuables > Default sort order', feature: true do
       it 'is "last updated"' do
         visit_merge_requests_with_state(project, 'closed')
 
-        expect(selected_sort_order).to eq('last updated')
+        expect(find('.issues-other-filters')).to have_content('Last updated')
         expect(first_merge_request).to include(last_updated_issuable.title)
         expect(last_merge_request).to include(first_updated_issuable.title)
       end
@@ -79,7 +79,7 @@ describe 'Projects > Issuables > Default sort order', feature: true do
       it 'is "last created"' do
         visit_merge_requests_with_state(project, 'all')
 
-        expect(selected_sort_order).to eq('last created')
+        expect(find('.issues-other-filters')).to have_content('Last created')
         expect(first_merge_request).to include(last_created_issuable.title)
         expect(last_merge_request).to include(first_created_issuable.title)
       end
@@ -108,7 +108,7 @@ describe 'Projects > Issuables > Default sort order', feature: true do
       it 'is "last created"' do
         visit_issues project
 
-        expect(selected_sort_order).to eq('last created')
+        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
@@ -120,7 +120,7 @@ describe 'Projects > Issuables > Default sort order', feature: true do
       it 'is "last created"' do
         visit_issues_with_state(project, 'open')
 
-        expect(selected_sort_order).to eq('last created')
+        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
@@ -132,7 +132,7 @@ describe 'Projects > Issuables > Default sort order', feature: true do
       it 'is "last updated"' do
         visit_issues_with_state(project, 'closed')
 
-        expect(selected_sort_order).to eq('last updated')
+        expect(find('.issues-other-filters')).to have_content('Last updated')
         expect(first_issue).to include(last_updated_issuable.title)
         expect(last_issue).to include(first_updated_issuable.title)
       end
@@ -144,11 +144,35 @@ describe 'Projects > Issuables > Default sort order', feature: true do
       it 'is "last created"' do
         visit_issues_with_state(project, 'all')
 
-        expect(selected_sort_order).to eq('last created')
+        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_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 07a854ea01419177d76cb7664c2103f2a47a2846..6eb04cf74c523db554b8574e3eaab06ca8eb85de 100644
--- a/spec/features/issues/award_emoji_spec.rb
+++ b/spec/features/issues/award_emoji_spec.rb
@@ -21,32 +21,32 @@ describe 'Awards Emoji', feature: true do
       visit namespace_project_issue_path(project.namespace, project, issue)
     end
 
-    it 'should increment the thumbsdown emoji', js: true do
+    it 'increments the thumbsdown emoji', js: true do
       find('[data-emoji="thumbsdown"]').click
       sleep 2
       expect(thumbsdown_emoji).to have_text("1")
     end
 
     context 'click the thumbsup emoji' do
-      it 'should increment the thumbsup emoji', js: true do
+      it 'increments the thumbsup emoji', js: true do
         find('[data-emoji="thumbsup"]').click
         sleep 2
         expect(thumbsup_emoji).to have_text("1")
       end
 
-      it 'should decrement the thumbsdown emoji', js: true do
+      it 'decrements the thumbsdown emoji', js: true do
         expect(thumbsdown_emoji).to have_text("0")
       end
     end
 
     context 'click the thumbsdown emoji' do
-      it 'should increment the thumbsdown emoji', js: true do
+      it 'increments the thumbsdown emoji', js: true do
         find('[data-emoji="thumbsdown"]').click
         sleep 2
         expect(thumbsdown_emoji).to have_text("1")
       end
 
-      it 'should decrement the thumbsup emoji', js: true do
+      it 'decrements the thumbsup emoji', js: true do
         expect(thumbsup_emoji).to have_text("0")
       end
     end
diff --git a/spec/features/issues/award_spec.rb b/spec/features/issues/award_spec.rb
index 63efecf87802bd842aea91ae8e35fc62a007d11d..401e1ea2b893e27216bbf160a980cac0877f92c4 100644
--- a/spec/features/issues/award_spec.rb
+++ b/spec/features/issues/award_spec.rb
@@ -11,7 +11,7 @@ feature 'Issue awards', js: true, feature: true do
       visit namespace_project_issue_path(project.namespace, project, issue)
     end
 
-    it 'should add award to issue' do
+    it 'adds award to issue' do
       first('.js-emoji-btn').click
       expect(page).to have_selector('.js-emoji-btn.active')
       expect(first('.js-emoji-btn')).to have_content '1'
@@ -20,7 +20,7 @@ feature 'Issue awards', js: true, feature: true do
       expect(first('.js-emoji-btn')).to have_content '1'
     end
 
-    it 'should remove award from issue' do
+    it 'removes award from issue' do
       first('.js-emoji-btn').click
       find('.js-emoji-btn.active').click
       expect(first('.js-emoji-btn')).to have_content '0'
@@ -29,7 +29,7 @@ feature 'Issue awards', js: true, feature: true do
       expect(first('.js-emoji-btn')).to have_content '0'
     end
 
-    it 'should only have one menu on the page' do
+    it 'only has one menu on the page' do
       first('.js-add-award').click
       expect(page).to have_selector('.emoji-menu')
 
@@ -42,7 +42,7 @@ feature 'Issue awards', js: true, feature: true do
       visit namespace_project_issue_path(project.namespace, project, issue)
     end
 
-    it 'should not see award menu button' do
+    it 'does not see award menu button' do
       expect(page).not_to have_selector('.js-award-holder')
     end
   end
diff --git a/spec/features/issues/bulk_assignment_labels_spec.rb b/spec/features/issues/bulk_assignment_labels_spec.rb
index afc093cc1f5626c8d943b82b0e84451f2131fbcd..bc2c087c9b9a0f44ebc0a2608c716baaf36e92e5 100644
--- a/spec/features/issues/bulk_assignment_labels_spec.rb
+++ b/spec/features/issues/bulk_assignment_labels_spec.rb
@@ -175,7 +175,7 @@ feature 'Issues > Labels bulk assignment', feature: true do
           visit namespace_project_issues_path(project.namespace, project)
         end
 
-        it 'labels are kept' do
+        it 'keeps labels' do
           expect(find("#issue_#{issue1.id}")).to have_content 'bug'
           expect(find("#issue_#{issue2.id}")).to have_content 'feature'
 
@@ -197,7 +197,7 @@ feature 'Issues > Labels bulk assignment', feature: true do
           visit namespace_project_issues_path(project.namespace, project)
         end
 
-        it 'existing label is kept and new label is present' do
+        it 'keeps existing label and new label is present' do
           expect(find("#issue_#{issue1.id}")).to have_content 'bug'
 
           check 'check_all_issues'
@@ -222,7 +222,7 @@ feature 'Issues > Labels bulk assignment', feature: true do
           visit namespace_project_issues_path(project.namespace, project)
         end
 
-        it 'existing label is kept and new label is present' do
+        it 'keeps existing label and new label is present' do
           expect(find("#issue_#{issue1.id}")).to have_content 'bug'
           expect(find("#issue_#{issue1.id}")).to have_content 'bug'
           expect(find("#issue_#{issue2.id}")).to have_content 'feature'
@@ -252,7 +252,7 @@ feature 'Issues > Labels bulk assignment', feature: true do
           visit namespace_project_issues_path(project.namespace, project)
         end
 
-        it 'labels are kept' do
+        it 'keeps labels' do
           expect(find("#issue_#{issue1.id}")).to have_content 'bug'
           expect(find("#issue_#{issue1.id}")).to have_content 'First Release'
           expect(find("#issue_#{issue2.id}")).to have_content 'feature'
diff --git a/spec/features/issues/filter_by_labels_spec.rb b/spec/features/issues/filter_by_labels_spec.rb
index cb117d2476f16e221c175fdd7d9659b13d8cd156..908b18e5339cdf55cc8f5b2e0fdab047a1c3753c 100644
--- a/spec/features/issues/filter_by_labels_spec.rb
+++ b/spec/features/issues/filter_by_labels_spec.rb
@@ -37,25 +37,25 @@ feature 'Issue filtering by Labels', feature: true do
       wait_for_ajax
     end
 
-    it 'should show issue "Bugfix1" and "Bugfix2" in issues list' do
+    it 'shows issue "Bugfix1" and "Bugfix2" in issues list' do
       expect(page).to have_content "Bugfix1"
       expect(page).to have_content "Bugfix2"
     end
 
-    it 'should not show "Feature1" in issues list' do
+    it 'does not show "Feature1" in issues list' do
       expect(page).not_to have_content "Feature1"
     end
 
-    it 'should show label "bug" in filtered-labels' do
+    it 'shows label "bug" in filtered-labels' do
       expect(find('.filtered-labels')).to have_content "bug"
     end
 
-    it 'should not show label "feature" and "enhancement" in filtered-labels' do
+    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 'should remove label "bug"' do
+    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"
@@ -71,20 +71,20 @@ feature 'Issue filtering by Labels', feature: true do
       wait_for_ajax
     end
 
-    it 'should show issue "Feature1" in issues list' do
+    it 'shows issue "Feature1" in issues list' do
       expect(page).to have_content "Feature1"
     end
 
-    it 'should not show "Bugfix1" and "Bugfix2" in issues list' do
+    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 'should show label "feature" in filtered-labels' do
+    it 'shows label "feature" in filtered-labels' do
       expect(find('.filtered-labels')).to have_content "feature"
     end
 
-    it 'should not show label "bug" and "enhancement" in filtered-labels' do
+    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
@@ -99,20 +99,20 @@ feature 'Issue filtering by Labels', feature: true do
       wait_for_ajax
     end
 
-    it 'should show issue "Bugfix2" in issues list' do
+    it 'shows issue "Bugfix2" in issues list' do
       expect(page).to have_content "Bugfix2"
     end
 
-    it 'should not show "Feature1" and "Bugfix1" in issues list' do
+    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 'should show label "enhancement" in filtered-labels' do
+    it 'shows label "enhancement" in filtered-labels' do
       expect(find('.filtered-labels')).to have_content "enhancement"
     end
 
-    it 'should not show label "feature" and "bug" in filtered-labels' do
+    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
@@ -128,21 +128,21 @@ feature 'Issue filtering by Labels', feature: true do
       wait_for_ajax
     end
 
-    it 'should not show "Bugfix1" or "Feature1" in issues list' do
+    it 'does not show "Bugfix1" or "Feature1" in issues list' do
       expect(page).not_to have_content "Bugfix1"
       expect(page).not_to have_content "Feature1"
     end
 
-    it 'should show label "enhancement" and "feature" in filtered-labels' do
+    it 'shows label "enhancement" and "feature" in filtered-labels' do
       expect(find('.filtered-labels')).to have_content "enhancement"
       expect(find('.filtered-labels')).to have_content "feature"
     end
 
-    it 'should not show label "bug" in filtered-labels' do
+    it 'does not show label "bug" in filtered-labels' do
       expect(find('.filtered-labels')).not_to have_content "bug"
     end
 
-    it 'should remove label "enhancement"' do
+    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"
@@ -159,20 +159,20 @@ feature 'Issue filtering by Labels', feature: true do
       wait_for_ajax
     end
 
-    it 'should show issue "Bugfix2" in issues list' do
+    it 'shows issue "Bugfix2" in issues list' do
       expect(page).to have_content "Bugfix2"
     end
 
-    it 'should not show "Feature1"' do
+    it 'does not show "Feature1"' do
       expect(page).not_to have_content "Feature1"
     end
 
-    it 'should show label "bug" and "enhancement" in filtered-labels' do
+    it 'shows label "bug" and "enhancement" in filtered-labels' do
       expect(find('.filtered-labels')).to have_content "bug"
       expect(find('.filtered-labels')).to have_content "enhancement"
     end
 
-    it 'should not show label "feature" in filtered-labels' do
+    it 'does not show label "feature" in filtered-labels' do
       expect(find('.filtered-labels')).not_to have_content "feature"
     end
   end
@@ -191,7 +191,7 @@ feature 'Issue filtering by Labels', feature: true do
       end
     end
 
-    it 'should allow user to remove filtered labels' do
+    it 'allows user to remove filtered labels' do
       first('.js-label-filter-remove').click
       wait_for_ajax
 
@@ -201,7 +201,7 @@ feature 'Issue filtering by Labels', feature: true do
   end
 
   context 'dropdown filtering', js: true do
-    it 'should filter by label name' do
+    it 'filters by label name' do
       page.within '.labels-filter' do
         click_button 'Label'
         wait_for_ajax
diff --git a/spec/features/issues/filter_by_milestone_spec.rb b/spec/features/issues/filter_by_milestone_spec.rb
index 9944518589378aae5e141d4bfcb51435eb430e86..485dc5600616d6cf5fe24cff40fe4f129744862d 100644
--- a/spec/features/issues/filter_by_milestone_spec.rb
+++ b/spec/features/issues/filter_by_milestone_spec.rb
@@ -15,7 +15,7 @@ feature 'Issue filtering by Milestone', feature: true do
   end
 
   context 'filters by upcoming milestone', js: true do
-    it 'should not show issues with no expiry' do
+    it 'does not show issues with no expiry' do
       create(:issue, project: project)
       create(:issue, project: project, milestone: milestone)
 
@@ -25,7 +25,7 @@ feature 'Issue filtering by Milestone', feature: true do
       expect(page).to have_css('.issue', count: 0)
     end
 
-    it 'should show issues in future' do
+    it 'shows issues in future' do
       milestone = create(:milestone, project: project, due_date: Date.tomorrow)
       create(:issue, project: project)
       create(:issue, project: project, milestone: milestone)
@@ -36,7 +36,7 @@ feature 'Issue filtering by Milestone', feature: true do
       expect(page).to have_css('.issue', count: 1)
     end
 
-    it 'should not show issues in past' do
+    it 'does not show issues in past' do
       milestone = create(:milestone, project: project, due_date: Date.yesterday)
       create(:issue, project: project)
       create(:issue, project: project, milestone: milestone)
diff --git a/spec/features/issues/filter_issues_spec.rb b/spec/features/issues/filter_issues_spec.rb
index 4b9b5394b61afcfc72b53c9f5dcf5dc83839b587..e262f2858680e99e3997abd84fad64c6a5865e72 100644
--- a/spec/features/issues/filter_issues_spec.rb
+++ b/spec/features/issues/filter_issues_spec.rb
@@ -26,17 +26,17 @@ describe 'Filter issues', feature: true do
     end
 
     context 'assignee', js: true do
-      it 'should update to current user' do
+      it 'updates to current user' do
         expect(find('.js-assignee-search .dropdown-toggle-text')).to have_content(user.name)
       end
 
-      it 'should not change when closed link is clicked' do
+      it 'does not change when closed link is clicked' do
         find('.issues-state-filters a', text: "Closed").click
 
         expect(find('.js-assignee-search .dropdown-toggle-text')).to have_content(user.name)
       end
 
-      it 'should not change when all link is clicked' do
+      it 'does not change when all link is clicked' do
         find('.issues-state-filters a', text: "All").click
 
         expect(find('.js-assignee-search .dropdown-toggle-text')).to have_content(user.name)
@@ -56,17 +56,17 @@ describe 'Filter issues', feature: true do
     end
 
     context 'milestone', js: true do
-      it 'should update to current milestone' do
+      it 'updates to current milestone' do
         expect(find('.js-milestone-select .dropdown-toggle-text')).to have_content(milestone.title)
       end
 
-      it 'should not change when closed link is clicked' do
+      it 'does not change when closed link is clicked' do
         find('.issues-state-filters a', text: "Closed").click
 
         expect(find('.js-milestone-select .dropdown-toggle-text')).to have_content(milestone.title)
       end
 
-      it 'should not change when all link is clicked' do
+      it 'does not change when all link is clicked' do
         find('.issues-state-filters a', text: "All").click
 
         expect(find('.js-milestone-select .dropdown-toggle-text')).to have_content(milestone.title)
@@ -81,7 +81,7 @@ describe 'Filter issues', feature: true do
       wait_for_ajax
     end
 
-    it 'should filter by any label' do
+    it 'filters by any label' do
       find('.dropdown-menu-labels a', text: 'Any Label').click
       page.first('.labels-filter .dropdown-title .dropdown-menu-close-icon').click
       wait_for_ajax
@@ -89,7 +89,7 @@ describe 'Filter issues', feature: true do
       expect(find('.labels-filter')).to have_content 'Label'
     end
 
-    it 'should filter by no label' do
+    it 'filters by no label' do
       find('.dropdown-menu-labels a', text: 'No Label').click
       page.first('.labels-filter .dropdown-title .dropdown-menu-close-icon').click
       wait_for_ajax
@@ -100,7 +100,7 @@ describe 'Filter issues', feature: true do
       expect(find('.js-label-select .dropdown-toggle-text')).to have_content('No Label')
     end
 
-    it 'should filter by no label' do
+    it 'filters by no label' do
       find('.dropdown-menu-labels a', text: label.title).click
       page.within '.labels-filter' do
         expect(page).to have_content label.title
@@ -117,7 +117,7 @@ describe 'Filter issues', feature: true do
 
       find('.dropdown-menu-user-link', text: user.username).click
 
-      wait_for_ajax
+      expect(page).not_to have_selector('.issues-list .issue')
 
       find('.js-label-select').click
 
@@ -128,19 +128,19 @@ describe 'Filter issues', feature: true do
     end
 
     context 'assignee and label', js: true do
-      it 'should update to current assignee and label' do
+      it 'updates to current assignee and label' do
         expect(find('.js-assignee-search .dropdown-toggle-text')).to have_content(user.name)
         expect(find('.js-label-select .dropdown-toggle-text')).to have_content(label.title)
       end
 
-      it 'should not change when closed link is clicked' do
+      it 'does not change when closed link is clicked' do
         find('.issues-state-filters a', text: "Closed").click
 
         expect(find('.js-assignee-search .dropdown-toggle-text')).to have_content(user.name)
         expect(find('.js-label-select .dropdown-toggle-text')).to have_content(label.title)
       end
 
-      it 'should not change when all link is clicked' do
+      it 'does not change when all link is clicked' do
         find('.issues-state-filters a', text: "All").click
 
         expect(find('.js-assignee-search .dropdown-toggle-text')).to have_content(user.name)
@@ -168,7 +168,7 @@ describe 'Filter issues', feature: true do
     end
 
     context 'only text', js: true do
-      it 'should filter issues by searched text' do
+      it 'filters issues by searched text' do
         fill_in 'issue_search', with: 'Bug'
 
         page.within '.issues-list' do
@@ -176,7 +176,7 @@ describe 'Filter issues', feature: true do
         end
       end
 
-      it 'should not show any issues' do
+      it 'does not show any issues' do
         fill_in 'issue_search', with: 'testing'
 
         page.within '.issues-list' do
@@ -186,7 +186,7 @@ describe 'Filter issues', feature: true do
     end
 
     context 'text and dropdown options', js: true do
-      it 'should filter by text and label' do
+      it 'filters by text and label' do
         fill_in 'issue_search', with: 'Bug'
 
         page.within '.issues-list' do
@@ -204,7 +204,7 @@ describe 'Filter issues', feature: true do
         end
       end
 
-      it 'should filter by text and milestone' do
+      it 'filters by text and milestone' do
         fill_in 'issue_search', with: 'Bug'
 
         page.within '.issues-list' do
@@ -221,7 +221,7 @@ describe 'Filter issues', feature: true do
         end
       end
 
-      it 'should filter by text and assignee' do
+      it 'filters by text and assignee' do
         fill_in 'issue_search', with: 'Bug'
 
         page.within '.issues-list' do
@@ -238,7 +238,7 @@ describe 'Filter issues', feature: true do
         end
       end
 
-      it 'should filter by text and author' do
+      it 'filters by text and author' do
         fill_in 'issue_search', with: 'Bug'
 
         page.within '.issues-list' do
@@ -269,7 +269,7 @@ describe 'Filter issues', feature: true do
       visit namespace_project_issues_path(project.namespace, project)
     end
 
-    it 'should be able to filter and sort issues' do
+    it 'is able to filter and sort issues' do
       click_button 'Label'
       wait_for_ajax
       page.within '.labels-filter' do
diff --git a/spec/features/issues/issue_sidebar_spec.rb b/spec/features/issues/issue_sidebar_spec.rb
index 5739bc64dfb2b8c723000a459e4fbc03affb8197..4b1aec8bf7164eecc89982a6cb5bbcbc39ad38fe 100644
--- a/spec/features/issues/issue_sidebar_spec.rb
+++ b/spec/features/issues/issue_sidebar_spec.rb
@@ -17,7 +17,7 @@ feature 'Issue Sidebar', feature: true do
     end
 
     describe 'when clicking on edit labels', js: true do
-      it 'dropdown has an option to create a new label' do
+      it 'shows dropdown option to create a new label' do
         find('.block.labels .edit-link').click
 
         page.within('.block.labels') do
@@ -27,7 +27,7 @@ feature 'Issue Sidebar', feature: true do
     end
 
     context 'creating a new label', js: true do
-      it 'option to crate a new label is present' do
+      it 'shows option to crate a new label is present' do
         page.within('.block.labels') do
           find('.edit-link').click
 
@@ -35,7 +35,7 @@ feature 'Issue Sidebar', feature: true do
         end
       end
 
-      it 'dropdown switches to "create label" section' do
+      it 'shows dropdown switches to "create label" section' do
         page.within('.block.labels') do
           find('.edit-link').click
           click_link 'Create new'
@@ -44,7 +44,7 @@ feature 'Issue Sidebar', feature: true do
         end
       end
 
-      it 'new label is added' do
+      it 'adds new label' do
         page.within('.block.labels') do
           find('.edit-link').click
           sleep 1
diff --git a/spec/features/issues/new_branch_button_spec.rb b/spec/features/issues/new_branch_button_spec.rb
index 16e188d2a8af869fd79e0c5c192335fa851b896c..e528aff4d41454920daab187a0c44d1bdf8e98b5 100644
--- a/spec/features/issues/new_branch_button_spec.rb
+++ b/spec/features/issues/new_branch_button_spec.rb
@@ -41,7 +41,7 @@ feature 'Start new branch from an issue', feature: true do
   end
 
   context "for visiters" do
-    it 'no button is shown', js: true do
+    it 'shows no buttons', js: true do
       visit namespace_project_issue_path(project.namespace, project, issue)
 
       expect(page).not_to have_css('#new-branch')
diff --git a/spec/features/issues/todo_spec.rb b/spec/features/issues/todo_spec.rb
index bc0f437a8ce8d23b2b6c1aee33f11c4a5ebe8807..de8fdda388dc7a109cd6e5c6eebcceec0c472934 100644
--- a/spec/features/issues/todo_spec.rb
+++ b/spec/features/issues/todo_spec.rb
@@ -11,7 +11,7 @@ feature 'Manually create a todo item from issue', feature: true, js: true do
     visit namespace_project_issue_path(project.namespace, project, issue)
   end
 
-  it 'should create todo when clicking button' do
+  it 'creates todo when clicking button' do
     page.within '.issuable-sidebar' do
       click_button 'Add Todo'
       expect(page).to have_content 'Mark Done'
@@ -28,7 +28,7 @@ feature 'Manually create a todo item from issue', feature: true, js: true do
     end
   end
 
-  it 'should mark a todo as done' do
+  it 'marks a todo as done' do
     page.within '.issuable-sidebar' do
       click_button 'Add Todo'
       click_button 'Mark Done'
diff --git a/spec/features/issues/update_issues_spec.rb b/spec/features/issues/update_issues_spec.rb
index ddbd69b28912af7ffa682bb2c24cc7622a6a472a..ae5da3877a89aaa7717ce92697fb5bf7333d66b9 100644
--- a/spec/features/issues/update_issues_spec.rb
+++ b/spec/features/issues/update_issues_spec.rb
@@ -13,7 +13,7 @@ feature 'Multiple issue updating from issues#index', feature: true do
   end
 
   context 'status', js: true do
-    it 'should be set to closed' do
+    it 'sets to closed' do
       visit namespace_project_issues_path(project.namespace, project)
 
       find('#check_all_issues').click
@@ -24,7 +24,7 @@ feature 'Multiple issue updating from issues#index', feature: true do
       expect(page).to have_selector('.issue', count: 0)
     end
 
-    it 'should be set to open' do
+    it 'sets to open' do
       create_closed
       visit namespace_project_issues_path(project.namespace, project, state: 'closed')
 
@@ -38,7 +38,7 @@ feature 'Multiple issue updating from issues#index', feature: true do
   end
 
   context 'assignee', js: true do
-    it 'should update to current user' do
+    it 'updates to current user' do
       visit namespace_project_issues_path(project.namespace, project)
 
       find('#check_all_issues').click
@@ -52,7 +52,7 @@ feature 'Multiple issue updating from issues#index', feature: true do
       end
     end
 
-    it 'should update to unassigned' do
+    it 'updates to unassigned' do
       create_assigned
       visit namespace_project_issues_path(project.namespace, project)
 
@@ -68,7 +68,7 @@ feature 'Multiple issue updating from issues#index', feature: true do
   context 'milestone', js: true do
     let(:milestone)  { create(:milestone, project: project) }
 
-    it 'should update milestone' do
+    it 'updates milestone' do
       visit namespace_project_issues_path(project.namespace, project)
 
       find('#check_all_issues').click
@@ -80,7 +80,7 @@ feature 'Multiple issue updating from issues#index', feature: true do
       expect(find('.issue')).to have_content milestone.title
     end
 
-    it 'should set to no milestone' do
+    it 'sets to no milestone' do
       create_with_milestone
       visit namespace_project_issues_path(project.namespace, project)
 
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..2883e3926940aef814ab89057fad03a8778b6f76
--- /dev/null
+++ b/spec/features/issues/user_uses_slash_commands_spec.rb
@@ -0,0 +1,58 @@
+require 'rails_helper'
+
+feature 'Issues > User uses slash commands', feature: true, js: true do
+  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
+
+    describe 'adding a due date from note' do
+      let(:issue) { create(:issue, project: project) }
+
+      it 'does not create a note, and sets the due date accordingly' do
+        page.within('.js-main-target-form') do
+          fill_in 'note[note]', with: "/due 2016-08-28"
+          click_button 'Comment'
+        end
+
+        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
+
+    describe 'removing a due date from note' do
+      let(:issue) { create(:issue, project: project, due_date: Date.new(2016, 8, 28)) }
+
+      it 'does not create a note, and removes the due date accordingly' do
+        expect(issue.due_date).to eq Date.new(2016, 8, 28)
+
+        page.within('.js-main-target-form') do
+          fill_in 'note[note]', with: "/remove_due_date"
+          click_button 'Comment'
+        end
+
+        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
+  end
+end
diff --git a/spec/features/issues_spec.rb b/spec/features/issues_spec.rb
index 9c92b52898c3916e846f6611b827f92a0d42ac48..d744d0eb6aff5f60f0b5ec7408202c4325f34f1f 100644
--- a/spec/features/issues_spec.rb
+++ b/spec/features/issues_spec.rb
@@ -26,7 +26,7 @@ describe 'Issues', feature: true do
       find('.js-zen-enter').click
     end
 
-    it 'should open new issue popup' do
+    it 'opens new issue popup' do
       expect(page).to have_content("Issue ##{issue.iid}")
     end
 
@@ -71,7 +71,7 @@ describe 'Issues', feature: true do
         visit new_namespace_project_issue_path(project.namespace, project)
       end
 
-      it 'should save with due date' do
+      it 'saves with due date' do
         date = Date.today.at_beginning_of_month
 
         fill_in 'issue_title', with: 'bug 345'
@@ -99,7 +99,7 @@ describe 'Issues', feature: true do
         visit edit_namespace_project_issue_path(project.namespace, project, issue)
       end
 
-      it 'should save with due date' do
+      it 'saves with due date' do
         date = Date.today.at_beginning_of_month
 
         expect(find('#issuable-due-date').value).to eq date.to_s
@@ -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
 
@@ -155,7 +166,7 @@ describe 'Issues', feature: true do
 
     let(:issue) { @issue }
 
-    it 'should allow filtering by issues with no specified assignee' do
+    it 'allows filtering by issues with no specified assignee' do
       visit namespace_project_issues_path(project.namespace, project, assignee_id: IssuableFinder::NONE)
 
       expect(page).to have_content 'foobar'
@@ -163,7 +174,7 @@ describe 'Issues', feature: true do
       expect(page).not_to have_content 'gitlab'
     end
 
-    it 'should allow filtering by a specified assignee' do
+    it 'allows filtering by a specified assignee' do
       visit namespace_project_issues_path(project.namespace, project, assignee_id: @user.id)
 
       expect(page).not_to have_content 'foobar'
@@ -514,7 +525,7 @@ describe 'Issues', feature: true do
         visit new_namespace_project_issue_path(project.namespace, project)
       end
 
-      it 'should upload file when dragging into textarea' do
+      it 'uploads file when dragging into textarea' do
         drop_in_dropzone test_image_file
 
         # Wait for the file to upload
@@ -525,7 +536,7 @@ describe 'Issues', feature: true do
     end
   end
 
-  describe 'new issue by email' do
+  xdescribe 'new issue by email' do
     shared_examples 'show the email in the modal' do
       before do
         stub_incoming_email_setting(enabled: true, address: "p+%{key}@gl.ab")
@@ -562,7 +573,7 @@ describe 'Issues', feature: true do
         visit namespace_project_issue_path(project.namespace, project, issue)
       end
 
-      it 'should add due date to issue' do
+      it 'adds due date to issue' do
         page.within '.due_date' do
           click_link 'Edit'
 
@@ -574,7 +585,7 @@ describe 'Issues', feature: true do
         end
       end
 
-      it 'should remove due date from issue' do
+      it 'removes due date from issue' do
         page.within '.due_date' do
           click_link 'Edit'
 
diff --git a/spec/features/login_spec.rb b/spec/features/login_spec.rb
index 58753ff21f6de84538bec0af72f0a4dbc1888eeb..2523b4b78982c6d5afe78a1ca9d66a3ed8aae604 100644
--- a/spec/features/login_spec.rb
+++ b/spec/features/login_spec.rb
@@ -128,10 +128,10 @@ feature 'Login', feature: true do
         end
         allow(Gitlab::OAuth::Provider).to receive_messages(providers: [:saml], config_for: saml_config)
         allow(Gitlab.config.omniauth).to receive_messages(messages)
-        allow_any_instance_of(Object).to receive(:user_omniauth_authorize_path).with('saml').and_return('/users/auth/saml')
+        expect_any_instance_of(Object).to receive(:omniauth_authorize_path).with(:user, "saml").and_return('/users/auth/saml')
       end
 
-      it 'should show 2FA prompt after OAuth login' do
+      it 'shows 2FA prompt after OAuth login' do
         stub_omniauth_config(enabled: true, auto_link_saml_user: true, allow_single_sign_on: ['saml'], providers: [saml_config])
         user = create(:omniauth_user, :two_factor, extern_uid: 'my-uid', provider: 'saml')
         login_via('saml', user, 'my-uid')
diff --git a/spec/features/merge_requests/award_spec.rb b/spec/features/merge_requests/award_spec.rb
index 007f67d60804e11bffc33c4675ad7614e89a76a5..ac260e118d00c9d9e8e0e263c443ad984470ba63 100644
--- a/spec/features/merge_requests/award_spec.rb
+++ b/spec/features/merge_requests/award_spec.rb
@@ -11,7 +11,7 @@ feature 'Merge request awards', js: true, feature: true do
       visit namespace_project_merge_request_path(project.namespace, project, merge_request)
     end
 
-    it 'should add award to merge request' do
+    it 'adds award to merge request' do
       first('.js-emoji-btn').click
       expect(page).to have_selector('.js-emoji-btn.active')
       expect(first('.js-emoji-btn')).to have_content '1'
@@ -20,7 +20,7 @@ feature 'Merge request awards', js: true, feature: true do
       expect(first('.js-emoji-btn')).to have_content '1'
     end
 
-    it 'should remove award from merge request' do
+    it 'removes award from merge request' do
       first('.js-emoji-btn').click
       find('.js-emoji-btn.active').click
       expect(first('.js-emoji-btn')).to have_content '0'
@@ -29,7 +29,7 @@ feature 'Merge request awards', js: true, feature: true do
       expect(first('.js-emoji-btn')).to have_content '0'
     end
 
-    it 'should only have one menu on the page' do
+    it 'has only one menu on the page' do
       first('.js-add-award').click
       expect(page).to have_selector('.emoji-menu')
 
@@ -42,7 +42,7 @@ feature 'Merge request awards', js: true, feature: true do
       visit namespace_project_merge_request_path(project.namespace, project, merge_request)
     end
 
-    it 'should not see award menu button' do
+    it 'does not see award menu button' do
       expect(page).not_to have_selector('.js-award-holder')
     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..930c36ade2bf48ef77e589545c477a70f24be605
--- /dev/null
+++ b/spec/features/merge_requests/conflicts_spec.rb
@@ -0,0 +1,72 @@
+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
+
+  context 'when a merge request can be resolved in the UI' do
+    let(:merge_request) { create_merge_request('conflict-resolvable') }
+
+    before do
+      project.team << [user, :developer]
+      login_as(user)
+
+      visit namespace_project_merge_request_path(project.namespace, project, merge_request)
+    end
+
+    it 'shows a link to the conflict resolution page' do
+      expect(page).to have_link('conflicts', href: /\/conflicts\Z/)
+    end
+
+    context 'visiting the conflicts resolution page' do
+      before { click_link('conflicts', href: /\/conflicts\Z/) }
+
+      it 'shows the conflicts' do
+        begin
+          expect(find('#conflicts')).to have_content('popen.rb')
+        rescue Capybara::Poltergeist::JavascriptError
+          retry
+        end
+      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-contains-conflict-markers' => 'when the conflicts contain a file with ambiguous conflict markers',
+    'conflict-missing-side' => 'when the conflicts contain a file edited in one branch and deleted in another'
+  }
+
+  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 e296078bad80fb8ed4e85cffe3fe727264a39c2e..b963d1305b5315314ff50e6a1f2f869f776bbc6e 100644
--- a/spec/features/merge_requests/create_new_mr_spec.rb
+++ b/spec/features/merge_requests/create_new_mr_spec.rb
@@ -8,11 +8,14 @@ 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')
 
     first('.js-source-branch').click
     first('.dropdown-source-branch .dropdown-content a', text: 'orphaned-branch').click
@@ -40,4 +43,20 @@ 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
 end
diff --git a/spec/features/merge_requests/created_from_fork_spec.rb b/spec/features/merge_requests/created_from_fork_spec.rb
index f676200ecf33631f3499e97f3896abe0e431d8d9..4d5d4aa121add23e76759dc7de4fd0f318ac3001 100644
--- a/spec/features/merge_requests/created_from_fork_spec.rb
+++ b/spec/features/merge_requests/created_from_fork_spec.rb
@@ -29,12 +29,16 @@ feature 'Merge request created from fork' do
     include WaitForAjax
 
     given(:pipeline) do
-      create(:ci_pipeline_with_two_job, project: fork_project,
-                                        sha: merge_request.diff_head_sha,
-                                        ref: merge_request.source_branch)
+      create(:ci_pipeline,
+             project: fork_project,
+             sha: merge_request.diff_head_sha,
+             ref: merge_request.source_branch)
     end
 
-    background { pipeline.create_builds(user) }
+    background do
+      create(:ci_build, pipeline: pipeline, name: 'rspec')
+      create(:ci_build, pipeline: pipeline, name: 'spinach')
+    end
 
     scenario 'user visits a pipelines page', js: true do
       visit_merge_request(merge_request)
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..c6adf7e4c567799b507a852dd0ea0fdc8837df39
--- /dev/null
+++ b/spec/features/merge_requests/diff_notes_resolve_spec.rb
@@ -0,0 +1,497 @@
+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)
+      end
+
+      it 'does not mark discussion as resolved when resolving single note' do
+        page.within '.diff-content .note' do
+          first('.line-resolve-btn').click
+          sleep 1
+          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.find('.line-resolve-btn').click
+        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..a818679a8748191ff66fec766c853897c1574552
--- /dev/null
+++ b/spec/features/merge_requests/diff_notes_spec.rb
@@ -0,0 +1,207 @@
+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
+
+    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 9e007ab7635f90ede8dd52b60e531c60359ce0dd..c77e719c5df66353b6b534585fc734f2d375dd73 100644
--- a/spec/features/merge_requests/edit_mr_spec.rb
+++ b/spec/features/merge_requests/edit_mr_spec.rb
@@ -14,8 +14,19 @@ feature 'Edit Merge Request', feature: true do
   end
 
   context 'editing a MR' do
-    it 'form should have class js-quick-submit' 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
   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 e3ecd60a5f3e12ebedd772fc315d6d00f49d5ef0..bb0bb590a465c2692a1192119f7e297863470a32 100644
--- a/spec/features/merge_requests/filter_by_milestone_spec.rb
+++ b/spec/features/merge_requests/filter_by_milestone_spec.rb
@@ -21,7 +21,7 @@ feature 'Merge Request filtering by Milestone', feature: true do
   end
 
   context 'filters by upcoming milestone', js: true do
-    it 'should not show issues with no expiry' do
+    it 'does not show issues with no expiry' do
       create(:merge_request, :with_diffs, source_project: project)
       create(:merge_request, :simple, source_project: project, milestone: milestone)
 
@@ -31,7 +31,7 @@ feature 'Merge Request filtering by Milestone', feature: true do
       expect(page).to have_css('.merge-request', count: 0)
     end
 
-    it 'should show issues in future' do
+    it 'shows issues in future' do
       milestone = create(:milestone, project: project, due_date: Date.tomorrow)
       create(:merge_request, :with_diffs, source_project: project)
       create(:merge_request, :simple, source_project: project, milestone: milestone)
@@ -42,7 +42,7 @@ feature 'Merge Request filtering by Milestone', feature: true do
       expect(page).to have_css('.merge-request', count: 1)
     end
 
-    it 'should not show issues in past' do
+    it 'does not show issues in past' do
       milestone = create(:milestone, project: project, due_date: Date.yesterday)
       create(:merge_request, :with_diffs, source_project: project)
       create(:merge_request, :simple, source_project: project, milestone: milestone)
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 96f7b8c993238c902dfb6875016b92835e2b2aed..60bc07bd1a0fa9218a847b4e566e2883a56cb6cd 100644
--- a/spec/features/merge_requests/merge_when_build_succeeds_spec.rb
+++ b/spec/features/merge_requests/merge_when_build_succeeds_spec.rb
@@ -73,7 +73,7 @@ feature 'Merge When Build Succeeds', feature: true, js: true do
   end
 
   context 'Build is not active' do
-    it "should not allow for enabling" do
+    it "does not allow for enabling" do
       visit_merge_request(merge_request)
       expect(page).not_to have_link "Merge When Build Succeeds"
     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/user_uses_slash_commands_spec.rb b/spec/features/merge_requests/user_uses_slash_commands_spec.rb
new file mode 100644
index 0000000000000000000000000000000000000000..d9ef0d180742e5ae0797f6e35f33c1d4455dde76
--- /dev/null
+++ b/spec/features/merge_requests/user_uses_slash_commands_spec.rb
@@ -0,0 +1,32 @@
+require 'rails_helper'
+
+feature 'Merge Requests > User uses slash commands', feature: true, js: true do
+  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 'adding a due date from note' do
+    before do
+      project.team << [user, :master]
+      login_with(user)
+      visit namespace_project_merge_request_path(project.namespace, project, merge_request)
+    end
+
+    it 'does not recognize the command nor create a note' do
+      page.within('.js-main-target-form') do
+        fill_in 'note[note]', with: "/due 2016-08-28"
+        click_button 'Comment'
+      end
+
+      expect(page).not_to have_content '/due 2016-08-28'
+    end
+  end
+end
diff --git a/spec/features/milestone_spec.rb b/spec/features/milestone_spec.rb
index c2c7acff3e8dc7462d08ba4ea9ec551b97b631c7..c43661e56813ae8a9135266d5c6a57abd3578de6 100644
--- a/spec/features/milestone_spec.rb
+++ b/spec/features/milestone_spec.rb
@@ -13,7 +13,7 @@ feature 'Milestone', feature: true do
   end
 
   feature 'Create a milestone' do
-    scenario 'should show an informative message for a new issue' do
+    scenario 'shows an informative message for a new issue' do
       visit new_namespace_project_milestone_path(project.namespace, project)
       page.within '.milestone-form' do
         fill_in "milestone_title", with: '8.7'
@@ -25,7 +25,7 @@ feature 'Milestone', feature: true do
   end
 
   feature 'Open a milestone with closed issues' do
-    scenario 'should show an informative message' do
+    scenario 'shows an informative message' do
       create(:issue, title: "Bugfix1", project: project, milestone: milestone, state: "closed")
       visit namespace_project_milestone_path(project.namespace, project, milestone)
 
diff --git a/spec/features/notes_on_merge_requests_spec.rb b/spec/features/notes_on_merge_requests_spec.rb
index 0b38c413f4401ce5fa4e5275e10575e4f9f3fdad..7a9edbbe33968fda188cd4f9772f80b9013181c2 100644
--- a/spec/features/notes_on_merge_requests_spec.rb
+++ b/spec/features/notes_on_merge_requests_spec.rb
@@ -23,7 +23,7 @@ describe 'Comments', feature: true do
     subject { page }
 
     describe 'the note form' do
-      it 'should be valid' do
+      it 'is valid' do
         is_expected.to have_css('.js-main-target-form', visible: true, count: 1)
         expect(find('.js-main-target-form input[type=submit]').value).
           to eq('Comment')
@@ -39,7 +39,7 @@ describe 'Comments', feature: true do
           end
         end
 
-        it 'should have enable submit button and preview button' do
+        it 'has enable submit button and preview button' do
           page.within('.js-main-target-form') do
             expect(page).not_to have_css('.js-comment-button[disabled]')
             expect(page).to have_css('.js-md-preview-button', visible: true)
@@ -57,7 +57,7 @@ describe 'Comments', feature: true do
         end
       end
 
-      it 'should be added and form reset' do
+      it 'is added and form reset' do
         is_expected.to have_content('This is awsome!')
         page.within('.js-main-target-form') do
           expect(page).to have_no_field('note[note]', with: 'This is awesome!')
@@ -70,7 +70,7 @@ describe 'Comments', feature: true do
     end
 
     describe 'when editing a note', js: true do
-      it 'should contain the hidden edit form' do
+      it 'contains the hidden edit form' do
         page.within("#note_#{note.id}") do
           is_expected.to have_css('.note-edit-form', visible: false)
         end
@@ -82,7 +82,7 @@ describe 'Comments', feature: true do
           find(".js-note-edit").click
         end
 
-        it 'should show the note edit form and hide the note body' do
+        it 'shows the note edit form and hide the note body' do
           page.within("#note_#{note.id}") do
             expect(find('.current-note-edit-form', visible: true)).to be_visible
             expect(find('.note-edit-form', visible: true)).to be_visible
@@ -234,7 +234,7 @@ describe 'Comments', feature: true do
           end
         end
 
-        it 'should be added as discussion' do
+        it 'adds as discussion' do
           is_expected.to have_content('Another comment on line 10')
           is_expected.to have_css('.notes_holder')
           is_expected.to have_css('.notes_holder .note', count: 1)
diff --git a/spec/features/participants_autocomplete_spec.rb b/spec/features/participants_autocomplete_spec.rb
index c7c00a3266a32cfe5bd3e84301e7861b75d1e64f..a78a1c9c8905ca1721c7026df7707a4765961e40 100644
--- a/spec/features/participants_autocomplete_spec.rb
+++ b/spec/features/participants_autocomplete_spec.rb
@@ -12,17 +12,17 @@ feature 'Member autocomplete', feature: true do
   end
 
   shared_examples "open suggestions" do
-    it 'suggestions are displayed' do
+    it 'displays suggestions' do
       expect(page).to have_selector('.atwho-view', visible: true)
     end
 
-    it 'author is suggested' do
+    it 'suggests author' do
       page.within('.atwho-view', visible: true) do
         expect(page).to have_content(author.username)
       end
     end
 
-    it 'participant is suggested' do
+    it 'suggests participant' do
       page.within('.atwho-view', visible: true) do
         expect(page).to have_content(participant.username)
       end
diff --git a/spec/features/profile_spec.rb b/spec/features/profile_spec.rb
index c80253fead8c7dd280eaa2036d22f244c09a0bc8..c3d8c349ca4c1bfeae0526a1a52383814a111932 100644
--- a/spec/features/profile_spec.rb
+++ b/spec/features/profile_spec.rb
@@ -15,7 +15,7 @@ describe 'Profile account page', feature: true do
 
     it { expect(page).to have_content('Remove account') }
 
-    it 'should delete the account' do
+    it 'deletes the account' do
       expect { click_link 'Delete account' }.to change { User.count }.by(-1)
       expect(current_path).to eq(new_user_session_path)
     end
@@ -27,7 +27,7 @@ describe 'Profile account page', feature: true do
       visit profile_account_path
     end
 
-    it 'should not have option to remove account' do
+    it 'does not have option to remove account' do
       expect(page).not_to have_content('Remove account')
       expect(current_path).to eq(profile_account_path)
     end
diff --git a/spec/features/profiles/password_spec.rb b/spec/features/profiles/password_spec.rb
new file mode 100644
index 0000000000000000000000000000000000000000..4cbdd89d46f02830d5cfc8e1ea5248fe373e4973
--- /dev/null
+++ b/spec/features/profiles/password_spec.rb
@@ -0,0 +1,45 @@
+require 'spec_helper'
+
+describe 'Profile > Password', feature: true do
+  let(:user) { create(:user, password_automatically_set: true) }
+
+  before do
+    login_as(user)
+    visit edit_profile_password_path
+  end
+
+  def fill_passwords(password, confirmation)
+    fill_in 'New password',          with: password
+    fill_in 'Password confirmation', with: confirmation
+
+    click_button 'Save password'
+  end
+
+  context 'User with password automatically set' do
+    describe 'User puts different passwords in the field and in the confirmation' do
+      it 'shows an error message' do
+        fill_passwords('mypassword', 'mypassword2')
+
+        page.within('.alert-danger') do
+          expect(page).to have_content("Password confirmation doesn't match Password")
+        end
+      end
+
+      it 'does not contains the current password field after an error' do
+        fill_passwords('mypassword', 'mypassword2')
+
+        expect(page).to have_no_field('user[current_password]')
+      end
+    end
+
+    describe 'User puts the same passwords in the field and in the confirmation' do
+      it 'shows a success message' do
+        fill_passwords('mypassword', 'mypassword')
+
+        page.within('.flash-notice') do
+          expect(page).to have_content('Password was successfully updated. Please login with it')
+        end
+      end
+    end
+  end
+end
diff --git a/spec/features/profiles/preferences_spec.rb b/spec/features/profiles/preferences_spec.rb
index 787bf42d0487ffa8cfa191f93a11bdf959a7b63d..d14a1158b676a27ce9558b793e07590f3d57cab1 100644
--- a/spec/features/profiles/preferences_spec.rb
+++ b/spec/features/profiles/preferences_spec.rb
@@ -68,10 +68,14 @@ describe 'Profile > Preferences', feature: true do
 
       allowing_for_delay do
         find('#logo').click
+
+        expect(page).to have_content("You don't have starred projects yet")
         expect(page.current_path).to eq starred_dashboard_projects_path
       end
 
       click_link 'Your Projects'
+
+      expect(page).not_to have_content("You don't have starred projects yet")
       expect(page.current_path).to eq dashboard_projects_path
     end
   end
diff --git a/spec/features/projects/badges/coverage_spec.rb b/spec/features/projects/badges/coverage_spec.rb
new file mode 100644
index 0000000000000000000000000000000000000000..5972e7f31c2acc92fc979630aa53b1e5aca358fa
--- /dev/null
+++ b/spec/features/projects/badges/coverage_spec.rb
@@ -0,0 +1,82 @@
+require 'spec_helper'
+
+feature 'test coverage badge' do
+  given!(:user) { create(:user) }
+  given!(:project) { create(:project, :private) }
+
+  context 'when user has access to view badge' do
+    background do
+      project.team << [user, :developer]
+      login_as(user)
+    end
+
+    scenario 'user requests coverage badge image for pipeline' do
+      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
+
+      expect_coverage_badge('95%')
+    end
+
+    scenario 'user requests coverage badge for specific job' do
+      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')
+
+      expect_coverage_badge('85%')
+    end
+
+    scenario 'user requests coverage badge for pipeline without coverage' do
+      create_pipeline do |pipeline|
+        create_build(pipeline, coverage: nil, name: 'test')
+      end
+
+      show_test_coverage_badge
+
+      expect_coverage_badge('unknown')
+    end
+  end
+
+  context 'when user does not have access to view badge' do
+    background { login_as(user) }
+
+    scenario 'user requests test coverage badge image' do
+      show_test_coverage_badge
+
+      expect(page).to have_http_status(404)
+    end
+  end
+
+  def create_pipeline
+    opts = { project: project, ref: 'master', sha: project.commit.id }
+
+    create(:ci_pipeline, opts).tap do |pipeline|
+      yield pipeline
+      pipeline.build_updated
+    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)
+    visit coverage_namespace_project_badges_path(
+      project.namespace, project, ref: :master, job: job, format: :svg)
+  end
+
+  def expect_coverage_badge(coverage)
+    svg = Nokogiri::XML.parse(page.body)
+    expect(page.response_headers['Content-Type']).to include('image/svg+xml')
+    expect(svg.at(%Q{text:contains("#{coverage}")})).to be_truthy
+  end
+end
diff --git a/spec/features/projects/badges/list_spec.rb b/spec/features/projects/badges/list_spec.rb
index 75166bca119de2e50785ec53245c1e6dd00964a7..67a4a5d1ab13f5b54f95e94dbd130038ab761067 100644
--- a/spec/features/projects/badges/list_spec.rb
+++ b/spec/features/projects/badges/list_spec.rb
@@ -9,25 +9,43 @@ feature 'list of badges' do
     visit namespace_project_pipelines_settings_path(project.namespace, project)
   end
 
-  scenario 'user displays list of badges' do
-    expect(page).to have_content 'build status'
-    expect(page).to have_content 'Markdown'
-    expect(page).to have_content 'HTML'
-    expect(page).to have_css('.highlight', count: 2)
-    expect(page).to have_xpath("//img[@alt='build status']")
-
-    page.within('.highlight', match: :first) do
-      expect(page).to have_content 'badges/master/build.svg'
+  scenario 'user wants to see build status badge' do
+    page.within('.build-status') do
+      expect(page).to have_content 'build status'
+      expect(page).to have_content 'Markdown'
+      expect(page).to have_content 'HTML'
+      expect(page).to have_css('.highlight', count: 2)
+      expect(page).to have_xpath("//img[@alt='build status']")
+
+      page.within('.highlight', match: :first) do
+        expect(page).to have_content 'badges/master/build.svg'
+      end
     end
   end
 
-  scenario 'user changes current ref on badges list page', js: true do
-    first('.js-project-refs-dropdown').click
+  scenario 'user wants to see coverage report badge' do
+    page.within('.coverage-report') do
+      expect(page).to have_content 'coverage report'
+      expect(page).to have_content 'Markdown'
+      expect(page).to have_content 'HTML'
+      expect(page).to have_css('.highlight', count: 2)
+      expect(page).to have_xpath("//img[@alt='coverage report']")
 
-    page.within '.project-refs-form' do
-      click_link 'improve/awesome'
+      page.within('.highlight', match: :first) do
+        expect(page).to have_content 'badges/master/coverage.svg'
+      end
     end
+  end
+
+  scenario 'user changes current ref of build status badge', js: true do
+    page.within('.build-status') do
+      first('.js-project-refs-dropdown').click
 
-    expect(page).to have_content 'badges/improve/awesome/build.svg'
+      page.within '.project-refs-form' do
+        click_link 'improve/awesome'
+      end
+
+      expect(page).to have_content 'badges/improve/awesome/build.svg'
+    end
   end
 end
diff --git a/spec/features/projects/branches/delete_spec.rb b/spec/features/projects/branches/delete_spec.rb
new file mode 100644
index 0000000000000000000000000000000000000000..63878c554219d5eb218ab36fc08cdefe13c67ef6
--- /dev/null
+++ b/spec/features/projects/branches/delete_spec.rb
@@ -0,0 +1,24 @@
+require 'spec_helper'
+
+feature 'Delete branch', feature: true, js: true do
+  include WaitForAjax
+
+  let(:project) { create(:project) }
+  let(:user) { create(:user) }
+
+  before do
+    project.team << [user, :master]
+    login_as user
+    visit namespace_project_branches_path(project.namespace, project)
+  end
+
+  it 'destroys tooltip' do
+    first('.remove-row').hover
+    expect(page).to have_selector('.tooltip')
+
+    first('.remove-row').click
+    wait_for_ajax
+
+    expect(page).not_to have_selector('.tooltip')
+  end
+end
diff --git a/spec/features/projects/branches_spec.rb b/spec/features/projects/branches_spec.rb
index 79abba21854309b77ba86fadfdc721d4d636e1f0..1b14945bf0a7fbd951f239a05abe3752c3725719 100644
--- a/spec/features/projects/branches_spec.rb
+++ b/spec/features/projects/branches_spec.rb
@@ -20,7 +20,7 @@ describe 'Branches', feature: true do
 
   describe 'Find branches' do
     it 'shows filtered branches', js: true do
-      visit namespace_project_branches_path(project.namespace, project, project.id)
+      visit namespace_project_branches_path(project.namespace, project)
 
       fill_in 'branch-search', with: 'fix'
       find('#branch-search').native.send_keys(:enter)
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/files/editing_a_file_spec.rb b/spec/features/projects/files/editing_a_file_spec.rb
new file mode 100644
index 0000000000000000000000000000000000000000..fe047e00409e2a24ccc58d696a9b20a09d5b3c16
--- /dev/null
+++ b/spec/features/projects/files/editing_a_file_spec.rb
@@ -0,0 +1,34 @@
+require 'spec_helper'
+
+feature 'User wants to edit a file', feature: true do
+  include WaitForAjax
+
+  let(:project) { create(:project) }
+  let(:user) { create(:user) }
+  let(:commit_params) do
+    {
+      source_branch: project.default_branch,
+      target_branch: project.default_branch,
+      commit_message: "Committing First Update",
+      file_path: ".gitignore",
+      file_content: "First Update",
+      last_commit_sha: Gitlab::Git::Commit.last_for_path(project.repository, project.default_branch,
+                                                         ".gitignore").sha
+    }
+  end
+
+  background do
+    project.team << [user, :master]
+    login_as user
+    visit namespace_project_edit_blob_path(project.namespace, project,
+                                           File.join(project.default_branch, '.gitignore'))
+  end
+
+  scenario 'file has been updated since the user opened the edit page' do
+    Files::UpdateService.new(project, user, commit_params).execute
+
+    click_button 'Commit Changes'
+
+    expect(page).to have_content 'Someone edited the file the same time you did.'
+  end
+end
diff --git a/spec/features/projects/files/files_sort_submodules_with_folders_spec.rb b/spec/features/projects/files/files_sort_submodules_with_folders_spec.rb
new file mode 100644
index 0000000000000000000000000000000000000000..10b91d8990b823c076f90b2c67f866ebf8997937
--- /dev/null
+++ b/spec/features/projects/files/files_sort_submodules_with_folders_spec.rb
@@ -0,0 +1,29 @@
+require 'spec_helper'
+
+feature 'User views files page', feature: true do
+  include WaitForAjax
+
+  let(:user) { create(:user) }
+  let(:project) { create(:forked_project_with_submodules) }
+
+  before do
+    project.team << [user, :master]
+    login_as user
+    visit namespace_project_tree_path(project.namespace, project, project.repository.root_ref)
+  end
+
+  scenario 'user sees folders and submodules sorted together, followed by files' do
+    rows = all('td.tree-item-file-name').map(&:text)
+    tree = project.repository.tree
+
+    folders = tree.trees.map(&:name)
+    files = tree.blobs.map(&:name)
+    submodules = tree.submodules.map do |submodule|
+      submodule.name + " @ " + submodule.id[0..7]
+    end
+
+    sorted_titles = (folders + submodules).sort + files
+
+    expect(rows).to eq(sorted_titles)
+  end
+end
diff --git a/spec/features/projects/files/project_owner_creates_license_file_spec.rb b/spec/features/projects/files/project_owner_creates_license_file_spec.rb
index e1e105e6bbea1e7b5c663ae1cb75c7f7241fb987..a521ce50f3574c15d9db39efdda54119df7e01f9 100644
--- a/spec/features/projects/files/project_owner_creates_license_file_spec.rb
+++ b/spec/features/projects/files/project_owner_creates_license_file_spec.rb
@@ -23,7 +23,7 @@ feature 'project owner creates a license file', feature: true, js: true do
 
     select_template('MIT License')
 
-    file_content = find('.file-content')
+    file_content = first('.file-editor')
     expect(file_content).to have_content('The MIT License (MIT)')
     expect(file_content).to have_content("Copyright (c) #{Time.now.year} #{project.namespace.human_name}")
 
@@ -39,6 +39,7 @@ feature 'project owner creates a license file', feature: true, js: true do
   scenario 'project master creates a license file from the "Add license" link' do
     click_link 'Add License'
 
+    expect(page).to have_content('New File')
     expect(current_path).to eq(
       namespace_project_new_blob_path(project.namespace, project, 'master'))
     expect(find('#file_name').value).to eq('LICENSE')
@@ -46,7 +47,7 @@ feature 'project owner creates a license file', feature: true, js: true do
 
     select_template('MIT License')
 
-    file_content = find('.file-content')
+    file_content = first('.file-editor')
     expect(file_content).to have_content('The MIT License (MIT)')
     expect(file_content).to have_content("Copyright (c) #{Time.now.year} #{project.namespace.human_name}")
 
diff --git a/spec/features/projects/files/project_owner_sees_link_to_create_license_file_in_empty_project_spec.rb b/spec/features/projects/files/project_owner_sees_link_to_create_license_file_in_empty_project_spec.rb
index 67aac25e427a7fa71832599fcc8e3d0f9c32b7f0..4453b6d485fef004d9643c74c6514dfd008a7685 100644
--- a/spec/features/projects/files/project_owner_sees_link_to_create_license_file_in_empty_project_spec.rb
+++ b/spec/features/projects/files/project_owner_sees_link_to_create_license_file_in_empty_project_spec.rb
@@ -14,6 +14,7 @@ feature 'project owner sees a link to create a license file in empty project', f
     visit namespace_project_path(project.namespace, project)
     click_link 'Create empty bare repository'
     click_on 'LICENSE'
+    expect(page).to have_content('New File')
 
     expect(current_path).to eq(
       namespace_project_new_blob_path(project.namespace, project, 'master'))
@@ -22,7 +23,7 @@ feature 'project owner sees a link to create a license file in empty project', f
 
     select_template('MIT License')
 
-    file_content = find('.file-content')
+    file_content = first('.file-editor')
     expect(file_content).to have_content('The MIT License (MIT)')
     expect(file_content).to have_content("Copyright (c) #{Time.now.year} #{project.namespace.human_name}")
 
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/import_export/import_file_spec.rb b/spec/features/projects/import_export/import_file_spec.rb
index 7835e1678ad0596a1ed2c60fedada963261df467..f707ccf4e93e0113d153bbcf655db346a34f67f5 100644
--- a/spec/features/projects/import_export/import_file_spec.rb
+++ b/spec/features/projects/import_export/import_file_spec.rb
@@ -3,8 +3,9 @@ require 'spec_helper'
 feature 'project import', feature: true, js: true do
   include Select2Helper
 
-  let(:user) { create(:admin) }
-  let!(:namespace) { create(:namespace, name: "asd", owner: user) }
+  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 }
@@ -12,66 +13,87 @@ feature 'project import', feature: true, js: true do
 
   background do
     allow_any_instance_of(Gitlab::ImportExport).to receive(:storage_path).and_return(export_path)
-    login_as(user)
   end
 
   after(:each) do
     FileUtils.rm_rf(export_path, secure: true)
   end
 
-  scenario 'user imports an exported project successfully' do
-    expect(Project.all.count).to be_zero
+  context 'admin user' do
+    before do
+      login_as(admin)
+    end
 
-    visit new_project_path
+    scenario 'user imports an exported project successfully' do
+      expect(Project.all.count).to be_zero
 
-    select2('2', from: '#project_namespace_id')
-    fill_in :project_path, with: 'test-project-path', visible: true
-    click_link 'GitLab export'
+      visit new_project_path
 
-    expect(page).to have_content('GitLab project export')
-    expect(URI.parse(current_url).query).to eq('namespace_id=2&path=test-project-path')
+      select2('2', from: '#project_namespace_id')
+      fill_in :project_path, with: 'test-project-path', visible: true
+      click_link 'GitLab export'
 
-    attach_file('file', file)
+      expect(page).to have_content('GitLab project export')
+      expect(URI.parse(current_url).query).to eq('namespace_id=2&path=test-project-path')
 
-    click_on 'Import project' # import starts
+      attach_file('file', file)
 
-    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.import_status).to eq('finished')
-  end
+      click_on 'Import project' # import starts
+
+      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.import_status).to eq('finished')
+    end
 
-  scenario 'invalid project' do
-    project = create(:project, namespace_id: 2)
+    scenario 'invalid project' do
+      project = create(:project, namespace_id: 2)
 
-    visit new_project_path
+      visit new_project_path
 
-    select2('2', from: '#project_namespace_id')
-    fill_in :project_path, with: project.name, visible: true
-    click_link 'GitLab export'
+      select2('2', from: '#project_namespace_id')
+      fill_in :project_path, with: project.name, visible: true
+      click_link 'GitLab export'
 
-    attach_file('file', file)
-    click_on 'Import project'
+      attach_file('file', file)
+      click_on 'Import project'
 
-    page.within('.flash-container') do
-      expect(page).to have_content('Project could not be imported')
+      page.within('.flash-container') do
+        expect(page).to have_content('Project could not be imported')
+      end
+    end
+
+    scenario 'project with no name' do
+      create(:project, namespace_id: 2)
+
+      visit new_project_path
+
+      select2('2', from: '#project_namespace_id')
+
+      # click on disabled element
+      find(:link, 'GitLab export').trigger('click')
+
+      page.within('.flash-container') do
+        expect(page).to have_content('Please enter path and name')
+      end
     end
   end
 
-  scenario 'project with no name' do
-    create(:project, namespace_id: 2)
+  context 'normal user' do
+    before do
+      login_as(normal_user)
+    end
 
-    visit new_project_path
+    scenario 'non-admin user is not allowed to import a project' do
+      expect(Project.all.count).to be_zero
 
-    select2('2', from: '#project_namespace_id')
+      visit new_project_path
 
-    # click on disabled element
-    find(:link, 'GitLab export').trigger('click')
+      fill_in :project_path, with: 'test-project-path', visible: true
 
-    page.within('.flash-container') do
-      expect(page).to have_content('Please enter path and name')
+      expect(page).not_to have_content('GitLab export')
     end
   end
 
diff --git a/spec/features/projects/issuable_templates_spec.rb b/spec/features/projects/issuable_templates_spec.rb
new file mode 100644
index 0000000000000000000000000000000000000000..4a83740621a698129f07e2e1f8e7c947c0bebe4c
--- /dev/null
+++ b/spec/features/projects/issuable_templates_spec.rb
@@ -0,0 +1,89 @@
+require 'spec_helper'
+
+feature 'issuable templates', feature: true, js: true do
+  include WaitForAjax
+
+  let(:user) { create(:user) }
+  let(:project) { create(:project, :public) }
+
+  before do
+    project.team << [user, :master]
+    login_as user
+  end
+
+  context 'user creates an issue using templates' do
+    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'
+    end
+
+    scenario 'user selects "bug" template' do
+      select_template 'bug'
+      wait_for_ajax
+      preview_template
+      save_changes
+    end
+  end
+
+  context 'user creates a merge request using templates' do
+    let(:template_content) { 'this is a test "feature-proposal" template' }
+    let(:merge_request) { create(:merge_request, :with_diffs, source_project: project) }
+
+    background do
+      project.repository.commit_file(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
+    end
+  end
+
+  context 'user creates a merge request from a forked project using templates' 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) }
+
+    background do
+      logout
+      project.team << [fork_user, :developer]
+      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
+      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
+    end
+  end
+
+  def preview_template
+    click_link 'Preview'
+    expect(page).to have_content template_content
+  end
+
+  def save_changes
+    click_button "Save changes"
+    expect(page).to have_content template_content
+  end
+
+  def select_template(name)
+    first('.js-issuable-selector').click
+    first('.js-issuable-selector-wrap .dropdown-content 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 98ba93b4036b3ed0d45780f904deb99868ae41d7..cb7495da8ebf4b278dae1dff54547be3ee461753 100644
--- a/spec/features/projects/labels/update_prioritization_spec.rb
+++ b/spec/features/projects/labels/update_prioritization_spec.rb
@@ -87,7 +87,7 @@ feature 'Prioritize labels', feature: true do
   end
 
   context 'as a guest' do
-    it 'can not prioritize labels' do
+    it 'does not prioritize labels' do
       user = create(:user)
       guest = create(:user)
       project = create(:project, name: 'test', namespace: user.namespace)
@@ -102,7 +102,7 @@ feature 'Prioritize labels', feature: true do
   end
 
   context 'as a non signed in user' do
-    it 'can not prioritize labels' do
+    it 'does not prioritize labels' do
       user = create(:user)
       project = create(:project, name: 'test', namespace: user.namespace)
 
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..430c384ac2ee824412a7051d55d80b927af3e6f5
--- /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 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 users 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
+        click_on 'Edit'
+        fill_in 'Access expiration date', with: '2016-08-09'
+        click_on 'Save'
+        expect(page).to have_content('Expires in 3 days')
+      end
+    end
+  end
+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/pipelines_spec.rb b/spec/features/projects/pipelines_spec.rb
similarity index 75%
rename from spec/features/pipelines_spec.rb
rename to spec/features/projects/pipelines_spec.rb
index 377a9aba60d890761e28b5838e8af239280bfcaa..47482bc3cc95e294bea9977ec788f997a0cf7125 100644
--- a/spec/features/pipelines_spec.rb
+++ b/spec/features/projects/pipelines_spec.rb
@@ -12,7 +12,7 @@ describe "Pipelines" do
   end
 
   describe 'GET /:project/pipelines' do
-    let!(:pipeline) { create(:ci_pipeline, project: project, ref: 'master', status: 'running') }
+    let!(:pipeline) { create(:ci_empty_pipeline, project: project, ref: 'master', status: 'running') }
 
     [:all, :running, :branches].each do |scope|
       context "displaying #{scope}" do
@@ -31,9 +31,12 @@ describe "Pipelines" do
     end
 
     context 'cancelable pipeline' do
-      let!(:running) { create(:ci_build, :running, pipeline: pipeline, stage: 'test', commands: 'test') }
+      let!(:build) { create(:ci_build, pipeline: pipeline, stage: 'test', commands: 'test') }
 
-      before { visit namespace_project_pipelines_path(project.namespace, project) }
+      before do
+        build.run
+        visit namespace_project_pipelines_path(project.namespace, project)
+      end
 
       it { expect(page).to have_link('Cancel') }
       it { expect(page).to have_selector('.ci-running') }
@@ -47,9 +50,12 @@ describe "Pipelines" do
     end
 
     context 'retryable pipelines' do
-      let!(:failed) { create(:ci_build, :failed, pipeline: pipeline, stage: 'test', commands: 'test') }
+      let!(:build) { create(:ci_build, pipeline: pipeline, stage: 'test', commands: 'test') }
 
-      before { visit namespace_project_pipelines_path(project.namespace, project) }
+      before do
+        build.drop
+        visit namespace_project_pipelines_path(project.namespace, project)
+      end
 
       it { expect(page).to have_link('Retry') }
       it { expect(page).to have_selector('.ci-failed') }
@@ -58,7 +64,7 @@ describe "Pipelines" do
         before { click_link('Retry') }
 
         it { expect(page).not_to have_link('Retry') }
-        it { expect(page).to have_selector('.ci-pending') }
+        it { expect(page).to have_selector('.ci-running') }
       end
     end
 
@@ -80,27 +86,32 @@ describe "Pipelines" do
       context 'when running' do
         let!(:running) { create(:generic_commit_status, status: 'running', pipeline: pipeline, stage: 'test') }
 
-        before { visit namespace_project_pipelines_path(project.namespace, project) }
+        before do
+          visit namespace_project_pipelines_path(project.namespace, project)
+        end
 
-        it 'not be cancelable' do
+        it 'is not cancelable' do
           expect(page).not_to have_link('Cancel')
         end
 
-        it 'pipeline is running' do
+        it 'has pipeline running' do
           expect(page).to have_selector('.ci-running')
         end
       end
 
       context 'when failed' do
-        let!(:running) { create(:generic_commit_status, status: 'failed', pipeline: pipeline, stage: 'test') }
+        let!(:status) { create(:generic_commit_status, :pending, pipeline: pipeline, stage: 'test') }
 
-        before { visit namespace_project_pipelines_path(project.namespace, project) }
+        before do
+          status.drop
+          visit namespace_project_pipelines_path(project.namespace, project)
+        end
 
-        it 'not be retryable' do
+        it 'is not retryable' do
           expect(page).not_to have_link('Retry')
         end
 
-        it 'pipeline is failed' do
+        it 'has failed pipeline' do
           expect(page).to have_selector('.ci-failed')
         end
       end
@@ -147,7 +158,7 @@ describe "Pipelines" do
 
     before { visit namespace_project_pipeline_path(project.namespace, project, pipeline) }
 
-    it 'showing a list of builds' do
+    it 'shows a list of builds' do
       expect(page).to have_content('Test')
       expect(page).to have_content(@success.id)
       expect(page).to have_content('Deploy')
@@ -182,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
@@ -194,7 +209,7 @@ describe "Pipelines" do
     before { visit new_namespace_project_pipeline_path(project.namespace, project) }
 
     context 'for valid commit' do
-      before { fill_in('Create for', with: 'master') }
+      before { fill_in('pipeline[ref]', with: 'master') }
 
       context 'with gitlab-ci.yml' do
         before { stub_ci_pipeline_to_return_yaml_file }
@@ -211,11 +226,37 @@ describe "Pipelines" do
 
     context 'for invalid commit' do
       before do
-        fill_in('Create for', with: 'invalid reference')
+        fill_in('pipeline[ref]', with: 'invalid-reference')
         click_on 'Create pipeline'
       end
 
       it { expect(page).to have_content('Reference not found') }
     end
   end
+
+  describe 'Create pipelines', feature: true do
+    let(:project) { create(:project) }
+
+    before do
+      visit new_namespace_project_pipeline_path(project.namespace, project)
+    end
+
+    describe 'new pipeline page' do
+      it 'has field to add a new pipeline' do
+        expect(page).to have_field('pipeline[ref]')
+        expect(page).to have_content('Create for')
+      end
+    end
+
+    describe 'find pipelines' do
+      it 'shows filtered pipelines', js: true do
+        fill_in('pipeline[ref]', with: 'fix')
+        find('input#ref').native.send_keys(:keydown)
+
+        within('.ui-autocomplete') do
+          expect(page).to have_selector('li', text: 'fix')
+        end
+      end
+    end
+  end
 end
diff --git a/spec/features/projects/ref_switcher_spec.rb b/spec/features/projects/ref_switcher_spec.rb
new file mode 100644
index 0000000000000000000000000000000000000000..b3ba40b35afc5adc7581fdb924a4434724e74a34
--- /dev/null
+++ b/spec/features/projects/ref_switcher_spec.rb
@@ -0,0 +1,29 @@
+require 'rails_helper'
+
+feature 'Ref switcher', feature: true, js: true do
+  include WaitForAjax
+  let(:user)      { create(:user) }
+  let(:project)   { create(:project, :public) }
+
+  before do
+    project.team << [user, :master]
+    login_as(user)
+    visit namespace_project_tree_path(project.namespace, project, 'master')
+  end
+
+  it 'allow user to change ref by enter key' do
+    click_button 'master'
+    wait_for_ajax
+
+    page.within '.project-refs-form' do
+      input = find('input[type="search"]')
+      input.set 'expand'
+
+      input.native.send_keys :down
+      input.native.send_keys :down
+      input.native.send_keys :enter
+
+      expect(page).to have_content 'expand-collapse-files'
+    end
+  end
+end
diff --git a/spec/features/projects_spec.rb b/spec/features/projects_spec.rb
index 6fa8298d4895b7bd17d34792f4590d50e50ecda9..e00d85904d5da844cd41b7c5730f22ad1293404d 100644
--- a/spec/features/projects_spec.rb
+++ b/spec/features/projects_spec.rb
@@ -44,7 +44,7 @@ feature 'Project', feature: true do
       visit edit_namespace_project_path(project.namespace, project)
     end
 
-    it 'should remove fork' do
+    it 'removes fork' do
       expect(page).to have_content 'Remove fork relationship'
 
       remove_with_confirm('Remove fork relationship', project.path)
@@ -65,7 +65,7 @@ feature 'Project', feature: true do
       visit edit_namespace_project_path(project.namespace, project)
     end
 
-    it 'should remove project' do
+    it 'removes project' do
       expect { remove_with_confirm('Remove project', project.path) }.to change {Project.count}.by(-1)
     end
   end
@@ -82,7 +82,7 @@ feature 'Project', feature: true do
       visit namespace_project_path(project.namespace, project)
     end
 
-    it 'click toggle and show dropdown', js: true do
+    it 'clicks toggle and shows dropdown', js: true do
       find('.js-projects-dropdown-toggle').click
       expect(page).to have_css('.dropdown-menu-projects .dropdown-content li', count: 1)
     end
@@ -102,7 +102,7 @@ feature 'Project', feature: true do
         visit namespace_project_issue_path(project.namespace, project, issue)
       end
 
-      it 'click toggle and show dropdown' do
+      it 'clicks toggle and shows dropdown' do
         find('.js-projects-dropdown-toggle').click
         expect(page).to have_css('.dropdown-menu-projects .dropdown-content li', count: 2)
 
@@ -115,6 +115,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 57734b33a44a81a89efeba35a2311a98b22e054a..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
@@ -11,7 +12,7 @@ feature 'Projected Branches', feature: true, js: true do
   def set_protected_branch_name(branch_name)
     find(".js-protected-branch-select").click
     find(".dropdown-input-field").set(branch_name)
-    click_on "Create Protected Branch: #{branch_name}"
+    click_on("Create wildcard #{branch_name}")
   end
 
   describe "explicit protected branches" do
@@ -71,7 +72,10 @@ feature 'Projected Branches', feature: true, js: true do
       project.repository.add_branch(user, 'production-stable', 'master')
       project.repository.add_branch(user, 'staging-stable', 'master')
       project.repository.add_branch(user, 'development', 'master')
-      create(:protected_branch, project: project, name: "*-stable")
+
+      visit namespace_project_protected_branches_path(project.namespace, project)
+      set_protected_branch_name('*-stable')
+      click_on "Protect"
 
       visit namespace_project_protected_branches_path(project.namespace, project)
       click_on "2 matching branches"
@@ -85,66 +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
-          find(".allowed-to-push").click
-          within(".dropdown.open .dropdown-menu") { click_on access_type_name }
-        end
-        click_on "Protect"
-
-        expect(ProtectedBranch.count).to eq(1)
-        expect(ProtectedBranch.last.push_access_level.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(".allowed-to-push").click
-          within('.dropdown-menu.push') { click_on access_type_name }
-        end
-
-        wait_for_ajax
-        expect(ProtectedBranch.last.push_access_level.access_level).to eq(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
-          find(".allowed-to-merge").click
-          within(".dropdown.open .dropdown-menu") { click_on access_type_name }
-        end
-        click_on "Protect"
-
-        expect(ProtectedBranch.count).to eq(1)
-        expect(ProtectedBranch.last.merge_access_level.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(".allowed-to-merge").click
-          within('.dropdown-menu.merge') { click_on access_type_name }
-        end
-
-        wait_for_ajax
-        expect(ProtectedBranch.last.merge_access_level.access_level).to eq(access_type_id)
-      end
-    end
+    include_examples "protected branches > access control > CE"
   end
 end
diff --git a/spec/features/search_spec.rb b/spec/features/search_spec.rb
index 09f70cd3b002e57db9504b09a3752ab855ad93ba..dcd3a2f17b048b5c0ca66cdc88eaa20df376335c 100644
--- a/spec/features/search_spec.rb
+++ b/spec/features/search_spec.rb
@@ -12,7 +12,7 @@ describe "Search", feature: true  do
     visit search_path
   end
 
-  it 'top right search form is not present' do
+  it 'does not show top right search form' do
     expect(page).not_to have_selector('.search')
   end
 
@@ -71,21 +71,31 @@ describe "Search", feature: true  do
   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)
       end
 
-      it 'top right search form is present' do
+      it 'shows top right search form' do
         expect(page).to have_selector('#search')
       end
 
-      it 'top right search form contains location badge' do
+      it 'contains location badge in top right search form' do
         expect(page).to have_selector('.has-location-badge')
       end
 
       context 'clicking the search field', js: true do
-        it 'should show category search dropdown' do
+        it 'shows category search dropdown' do
           page.find('#search').click
 
           expect(page).to have_selector('.dropdown-header', text: /#{project.name}/i)
@@ -97,7 +107,7 @@ describe "Search", feature: true  do
           page.find('#search').click
         end
 
-        it 'should take user to her issues page when issues assigned is clicked' do
+        it 'takes user to her issues page when issues assigned is clicked' do
           find('.dropdown-menu').click_link 'Issues assigned to me'
           sleep 2
 
@@ -105,7 +115,7 @@ describe "Search", feature: true  do
           expect(find('.js-assignee-search .dropdown-toggle-text')).to have_content(user.name)
         end
 
-        it 'should take user to her issues page when issues authored is clicked' do
+        it 'takes user to her issues page when issues authored is clicked' do
           find('.dropdown-menu').click_link "Issues I've created"
           sleep 2
 
@@ -113,7 +123,7 @@ describe "Search", feature: true  do
           expect(find('.js-author-search .dropdown-toggle-text')).to have_content(user.name)
         end
 
-        it 'should take user to her MR page when MR assigned is clicked' do
+        it 'takes user to her MR page when MR assigned is clicked' do
           find('.dropdown-menu').click_link 'Merge requests assigned to me'
           sleep 2
 
@@ -121,7 +131,7 @@ describe "Search", feature: true  do
           expect(find('.js-assignee-search .dropdown-toggle-text')).to have_content(user.name)
         end
 
-        it 'should take user to her MR page when MR authored is clicked' do
+        it 'takes user to her MR page when MR authored is clicked' do
           find('.dropdown-menu').click_link "Merge requests I've created"
           sleep 2
 
@@ -137,7 +147,7 @@ describe "Search", feature: true  do
           end
         end
 
-        it 'should not display the category search dropdown' do
+        it 'does not display the category search dropdown' do
           expect(page).not_to have_selector('.dropdown-header', text: /#{project.name}/i)
         end
       end
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/todos/todos_sorting_spec.rb b/spec/features/todos/todos_sorting_spec.rb
new file mode 100644
index 0000000000000000000000000000000000000000..e74a51acede8b52e189aab15020a3533e27183d9
--- /dev/null
+++ b/spec/features/todos/todos_sorting_spec.rb
@@ -0,0 +1,67 @@
+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) }
+
+  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
+
+    project.team << [user, :developer]
+    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
diff --git a/spec/features/todos/todos_spec.rb b/spec/features/todos/todos_spec.rb
index 0bdb1628c748509dc22ee4d6a098b61657522edd..0342f4f1d97e31b9dcde976092e1af23354cf940 100644
--- a/spec/features/todos/todos_spec.rb
+++ b/spec/features/todos/todos_spec.rb
@@ -24,7 +24,7 @@ describe 'Dashboard Todos', feature: true do
         visit dashboard_todos_path
       end
 
-      it 'todo is present' do
+      it 'has todo present' do
         expect(page).to have_selector('.todos-list .todo', count: 1)
       end
 
diff --git a/spec/features/u2f_spec.rb b/spec/features/u2f_spec.rb
index 9335f5bf120b7c5362560873ed9021b9440d63f8..a46e48c76ed6eb69bf122edd06cb5492a31c2edb 100644
--- a/spec/features/u2f_spec.rb
+++ b/spec/features/u2f_spec.rb
@@ -1,13 +1,23 @@
 require 'spec_helper'
 
 feature 'Using U2F (Universal 2nd Factor) Devices for Authentication', feature: true, js: true do
+  include WaitForAjax
+
   before { allow_any_instance_of(U2fHelper).to receive(:inject_u2f_api?).and_return(true) }
 
+  def manage_two_factor_authentication
+    click_on 'Manage Two-Factor Authentication'
+    expect(page).to have_content("Setup New U2F Device")
+    wait_for_ajax
+  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
@@ -32,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
-        click_on 'Manage Two-Factor Authentication'
+        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
 
@@ -46,23 +57,39 @@ feature 'Using U2F (Universal 2nd Factor) Devices for Authentication', feature:
         visit profile_account_path
 
         # First device
-        click_on 'Manage Two-Factor Authentication'
-        register_u2f_device
+        manage_two_factor_authentication
+        first_device = register_u2f_device
         expect(page.body).to match('Your U2F device was registered')
 
         # Second device
-        click_on 'Manage Two-Factor Authentication'
-        register_u2f_device
+        second_device = register_u2f_device
         expect(page.body).to match('Your U2F device was registered')
-        click_on 'Manage Two-Factor Authentication'
-        expect(page.body).to match('You have 2 U2F devices 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'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
 
     it 'allows the same device to be registered for multiple users' do
       # First user
       visit profile_account_path
-      click_on 'Manage Two-Factor Authentication'
+      manage_two_factor_authentication
       u2f_device = register_u2f_device
       expect(page.body).to match('Your U2F device was registered')
       logout
@@ -71,7 +98,7 @@ feature 'Using U2F (Universal 2nd Factor) Devices for Authentication', feature:
       user = login_as(:user)
       user.update_attribute(:otp_required_for_login, true)
       visit profile_account_path
-      click_on 'Manage Two-Factor Authentication'
+      manage_two_factor_authentication
       register_u2f_device(u2f_device)
       expect(page.body).to match('Your U2F device was registered')
 
@@ -81,7 +108,7 @@ feature 'Using U2F (Universal 2nd Factor) Devices for Authentication', feature:
     context "when there are form errors" do
       it "doesn't register the device if there are errors" do
         visit profile_account_path
-        click_on 'Manage Two-Factor Authentication'
+        manage_two_factor_authentication
 
         # Have the "u2f device" respond with bad data
         page.execute_script("u2f.register = function(_,_,_,callback) { callback('bad response'); };")
@@ -96,7 +123,7 @@ feature 'Using U2F (Universal 2nd Factor) Devices for Authentication', feature:
 
       it "allows retrying registration" do
         visit profile_account_path
-        click_on 'Manage Two-Factor Authentication'
+        manage_two_factor_authentication
 
         # Failed registration
         page.execute_script("u2f.register = function(_,_,_,callback) { callback('bad response'); };")
@@ -122,7 +149,7 @@ feature 'Using U2F (Universal 2nd Factor) Devices for Authentication', feature:
       login_as(user)
       user.update_attribute(:otp_required_for_login, true)
       visit profile_account_path
-      click_on 'Manage Two-Factor Authentication'
+      manage_two_factor_authentication
       @u2f_device = register_u2f_device
       logout
     end
@@ -161,7 +188,7 @@ feature 'Using U2F (Universal 2nd Factor) Devices for Authentication', feature:
           current_user = login_as(:user)
           current_user.update_attribute(:otp_required_for_login, true)
           visit profile_account_path
-          click_on 'Manage Two-Factor Authentication'
+          manage_two_factor_authentication
           register_u2f_device
           logout
 
@@ -182,7 +209,7 @@ feature 'Using U2F (Universal 2nd Factor) Devices for Authentication', feature:
           current_user = login_as(:user)
           current_user.update_attribute(:otp_required_for_login, true)
           visit profile_account_path
-          click_on 'Manage Two-Factor Authentication'
+          manage_two_factor_authentication
           register_u2f_device(@u2f_device)
           logout
 
@@ -200,7 +227,7 @@ 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"
@@ -248,12 +275,13 @@ feature 'Using U2F (Universal 2nd Factor) Devices for Authentication', feature:
         user = login_as(:user)
         user.update_attribute(:otp_required_for_login, true)
         visit profile_account_path
-        click_on 'Manage Two-Factor Authentication'
+        manage_two_factor_authentication
         expect(page).to have_content("Your U2F device needs to be set up.")
         register_u2f_device
       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/variables_spec.rb b/spec/features/variables_spec.rb
index a2b8f7b6931ffb5c9786a48b37aecc633b4ed09e..d7880d5778f27d33b94f47606374e6055a242924 100644
--- a/spec/features/variables_spec.rb
+++ b/spec/features/variables_spec.rb
@@ -13,13 +13,13 @@ describe 'Project variables', js: true do
     visit namespace_project_variables_path(project.namespace, project)
   end
 
-  it 'should show list of variables' do
+  it 'shows list of variables' do
     page.within('.variables-table') do
       expect(page).to have_content(variable.key)
     end
   end
 
-  it 'should add new variable' do
+  it 'adds new variable' do
     fill_in('variable_key', with: 'key')
     fill_in('variable_value', with: 'key value')
     click_button('Add new variable')
@@ -29,7 +29,7 @@ describe 'Project variables', js: true do
     end
   end
 
-  it 'should delete variable' do
+  it 'deletes variable' do
     page.within('.variables-table') do
       find('.btn-variable-delete').click
     end
@@ -37,11 +37,12 @@ describe 'Project variables', js: true do
     expect(page).not_to have_selector('variables-table')
   end
 
-  it 'should edit variable' do
+  it 'edits variable' do
     page.within('.variables-table') do
       find('.btn-variable-edit').click
     end
 
+    expect(page).to have_content('Update variable')
     fill_in('variable_key', with: 'key')
     fill_in('variable_value', with: 'key value')
     click_button('Save variable')
diff --git a/spec/finders/branches_finder_spec.rb b/spec/finders/branches_finder_spec.rb
index 482caeee64a7fe4a29497594953cbda0216cc2d7..6fce11de30fb32369557241ee43e6f32f4ba1055 100644
--- a/spec/finders/branches_finder_spec.rb
+++ b/spec/finders/branches_finder_spec.rb
@@ -20,7 +20,11 @@ describe BranchesFinder do
 
         result = branches_finder.execute
 
-        expect(result.first.name).to eq('crlf-diff')
+        recently_updated_branch = repository.branches.max do |a, b|
+          repository.commit(a.target).committed_date <=> repository.commit(b.target).committed_date
+        end
+
+        expect(result.first.name).to eq(recently_updated_branch.name)
       end
 
       it 'sorts by last_updated' do
diff --git a/spec/finders/merge_requests_finder_spec.rb b/spec/finders/merge_requests_finder_spec.rb
index bc385fd0d691c68820214a50d0c9ec293d46f9c8..535aabfc18d5fdf5712995428dbed844f507097a 100644
--- a/spec/finders/merge_requests_finder_spec.rb
+++ b/spec/finders/merge_requests_finder_spec.rb
@@ -18,13 +18,13 @@ describe MergeRequestsFinder do
   end
 
   describe "#execute" do
-    it 'should filter by scope' do
+    it 'filters by scope' do
       params = { scope: 'authored', state: 'opened' }
       merge_requests = MergeRequestsFinder.new(user, params).execute
       expect(merge_requests.size).to eq(2)
     end
 
-    it 'should filter by project' do
+    it 'filters by project' do
       params = { project_id: project1.id, scope: 'authored', state: 'opened' }
       merge_requests = MergeRequestsFinder.new(user, params).execute
       expect(merge_requests.size).to eq(1)
diff --git a/spec/finders/move_to_project_finder_spec.rb b/spec/finders/move_to_project_finder_spec.rb
new file mode 100644
index 0000000000000000000000000000000000000000..fdce4e714ffdaf9a90eacd3ae6a9d936b7eeda16
--- /dev/null
+++ b/spec/finders/move_to_project_finder_spec.rb
@@ -0,0 +1,97 @@
+require 'spec_helper'
+
+describe MoveToProjectFinder do
+  let(:user) { create(:user) }
+  let(:project) { create(:project) }
+
+  let(:no_access_project) { create(:project) }
+  let(:guest_project) { create(:project) }
+  let(:reporter_project) { create(:project) }
+  let(:developer_project) { create(:project) }
+  let(:master_project) { create(:project) }
+
+  subject { described_class.new(user) }
+
+  describe '#execute' do
+    context 'filter' do
+      it 'does not return projects under Gitlab::Access::REPORTER' do
+        guest_project.team << [user, :guest]
+
+        expect(subject.execute(project)).to be_empty
+      end
+
+      it 'returns projects equal or above Gitlab::Access::REPORTER ordered by id in descending order' do
+        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, reporter_project])
+      end
+
+      it 'does not include the source project' do
+        project.team << [user, :reporter]
+
+        expect(subject.execute(project).to_a).to be_empty
+      end
+
+      it 'does not return archived projects' do
+        reporter_project.team << [user, :reporter]
+        reporter_project.update_attributes(archived: true)
+        other_reporter_project = create(:project)
+        other_reporter_project.team << [user, :reporter]
+
+        expect(subject.execute(project).to_a).to eq([other_reporter_project])
+      end
+
+      it 'does not return projects for which issues are disabled' do
+        reporter_project.team << [user, :reporter]
+        reporter_project.update_attributes(issues_enabled: false)
+        other_reporter_project = create(:project)
+        other_reporter_project.team << [user, :reporter]
+
+        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
+      it 'uses Project#search' do
+        expect(user).to receive_message_chain(:projects_where_can_admin_issues, :search) { Project.all }
+
+        subject.execute(project, search: 'wadus')
+      end
+
+      it 'returns projects matching a search query' do
+        foo_project = create(:project)
+        foo_project.team << [user, :master]
+
+        wadus_project = create(:project, name: 'wadus')
+        wadus_project.team << [user, :master]
+
+        expect(subject.execute(project).to_a).to eq([wadus_project, foo_project])
+        expect(subject.execute(project, search: 'wadus').to_a).to eq([wadus_project])
+      end
+    end
+  end
+end
diff --git a/spec/finders/notes_finder_spec.rb b/spec/finders/notes_finder_spec.rb
index 8db897b16466bfe3f61d622f50f108a8ac7a0e38..7c6860372cc2c1041e61b2f78049a90ac6e3c11b 100644
--- a/spec/finders/notes_finder_spec.rb
+++ b/spec/finders/notes_finder_spec.rb
@@ -19,12 +19,12 @@ describe NotesFinder do
       note2
     end
 
-    it 'should find all notes' do
+    it 'finds all notes' do
       notes = NotesFinder.new.execute(project, user, params)
       expect(notes.size).to eq(2)
     end
 
-    it 'should raise an exception for an invalid target_type' do
+    it 'raises an exception for an invalid target_type' do
       params.merge!(target_type: 'invalid')
       expect { NotesFinder.new.execute(project, user, params) }.to raise_error('invalid target_type')
     end
diff --git a/spec/finders/projects_finder_spec.rb b/spec/finders/projects_finder_spec.rb
index 0a1cc3b3df7dedc6601ae75c1f48da9162b02a6b..7a3a74335e899a0c2dcef9506c2b418970910ec0 100644
--- a/spec/finders/projects_finder_spec.rb
+++ b/spec/finders/projects_finder_spec.rb
@@ -23,73 +23,36 @@ describe ProjectsFinder do
 
     let(:finder) { described_class.new }
 
-    describe 'without a group' do
-      describe 'without a user' do
-        subject { finder.execute }
+    describe 'without a user' do
+      subject { finder.execute }
 
-        it { is_expected.to eq([public_project]) }
-      end
-
-      describe 'with a user' do
-        subject { finder.execute(user) }
-
-        describe 'without private projects' do
-          it { is_expected.to eq([public_project, internal_project]) }
-        end
-
-        describe 'with private projects' do
-          before do
-            private_project.team.add_user(user, Gitlab::Access::MASTER)
-          end
-
-          it do
-            is_expected.to eq([public_project, internal_project,
-                               private_project])
-          end
-        end
-      end
+      it { is_expected.to eq([public_project]) }
     end
 
-    describe 'with a group' do
-      describe 'without a user' do
-        subject { finder.execute(nil, group: group) }
+    describe 'with a user' do
+      subject { finder.execute(user) }
 
-        it { is_expected.to eq([public_project]) }
+      describe 'without private projects' do
+        it { is_expected.to eq([public_project, internal_project]) }
       end
 
-      describe 'with a user' do
-        subject { finder.execute(user, group: group) }
-
-        describe 'without shared projects' do
-          it { is_expected.to eq([public_project, internal_project]) }
+      describe 'with private projects' do
+        before do
+          private_project.team.add_user(user, Gitlab::Access::MASTER)
         end
 
-        describe 'with shared projects and group membership' do
-          before do
-            group.add_user(user, Gitlab::Access::DEVELOPER)
-
-            shared_project.project_group_links.
-              create(group_access: Gitlab::Access::MASTER, group: group)
-          end
-
-          it do
-            is_expected.to eq([shared_project, public_project, internal_project])
-          end
+        it do
+          is_expected.to eq([public_project, internal_project, private_project])
         end
+      end
+    end
 
-        describe 'with shared projects and project membership' do
-          before do
-            shared_project.team.add_user(user, Gitlab::Access::DEVELOPER)
+    describe 'with project_ids_relation' do
+      let(:project_ids_relation) { Project.where(id: internal_project.id) }
 
-            shared_project.project_group_links.
-              create(group_access: Gitlab::Access::MASTER, group: group)
-          end
+      subject { finder.execute(user, project_ids_relation) }
 
-          it do
-            is_expected.to eq([shared_project, public_project, internal_project])
-          end
-        end
-      end
+      it { is_expected.to eq([internal_project]) }
     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/fixtures/api/schemas/issue.json b/spec/fixtures/api/schemas/issue.json
new file mode 100644
index 0000000000000000000000000000000000000000..532ebb9640e86aa0e6cc9b47d1ae6676fe6225a7
--- /dev/null
+++ b/spec/fixtures/api/schemas/issue.json
@@ -0,0 +1,48 @@
+{
+  "type": "object",
+  "required" : [
+    "iid",
+    "title",
+    "confidential"
+  ],
+  "properties" : {
+    "iid": { "type": "integer" },
+    "title": { "type": "string" },
+    "confidential": { "type": "boolean" },
+    "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" }
+    }
+  },
+  "additionalProperties": false
+}
diff --git a/spec/fixtures/api/schemas/issues.json b/spec/fixtures/api/schemas/issues.json
new file mode 100644
index 0000000000000000000000000000000000000000..0d2067f704a1877ac765d2fc273e0c9c8a7089c2
--- /dev/null
+++ b/spec/fixtures/api/schemas/issues.json
@@ -0,0 +1,4 @@
+{
+  "type": "array",
+  "items": { "$ref": "issue.json" }
+}
diff --git a/spec/fixtures/api/schemas/list.json b/spec/fixtures/api/schemas/list.json
new file mode 100644
index 0000000000000000000000000000000000000000..f070fa3b254f150095e6e58361bf02a5c9a8ad3c
--- /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"],
+      "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/config/redis_new_format_host.yml b/spec/fixtures/config/redis_new_format_host.yml
new file mode 100644
index 0000000000000000000000000000000000000000..13772677a45c2e837656e065ca272baed8858aac
--- /dev/null
+++ b/spec/fixtures/config/redis_new_format_host.yml
@@ -0,0 +1,29 @@
+# redis://[:password@]host[:port][/db-number][?option=value]
+# more details: http://www.iana.org/assignments/uri-schemes/prov/redis
+development:
+  url: redis://:mynewpassword@localhost:6379/99
+  sentinels:
+    -
+      host: localhost
+      port: 26380 # point to sentinel, not to redis port
+    -
+      host: slave2
+      port: 26381 # point to sentinel, not to redis port
+test:
+  url: redis://:mynewpassword@localhost:6379/99
+  sentinels:
+    -
+      host: localhost
+      port: 26380 # point to sentinel, not to redis port
+    -
+      host: slave2
+      port: 26381 # point to sentinel, not to redis port
+production:
+  url: redis://:mynewpassword@localhost:6379/99
+  sentinels:
+    -
+      host: slave1
+      port: 26380 # point to sentinel, not to redis port
+    -
+      host: slave2
+      port: 26381 # point to sentinel, not to redis port
diff --git a/spec/fixtures/config/redis_new_format_socket.yml b/spec/fixtures/config/redis_new_format_socket.yml
new file mode 100644
index 0000000000000000000000000000000000000000..4e76830c281de7a02d15d74e913ec4c5fb998b7d
--- /dev/null
+++ b/spec/fixtures/config/redis_new_format_socket.yml
@@ -0,0 +1,6 @@
+development:
+  url: unix:/path/to/redis.sock
+test:
+  url: unix:/path/to/redis.sock
+production:
+  url: unix:/path/to/redis.sock
diff --git a/spec/fixtures/config/redis_old_format_host.yml b/spec/fixtures/config/redis_old_format_host.yml
new file mode 100644
index 0000000000000000000000000000000000000000..253d0a994f5e6f12e515876051d4388e5d300917
--- /dev/null
+++ b/spec/fixtures/config/redis_old_format_host.yml
@@ -0,0 +1,5 @@
+# redis://[:password@]host[:port][/db-number][?option=value]
+# more details: http://www.iana.org/assignments/uri-schemes/prov/redis
+development: redis://:mypassword@localhost:6379/99
+test: redis://:mypassword@localhost:6379/99
+production: redis://:mypassword@localhost:6379/99
diff --git a/spec/fixtures/config/redis_old_format_socket.yml b/spec/fixtures/config/redis_old_format_socket.yml
new file mode 100644
index 0000000000000000000000000000000000000000..fd31ce8ea3d77ff714ccfd3324e4d883ee6501a7
--- /dev/null
+++ b/spec/fixtures/config/redis_old_format_socket.yml
@@ -0,0 +1,3 @@
+development: unix:/path/to/old/redis.sock
+test: unix:/path/to/old/redis.sock
+production: unix:/path/to/old/redis.sock
diff --git a/spec/fixtures/emails/commands_in_reply.eml b/spec/fixtures/emails/commands_in_reply.eml
new file mode 100644
index 0000000000000000000000000000000000000000..06bf60ab734823074dc90bd071e132fc8a5b9e5a
--- /dev/null
+++ b/spec/fixtures/emails/commands_in_reply.eml
@@ -0,0 +1,43 @@
+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
+/due tomorrow
+
+
+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..aed64224b06e61c95a88a1b57026bc1ab3e08a6b
--- /dev/null
+++ b/spec/fixtures/emails/commands_only_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
+
+/close
+/todo
+/due tomorrow
+
+
+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/project_services/campfire/rooms.json b/spec/fixtures/project_services/campfire/rooms.json
new file mode 100644
index 0000000000000000000000000000000000000000..71e9645c955f8838b189bdc836fdbd332793e618
--- /dev/null
+++ b/spec/fixtures/project_services/campfire/rooms.json
@@ -0,0 +1,22 @@
+{
+  "rooms": [
+    {
+      "name": "test-room",
+      "locked": false,
+      "created_at": "2009/01/07 20:43:11 +0000",
+      "updated_at": "2009/03/18 14:31:39 +0000",
+      "topic": "The room topic\n",
+      "id": 123,
+      "membership_limit": 4
+    },
+    {
+      "name": "another room",
+      "locked": true,
+      "created_at": "2009/03/18 14:30:42 +0000",
+      "updated_at": "2013/01/27 14:14:27 +0000",
+      "topic": "Comment, ideas, GitHub notifications for eCommittee App",
+      "id": 456,
+      "membership_limit": 4
+    }
+  ]
+}
diff --git a/spec/fixtures/project_services/campfire/rooms2.json b/spec/fixtures/project_services/campfire/rooms2.json
new file mode 100644
index 0000000000000000000000000000000000000000..3d5f635d8b39a8d88dc0764ba85259604bc0c41a
--- /dev/null
+++ b/spec/fixtures/project_services/campfire/rooms2.json
@@ -0,0 +1,22 @@
+{
+  "rooms": [
+    {
+      "name": "test-room-not-found",
+      "locked": false,
+      "created_at": "2009/01/07 20:43:11 +0000",
+      "updated_at": "2009/03/18 14:31:39 +0000",
+      "topic": "The room topic\n",
+      "id": 123,
+      "membership_limit": 4
+    },
+    {
+      "name": "another room",
+      "locked": true,
+      "created_at": "2009/03/18 14:30:42 +0000",
+      "updated_at": "2013/01/27 14:14:27 +0000",
+      "topic": "Comment, ideas, GitHub notifications for eCommittee App",
+      "id": 456,
+      "membership_limit": 4
+    }
+  ]
+}
diff --git a/spec/helpers/application_helper_spec.rb b/spec/helpers/application_helper_spec.rb
index bb28866f01009fca5dc0e0e6a081f6261750c215..73f5470cf358bb276f19ebee38b9aa11ba91686b 100644
--- a/spec/helpers/application_helper_spec.rb
+++ b/spec/helpers/application_helper_spec.rb
@@ -54,7 +54,7 @@ describe ApplicationHelper do
   describe 'project_icon' do
     let(:avatar_file_path) { File.join(Rails.root, 'spec', 'fixtures', 'banana_sample.gif') }
 
-    it 'should return an url for the avatar' do
+    it 'returns an url for the avatar' do
       project = create(:project, avatar: File.open(avatar_file_path))
 
       avatar_url = "http://localhost/uploads/project/avatar/#{project.id}/banana_sample.gif"
@@ -62,7 +62,7 @@ describe ApplicationHelper do
         to eq "<img src=\"#{avatar_url}\" alt=\"Banana sample\" />"
     end
 
-    it 'should give uploaded icon when present' do
+    it 'gives uploaded icon when present' do
       project = create(:project)
 
       allow_any_instance_of(Project).to receive(:avatar_in_git).and_return(true)
@@ -76,14 +76,14 @@ describe ApplicationHelper do
   describe 'avatar_icon' do
     let(:avatar_file_path) { File.join(Rails.root, 'spec', 'fixtures', 'banana_sample.gif') }
 
-    it 'should return an url for the avatar' do
+    it 'returns an url for the avatar' do
       user = create(:user, avatar: File.open(avatar_file_path))
 
       expect(helper.avatar_icon(user.email).to_s).
         to match("/uploads/user/avatar/#{user.id}/banana_sample.gif")
     end
 
-    it 'should return an url for the avatar with relative url' do
+    it 'returns an url for the avatar with relative url' do
       stub_config_setting(relative_url_root: '/gitlab')
       # Must be stubbed after the stub above, and separately
       stub_config_setting(url: Settings.send(:build_gitlab_url))
@@ -94,14 +94,14 @@ describe ApplicationHelper do
         to match("/gitlab/uploads/user/avatar/#{user.id}/banana_sample.gif")
     end
 
-    it 'should call gravatar_icon when no User exists with the given email' do
+    it 'calls gravatar_icon when no User exists with the given email' do
       expect(helper).to receive(:gravatar_icon).with('foo@example.com', 20, 2)
 
       helper.avatar_icon('foo@example.com', 20, 2)
     end
 
     describe 'using a User' do
-      it 'should return an URL for the avatar' do
+      it 'returns an URL for the avatar' do
         user = create(:user, avatar: File.open(avatar_file_path))
 
         expect(helper.avatar_icon(user).to_s).
@@ -146,7 +146,7 @@ describe ApplicationHelper do
           to match('https://secure.gravatar.com')
       end
 
-      it 'should return custom gravatar path when gravatar_url is set' do
+      it 'returns custom gravatar path when gravatar_url is set' do
         stub_gravatar_setting(plain_url: 'http://example.local/?s=%{size}&hash=%{hash}')
 
         expect(gravatar_icon(user_email, 20)).
@@ -218,12 +218,12 @@ describe ApplicationHelper do
     end
 
     it 'includes a default js-timeago class' do
-      expect(element.attr('class')).to eq 'time_ago js-timeago js-timeago-pending'
+      expect(element.attr('class')).to eq 'js-timeago js-timeago-pending'
     end
 
     it 'accepts a custom html_class' do
       expect(element(html_class: 'custom_class').attr('class')).
-        to eq 'custom_class js-timeago js-timeago-pending'
+        to eq 'js-timeago custom_class js-timeago-pending'
     end
 
     it 'accepts a custom tooltip placement' do
@@ -244,6 +244,19 @@ describe ApplicationHelper do
     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
+      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
   end
 
   describe 'render_markup' do
@@ -253,19 +266,19 @@ describe ApplicationHelper do
       allow(helper).to receive(:current_user).and_return(user)
     end
 
-    it 'should preserve encoding' do
+    it 'preserves encoding' do
       expect(content.encoding.name).to eq('UTF-8')
       expect(helper.render_markup('foo.rst', content).encoding.name).to eq('UTF-8')
     end
 
-    it "should delegate to #markdown when file name corresponds to Markdown" do
+    it "delegates to #markdown when file name corresponds to Markdown" do
       expect(helper).to receive(:gitlab_markdown?).with('foo.md').and_return(true)
       expect(helper).to receive(:markdown).and_return('NOEL')
 
       expect(helper.render_markup('foo.md', content)).to eq('NOEL')
     end
 
-    it "should delegate to #asciidoc when file name corresponds to AsciiDoc" do
+    it "delegates to #asciidoc when file name corresponds to AsciiDoc" do
       expect(helper).to receive(:asciidoc?).with('foo.adoc').and_return(true)
       expect(helper).to receive(:asciidoc).and_return('NOEL')
 
diff --git a/spec/helpers/blob_helper_spec.rb b/spec/helpers/blob_helper_spec.rb
index b2d6d59b1ee0c467bb01c143c777e80dbb278398..a43a7238c708b1ac9a5ba87d02e6e9ba5f61602a 100644
--- a/spec/helpers/blob_helper_spec.rb
+++ b/spec/helpers/blob_helper_spec.rb
@@ -17,19 +17,19 @@ describe BlobHelper do
   end
 
   describe '#highlight' do
-    it 'should return plaintext for unknown lexer context' do
+    it 'returns plaintext for unknown lexer context' do
       result = helper.highlight(blob_name, no_context_content)
       expect(result).to eq(%[<pre class="code highlight"><code><span id="LC1" class="line">:type "assem"))</span></code></pre>])
     end
 
-    it 'should highlight single block' do
+    it 'highlights single block' do
       expected = %Q[<pre class="code highlight"><code><span id="LC1" class="line"><span class="p">(</span><span class="nb">make-pathname</span> <span class="ss">:defaults</span> <span class="nv">name</span></span>
 <span id="LC2" class="line"><span class="ss">:type</span> <span class="s">"assem"</span><span class="p">))</span></span></code></pre>]
 
       expect(helper.highlight(blob_name, blob_content)).to eq(expected)
     end
 
-    it 'should highlight multi-line comments' do
+    it 'highlights multi-line comments' do
       result = helper.highlight(blob_name, multiline_content)
       html = Nokogiri::HTML(result)
       lines = html.search('.s')
@@ -49,7 +49,7 @@ describe BlobHelper do
 <span id="LC4" class="line"> ddd</span></code></pre>)
       end
 
-      it 'should highlight each line properly' do
+      it 'highlights each line properly' do
         result = helper.highlight(blob_name, blob_content)
         expect(result).to eq(expected)
       end
@@ -62,25 +62,47 @@ describe BlobHelper do
     let(:expected_svg_path) { File.join(Rails.root, 'spec', 'fixtures', 'sanitized.svg') }
     let(:expected) { open(expected_svg_path).read }
 
-    it 'should retain essential elements' do
+    it 'retains essential elements' do
       blob = OpenStruct.new(data: data)
       expect(sanitize_svg(blob).data).to eq(expected)
     end
   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/diff_helper_spec.rb b/spec/helpers/diff_helper_spec.rb
index c2fd2c8a5336e43eebda2225418195b2f6ae8ab2..9c7c79f57c6310e3e4e1338c67a87bf6569871e9 100644
--- a/spec/helpers/diff_helper_spec.rb
+++ b/spec/helpers/diff_helper_spec.rb
@@ -6,7 +6,7 @@ describe DiffHelper do
   let(:project) { create(:project) }
   let(:repository) { project.repository }
   let(:commit) { project.commit(sample_commit.id) }
-  let(:diffs) { commit.diffs }
+  let(:diffs) { commit.raw_diffs }
   let(:diff) { diffs.first }
   let(:diff_refs) { [commit.parent, commit] }
   let(:diff_file) { Gitlab::Diff::File.new(diff, diff_refs: diff_refs, repository: repository) }
@@ -15,74 +15,56 @@ describe DiffHelper do
     it 'returns a valid value when cookie is set' do
       helper.request.cookies[:diff_view] = 'parallel'
 
-      expect(helper.diff_view).to eq 'parallel'
+      expect(helper.diff_view).to eq :parallel
     end
 
     it 'returns a default value when cookie is invalid' do
       helper.request.cookies[:diff_view] = 'invalid'
 
-      expect(helper.diff_view).to eq 'inline'
+      expect(helper.diff_view).to eq :inline
     end
 
     it 'returns a default value when cookie is nil' do
       expect(helper.request.cookies).to be_empty
 
-      expect(helper.diff_view).to eq 'inline'
+      expect(helper.diff_view).to eq :inline
     end
   end
-  
-  describe 'diff_options' do
-    it 'should return hard limit for a diff if force diff is true' do
-      allow(controller).to receive(:params) { { force_show_diff: true } }
-      expect(diff_options).to include(Commit.max_diff_options)
-    end
-
-    it 'should return hard limit for a diff if expand_all_diffs is true' do
-      allow(controller).to receive(:params) { { expand_all_diffs: true } }
-      expect(diff_options).to include(Commit.max_diff_options)
-    end
 
-    it 'should return no collapse false' do
+  describe 'diff_options' do
+    it 'returns no collapse false' do
       expect(diff_options).to include(no_collapse: false)
     end
 
-    it 'should return no collapse true if expand_all_diffs' do
+    it 'returns no collapse true if expand_all_diffs' do
       allow(controller).to receive(:params) { { expand_all_diffs: true } }
       expect(diff_options).to include(no_collapse: true)
     end
 
-    it 'should return no collapse true if action name diff_for_path' do
+    it 'returns no collapse true if action name diff_for_path' do
       allow(controller).to receive(:action_name) { 'diff_for_path' }
       expect(diff_options).to include(no_collapse: true)
     end
-  end
-
-  describe 'unfold_bottom_class' do
-    it 'should return empty string when bottom line shouldnt be unfolded' do
-      expect(unfold_bottom_class(false)).to eq('')
-    end
-
-    it 'should return js class when bottom lines should be unfolded' do
-      expect(unfold_bottom_class(true)).to include('js-unfold-bottom')
-    end
-  end
 
-  describe 'unfold_class' do
-    it 'returns empty on false' do
-      expect(unfold_class(false)).to eq('')
+    it 'returns paths if action name diff_for_path and param old path' do
+      allow(controller).to receive(:params) { { old_path: 'lib/wadus.rb' } }
+      allow(controller).to receive(:action_name) { 'diff_for_path' }
+      expect(diff_options[:paths]).to include('lib/wadus.rb')
     end
 
-    it 'returns a class on true' do
-      expect(unfold_class(true)).to eq('unfold js-unfold')
+    it 'returns paths if action name diff_for_path and param new path' do
+      allow(controller).to receive(:params) { { new_path: 'lib/wadus.rb' } }
+      allow(controller).to receive(:action_name) { 'diff_for_path' }
+      expect(diff_options[:paths]).to include('lib/wadus.rb')
     end
   end
 
   describe '#diff_line_content' do
-    it 'should return non breaking space when line is empty' do
+    it 'returns non breaking space when line is empty' do
       expect(diff_line_content(nil)).to eq(' &nbsp;')
     end
 
-    it 'should return the line itself' do
+    it 'returns the line itself' do
       expect(diff_line_content(diff_file.diff_lines.first.text)).
         to eq('@@ -6,12 +6,18 @@ module Popen')
       expect(diff_line_content(diff_file.diff_lines.first.type)).to eq('match')
@@ -103,4 +85,56 @@ describe DiffHelper do
       expect(marked_new_line).to be_html_safe
     end
   end
+
+  describe "#diff_match_line" do
+    let(:old_pos) { 40 }
+    let(:new_pos) { 50 }
+    let(:text) { 'some_text' }
+
+    it "should generate foldable top match line for inline view with empty text by default" do
+      output = diff_match_line old_pos, new_pos
+
+      expect(output).to be_html_safe
+      expect(output).to have_css "td:nth-child(1):not(.js-unfold-bottom).diff-line-num.unfold.js-unfold.old_line[data-linenumber='#{old_pos}']", text: '...'
+      expect(output).to have_css "td:nth-child(2):not(.js-unfold-bottom).diff-line-num.unfold.js-unfold.new_line[data-linenumber='#{new_pos}']", text: '...'
+      expect(output).to have_css 'td:nth-child(3):not(.parallel).line_content.match', text: ''
+    end
+
+    it "should allow to define text and bottom option" do
+      output = diff_match_line old_pos, new_pos, text: text, bottom: true
+
+      expect(output).to be_html_safe
+      expect(output).to have_css "td:nth-child(1).diff-line-num.unfold.js-unfold.js-unfold-bottom.old_line[data-linenumber='#{old_pos}']", text: '...'
+      expect(output).to have_css "td:nth-child(2).diff-line-num.unfold.js-unfold.js-unfold-bottom.new_line[data-linenumber='#{new_pos}']", text: '...'
+      expect(output).to have_css 'td:nth-child(3):not(.parallel).line_content.match', text: text
+    end
+
+    it "should generate match line for parallel view" do
+      output = diff_match_line old_pos, new_pos, text: text, view: :parallel
+
+      expect(output).to be_html_safe
+      expect(output).to have_css "td:nth-child(1):not(.js-unfold-bottom).diff-line-num.unfold.js-unfold.old_line[data-linenumber='#{old_pos}']", text: '...'
+      expect(output).to have_css 'td:nth-child(2).line_content.match.parallel', text: text
+      expect(output).to have_css "td:nth-child(3):not(.js-unfold-bottom).diff-line-num.unfold.js-unfold.new_line[data-linenumber='#{new_pos}']", text: '...'
+      expect(output).to have_css 'td:nth-child(4).line_content.match.parallel', text: text
+    end
+
+    it "should allow to generate only left match line for parallel view" do
+      output = diff_match_line old_pos, nil, text: text, view: :parallel
+
+      expect(output).to be_html_safe
+      expect(output).to have_css "td:nth-child(1):not(.js-unfold-bottom).diff-line-num.unfold.js-unfold.old_line[data-linenumber='#{old_pos}']", text: '...'
+      expect(output).to have_css 'td:nth-child(2).line_content.match.parallel', text: text
+      expect(output).not_to have_css 'td:nth-child(3)'
+    end
+
+    it "should allow to generate only right match line for parallel view" do
+      output = diff_match_line nil, new_pos, text: text, view: :parallel
+
+      expect(output).to be_html_safe
+      expect(output).to have_css "td:nth-child(1):not(.js-unfold-bottom).diff-line-num.unfold.js-unfold.new_line[data-linenumber='#{new_pos}']", text: '...'
+      expect(output).to have_css 'td:nth-child(2).line_content.match.parallel', text: text
+      expect(output).not_to have_css 'td:nth-child(3)'
+    end
+  end
 end
diff --git a/spec/helpers/emails_helper_spec.rb b/spec/helpers/emails_helper_spec.rb
index 7a3e38d7e63cae8b0ce804635823b8317df47dc0..3223556e1d3444980ed81a24d912062f3f5f7bf1 100644
--- a/spec/helpers/emails_helper_spec.rb
+++ b/spec/helpers/emails_helper_spec.rb
@@ -8,37 +8,37 @@ describe EmailsHelper do
     end
 
     context 'when time limit is less than 2 hours' do
-      it 'should display the time in hours using a singular unit' do
+      it 'displays the time in hours using a singular unit' do
         validate_time_string(1.hour, '1 hour')
       end
     end
 
     context 'when time limit is 2 or more hours' do
-      it 'should display the time in hours using a plural unit' do
+      it 'displays the time in hours using a plural unit' do
         validate_time_string(2.hours, '2 hours')
       end
     end
 
     context 'when time limit contains fractions of an hour' do
-      it 'should round down to the nearest hour' do
+      it 'rounds down to the nearest hour' do
         validate_time_string(96.minutes, '1 hour')
       end
     end
 
     context 'when time limit is 24 or more hours' do
-      it 'should display the time in days using a singular unit' do
+      it 'displays the time in days using a singular unit' do
         validate_time_string(24.hours, '1 day')
       end
     end
 
     context 'when time limit is 2 or more days' do
-      it 'should display the time in days using a plural unit' do
+      it 'displays the time in days using a plural unit' do
         validate_time_string(2.days, '2 days')
       end
     end
 
     context 'when time limit contains fractions of a day' do
-      it 'should round down to the nearest day' do
+      it 'rounds down to the nearest day' do
         validate_time_string(57.hours, '2 days')
       end
     end
diff --git a/spec/helpers/events_helper_spec.rb b/spec/helpers/events_helper_spec.rb
index 6b5e3d93d48da88f7c01d576bb4c07ff2126600c..022aba0c0d079946ad05e6749cd9491370616107 100644
--- a/spec/helpers/events_helper_spec.rb
+++ b/spec/helpers/events_helper_spec.rb
@@ -6,34 +6,34 @@ describe EventsHelper do
       allow(helper).to receive(:current_user).and_return(double)
     end
 
-    it 'should display one line of plain text without alteration' do
+    it 'displays one line of plain text without alteration' do
       input = 'A short, plain note'
       expect(helper.event_note(input)).to match(input)
       expect(helper.event_note(input)).not_to match(/\.\.\.\z/)
     end
 
-    it 'should display inline code' do
+    it 'displays inline code' do
       input = 'A note with `inline code`'
       expected = 'A note with <code>inline code</code>'
 
       expect(helper.event_note(input)).to match(expected)
     end
 
-    it 'should truncate a note with multiple paragraphs' do
+    it 'truncates a note with multiple paragraphs' do
       input = "Paragraph 1\n\nParagraph 2"
       expected = 'Paragraph 1...'
 
       expect(helper.event_note(input)).to match(expected)
     end
 
-    it 'should display the first line of a code block' do
+    it 'displays the first line of a code block' do
       input = "```\nCode block\nwith two lines\n```"
       expected = %r{<pre.+><code>Code block\.\.\.</code></pre>}
 
       expect(helper.event_note(input)).to match(expected)
     end
 
-    it 'should truncate a single long line of text' do
+    it 'truncates a single long line of text' do
       text = 'The quick brown fox jumped over the lazy dog twice' # 50 chars
       input = text * 4
       expected = (text * 2).sub(/.{3}/, '...')
@@ -41,7 +41,7 @@ describe EventsHelper do
       expect(helper.event_note(input)).to match(expected)
     end
 
-    it 'should preserve a link href when link text is truncated' do
+    it 'preserves a link href when link text is truncated' do
       text = 'The quick brown fox jumped over the lazy dog' # 44 chars
       input = "#{text}#{text}#{text} " # 133 chars
       link_url = 'http://example.com/foo/bar/baz' # 30 chars
@@ -52,7 +52,7 @@ describe EventsHelper do
       expect(helper.event_note(input)).to match(expected_link_text)
     end
 
-    it 'should preserve code color scheme' do
+    it 'preserves code color scheme' do
       input = "```ruby\ndef test\n  'hello world'\nend\n```"
       expected = '<pre class="code highlight js-syntax-highlight ruby">' \
         "<code><span class=\"k\">def</span> <span class=\"nf\">test</span>\n" \
diff --git a/spec/helpers/gitlab_markdown_helper_spec.rb b/spec/helpers/gitlab_markdown_helper_spec.rb
index ade5c3b02d93660f9ef8eb9d39d3d31f00334e63..5368e5fab067fd5a3d17c120f3e2ff8c4a6b1411 100644
--- a/spec/helpers/gitlab_markdown_helper_spec.rb
+++ b/spec/helpers/gitlab_markdown_helper_spec.rb
@@ -26,17 +26,17 @@ describe GitlabMarkdownHelper do
     describe "referencing multiple objects" do
       let(:actual) { "#{merge_request.to_reference} -> #{commit.to_reference} -> #{issue.to_reference}" }
 
-      it "should link to the merge request" do
+      it "links to the merge request" do
         expected = namespace_project_merge_request_path(project.namespace, project, merge_request)
         expect(helper.markdown(actual)).to match(expected)
       end
 
-      it "should link to the commit" do
+      it "links to the commit" do
         expected = namespace_project_commit_path(project.namespace, project, commit)
         expect(helper.markdown(actual)).to match(expected)
       end
 
-      it "should link to the issue" do
+      it "links to the issue" do
         expected = namespace_project_issue_path(project.namespace, project, issue)
         expect(helper.markdown(actual)).to match(expected)
       end
@@ -47,7 +47,7 @@ describe GitlabMarkdownHelper do
       let(:second_project) { create(:project, :public) }
       let(:second_issue) { create(:issue, project: second_project) }
 
-      it 'should link to the issue' do
+      it 'links to the issue' do
         expected = namespace_project_issue_path(second_project.namespace, second_project, second_issue)
         expect(markdown(actual, project: second_project)).to match(expected)
       end
@@ -58,7 +58,7 @@ describe GitlabMarkdownHelper do
     let(:commit_path) { namespace_project_commit_path(project.namespace, project, commit) }
     let(:issues)      { create_list(:issue, 2, project: project) }
 
-    it 'should handle references nested in links with all the text' do
+    it 'handles references nested in links with all the text' do
       actual = helper.link_to_gfm("This should finally fix #{issues[0].to_reference} and #{issues[1].to_reference} for real", commit_path)
       doc = Nokogiri::HTML.parse(actual)
 
@@ -88,7 +88,7 @@ describe GitlabMarkdownHelper do
       expect(doc.css('a')[4].text).to eq ' for real'
     end
 
-    it 'should forward HTML options' do
+    it 'forwards HTML options' do
       actual = helper.link_to_gfm("Fixed in #{commit.id}", commit_path, class: 'foo')
       doc = Nokogiri::HTML.parse(actual)
 
@@ -110,7 +110,7 @@ describe GitlabMarkdownHelper do
       expect(act).to eq %Q(<a href="/foo">#{issues[0].to_reference}</a>)
     end
 
-    it 'should replace commit message with emoji to link' do
+    it 'replaces commit message with emoji to link' do
       actual = link_to_gfm(':book:Book', '/foo')
       expect(actual).
         to eq %Q(<img class="emoji" title=":book:" alt=":book:" src="http://localhost/assets/1F4D6.png" height="20" width="20" align="absmiddle"><a href="/foo">Book</a>)
@@ -125,7 +125,7 @@ describe GitlabMarkdownHelper do
       helper.instance_variable_set(:@project_wiki, @wiki)
     end
 
-    it "should use Wiki pipeline for markdown files" do
+    it "uses Wiki pipeline for markdown files" do
       allow(@wiki).to receive(:format).and_return(:markdown)
 
       expect(helper).to receive(:markdown).with('wiki content', pipeline: :wiki, project_wiki: @wiki, page_slug: "nested/page")
@@ -133,7 +133,7 @@ describe GitlabMarkdownHelper do
       helper.render_wiki_content(@wiki)
     end
 
-    it "should use Asciidoctor for asciidoc files" do
+    it "uses Asciidoctor for asciidoc files" do
       allow(@wiki).to receive(:format).and_return(:asciidoc)
 
       expect(helper).to receive(:asciidoc).with('wiki content')
@@ -141,7 +141,7 @@ describe GitlabMarkdownHelper do
       helper.render_wiki_content(@wiki)
     end
 
-    it "should use the Gollum renderer for all other file types" do
+    it "uses the Gollum renderer for all other file types" do
       allow(@wiki).to receive(:format).and_return(:rdoc)
       formatted_content_stub = double('formatted_content')
       expect(formatted_content_stub).to receive(:html_safe)
diff --git a/spec/helpers/graph_helper_spec.rb b/spec/helpers/graph_helper_spec.rb
index 4acf38771b7ab8b799c003efc27778aac7f17221..51c49f0e5871b8c912f123d024229df3f77ae7b6 100644
--- a/spec/helpers/graph_helper_spec.rb
+++ b/spec/helpers/graph_helper_spec.rb
@@ -6,7 +6,7 @@ describe GraphHelper do
     let(:commit)  { project.commit("master") }
     let(:graph) { Network::Graph.new(project, 'master', commit, '') }
 
-    it 'filter our refs used by GitLab' do
+    it 'filters our refs used by GitLab' do
       allow(commit).to receive(:ref_names).and_return(['refs/merge-requests/abc', 'master', 'refs/tmp/xyz'])
       self.instance_variable_set(:@graph, graph)
       refs = get_refs(project.repository, commit)
diff --git a/spec/helpers/groups_helper_spec.rb b/spec/helpers/groups_helper_spec.rb
index 4ea90a80a926649ba2597bf08ddad0831a8c14e8..0807534720a9b69a6b1812692b5de048bd2710dc 100644
--- a/spec/helpers/groups_helper_spec.rb
+++ b/spec/helpers/groups_helper_spec.rb
@@ -4,7 +4,7 @@ describe GroupsHelper do
   describe 'group_icon' do
     avatar_file_path = File.join(Rails.root, 'spec', 'fixtures', 'banana_sample.gif')
 
-    it 'should return an url for the avatar' do
+    it 'returns an url for the avatar' do
       group = create(:group)
       group.avatar = File.open(avatar_file_path)
       group.save!
@@ -12,7 +12,7 @@ describe GroupsHelper do
         to match("/uploads/group/avatar/#{group.id}/banana_sample.gif")
     end
 
-    it 'should give default avatar_icon when no avatar is present' do
+    it 'gives default avatar_icon when no avatar is present' do
       group = create(:group)
       group.save!
       expect(group_icon(group.path)).to match('group_avatar.png')
diff --git a/spec/helpers/issuables_helper_spec.rb b/spec/helpers/issuables_helper_spec.rb
new file mode 100644
index 0000000000000000000000000000000000000000..2dd2eab0524369b8e13ea8c17b5005ca0942894d
--- /dev/null
+++ b/spec/helpers/issuables_helper_spec.rb
@@ -0,0 +1,16 @@
+require 'spec_helper'
+
+describe IssuablesHelper do 
+  let(:label)  { build_stubbed(:label) }
+  let(:label2) { build_stubbed(:label) }
+
+  context 'label 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
+end
diff --git a/spec/helpers/issues_helper_spec.rb b/spec/helpers/issues_helper_spec.rb
index 831ae7fb69c51daa7c51fe82e7c5507bf5e989c2..67bac782591896c4a0a255cd61ab0405abba62e2 100644
--- a/spec/helpers/issues_helper_spec.rb
+++ b/spec/helpers/issues_helper_spec.rb
@@ -5,69 +5,24 @@ describe IssuesHelper do
   let(:issue) { create :issue, project: project }
   let(:ext_project) { create :redmine_project }
 
-  describe "url_for_project_issues" do
-    let(:project_url) { ext_project.external_issue_tracker.project_url }
-    let(:ext_expected) { project_url.gsub(':project_id', ext_project.id.to_s) }
-    let(:int_expected) { polymorphic_path([@project.namespace, project]) }
-
-    it "should return internal path if used internal tracker" do
-      @project = project
-      expect(url_for_project_issues).to match(int_expected)
-    end
-
-    it "should return path to external tracker" do
-      @project = ext_project
-
-      expect(url_for_project_issues).to match(ext_expected)
-    end
-
-    it "should return empty string if project nil" do
-      @project = nil
-
-      expect(url_for_project_issues).to eq ""
-    end
-
-    it 'returns an empty string if project_url is invalid' do
-      expect(project).to receive_message_chain('issues_tracker.project_url') { 'javascript:alert("foo");' }
-
-      expect(url_for_project_issues(project)).to eq ''
-    end
-
-    it 'returns an empty string if project_path is invalid' do
-      expect(project).to receive_message_chain('issues_tracker.project_path') { 'javascript:alert("foo");' }
-
-      expect(url_for_project_issues(project, only_path: true)).to eq ''
-    end
-
-    describe "when external tracker was enabled and then config removed" do
-      before do
-        @project = ext_project
-        allow(Gitlab.config).to receive(:issues_tracker).and_return(nil)
-      end
-
-      it "should return path to external tracker" do
-        expect(url_for_project_issues).to match(ext_expected)
-      end
-    end
-  end
-
   describe "url_for_issue" do
     let(:issues_url) { ext_project.external_issue_tracker.issues_url}
     let(:ext_expected) { issues_url.gsub(':id', issue.iid.to_s).gsub(':project_id', ext_project.id.to_s) }
     let(:int_expected) { polymorphic_path([@project.namespace, project, issue]) }
 
-    it "should return internal path if used internal tracker" do
+    it "returns internal path if used internal tracker" do
       @project = project
+
       expect(url_for_issue(issue.iid)).to match(int_expected)
     end
 
-    it "should return path to external tracker" do
+    it "returns path to external tracker" do
       @project = ext_project
 
       expect(url_for_issue(issue.iid)).to match(ext_expected)
     end
 
-    it "should return empty string if project nil" do
+    it "returns empty string if project nil" do
       @project = nil
 
       expect(url_for_issue(issue.iid)).to eq ""
@@ -91,66 +46,46 @@ describe IssuesHelper do
         allow(Gitlab.config).to receive(:issues_tracker).and_return(nil)
       end
 
-      it "should return external path" do
+      it "returns external path" do
         expect(url_for_issue(issue.iid)).to match(ext_expected)
       end
     end
   end
 
-  describe 'url_for_new_issue' do
-    let(:issues_url) { ext_project.external_issue_tracker.new_issue_url }
-    let(:ext_expected) { issues_url.gsub(':project_id', ext_project.id.to_s) }
-    let(:int_expected) { new_namespace_project_issue_path(project.namespace, project) }
-
-    it "should return internal path if used internal tracker" do
-      @project = project
-      expect(url_for_new_issue).to match(int_expected)
+  describe "merge_requests_sentence" do
+    subject { merge_requests_sentence(merge_requests)}
+    let(:merge_requests) do
+      [ build(:merge_request, iid: 1), build(:merge_request, iid: 2),
+        build(:merge_request, iid: 3)]
     end
 
-    it "should return path to external tracker" do
-      @project = ext_project
-
-      expect(url_for_new_issue).to match(ext_expected)
-    end
+    it { is_expected.to eq("!1, !2, or !3") }
+  end
 
-    it "should return empty string if project nil" do
-      @project = nil
+  describe '#award_user_list' do
+    let!(:awards) { build_list(:award_emoji, 15) }
 
-      expect(url_for_new_issue).to eq ""
+    it "returns a comma seperated list of 1-9 users" do
+      expect(award_user_list(awards.first(9), nil)).to eq(awards.first(9).map { |a| a.user.name }.to_sentence)
     end
 
-    it 'returns an empty string if issue_url is invalid' do
-      expect(project).to receive_message_chain('issues_tracker.new_issue_url') { 'javascript:alert("foo");' }
-
-      expect(url_for_new_issue(project)).to eq ''
+    it "displays the current user's name as 'You'" do
+      expect(award_user_list(awards.first(1), awards[0].user)).to eq('You')
     end
 
-    it 'returns an empty string if issue_path is invalid' do
-      expect(project).to receive_message_chain('issues_tracker.new_issue_path') { 'javascript:alert("foo");' }
-
-      expect(url_for_new_issue(project, only_path: true)).to eq ''
+    it "truncates lists of larger than 9 users" do
+      expect(award_user_list(awards, nil)).to eq(awards.first(9).map { |a| a.user.name }.join(', ') + ", and 6 more.")
     end
 
-    describe "when external tracker was enabled and then config removed" do
-      before do
-        @project = ext_project
-        allow(Gitlab.config).to receive(:issues_tracker).and_return(nil)
-      end
-
-      it "should return internal path" do
-        expect(url_for_new_issue).to match(ext_expected)
-      end
+    it "displays the current user in front of 0-9 other users" do
+      expect(award_user_list(awards, awards[0].user)).
+        to eq("You, " + awards[1..9].map { |a| a.user.name }.join(', ') + ", and 5 more.")
     end
-  end
 
-  describe "merge_requests_sentence" do
-    subject { merge_requests_sentence(merge_requests)}
-    let(:merge_requests) do
-      [ build(:merge_request, iid: 1), build(:merge_request, iid: 2),
-        build(:merge_request, iid: 3)]
+    it "displays the current user in front regardless of position in the list" do
+      expect(award_user_list(awards, awards[12].user)).
+        to eq("You, " + awards[0..8].map { |a| a.user.name }.join(', ') + ", and 5 more.")
     end
-
-    it { is_expected.to eq("!1, !2, or !3") }
   end
 
   describe '#award_active_class' do
diff --git a/spec/helpers/members_helper_spec.rb b/spec/helpers/members_helper_spec.rb
index f75fdb739f6891b0f547928043cc2210e19f30d0..7998209b7b00e7a759eade60dfa2e42ed37e7990 100644
--- a/spec/helpers/members_helper_spec.rb
+++ b/spec/helpers/members_helper_spec.rb
@@ -9,54 +9,6 @@ describe MembersHelper do
     it { expect(action_member_permission(:admin, group_member)).to eq :admin_group_member }
   end
 
-  describe '#default_show_roles' do
-    let(:user) { double }
-    let(:member) { build(:project_member) }
-
-    before do
-      allow(helper).to receive(:current_user).and_return(user)
-      allow(helper).to receive(:can?).with(user, :update_project_member, member).and_return(false)
-      allow(helper).to receive(:can?).with(user, :destroy_project_member, member).and_return(false)
-      allow(helper).to receive(:can?).with(user, :admin_project_member, member.source).and_return(false)
-    end
-
-    context 'when the current cannot update, destroy or admin the passed member' do
-      it 'returns false' do
-        expect(helper.default_show_roles(member)).to be_falsy
-      end
-    end
-
-    context 'when the current can update the passed member' do
-      before do
-        allow(helper).to receive(:can?).with(user, :update_project_member, member).and_return(true)
-      end
-
-      it 'returns true' do
-        expect(helper.default_show_roles(member)).to be_truthy
-      end
-    end
-
-    context 'when the current can destroy the passed member' do
-      before do
-        allow(helper).to receive(:can?).with(user, :destroy_project_member, member).and_return(true)
-      end
-
-      it 'returns true' do
-        expect(helper.default_show_roles(member)).to be_truthy
-      end
-    end
-
-    context 'when the current can admin the passed member source' do
-      before do
-        allow(helper).to receive(:can?).with(user, :admin_project_member, member.source).and_return(true)
-      end
-
-      it 'returns true' do
-        expect(helper.default_show_roles(member)).to be_truthy
-      end
-    end
-  end
-
   describe '#remove_member_message' do
     let(:requester) { build(:user) }
     let(:project) { create(:project) }
diff --git a/spec/helpers/notes_helper_spec.rb b/spec/helpers/notes_helper_spec.rb
index af371248ae95ee943a14fa2b377ea368d8dbd1a4..9c577501f003b9536d812ef549219eb5abe2d19b 100644
--- a/spec/helpers/notes_helper_spec.rb
+++ b/spec/helpers/notes_helper_spec.rb
@@ -21,7 +21,7 @@ describe NotesHelper do
   end
 
   describe "#notes_max_access_for_users" do
-    it 'return human access levels' do
+    it 'returns human access levels' do
       expect(helper.note_max_access_for_user(owner_note)).to eq('Owner')
       expect(helper.note_max_access_for_user(master_note)).to eq('Master')
       expect(helper.note_max_access_for_user(reporter_note)).to eq('Reporter')
@@ -38,6 +38,11 @@ describe NotesHelper do
   end
 
   describe '#preload_max_access_for_authors' do
+    before do
+      # This method reads cache from RequestStore, so make sure it's clean.
+      RequestStore.clear!
+    end
+
     it 'loads multiple users' do
       expected_access = {
         owner.id => Gitlab::Access::OWNER,
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/search_helper_spec.rb b/spec/helpers/search_helper_spec.rb
index 601b6915e27e83d1c13a76f6e324b885b79d90bb..b0bb991539b38ca0fb189c96739f72aafec045c6 100644
--- a/spec/helpers/search_helper_spec.rb
+++ b/spec/helpers/search_helper_spec.rb
@@ -42,7 +42,7 @@ describe SearchHelper do
         expect(search_autocomplete_opts(project.name).size).to eq(1)
       end
 
-      it "should not include the public group" do
+      it "does not include the public group" do
         group = create(:group)
         expect(search_autocomplete_opts(group.name).size).to eq(0)
       end
diff --git a/spec/helpers/submodule_helper_spec.rb b/spec/helpers/submodule_helper_spec.rb
index 101217591322d5c279222cd37071486b43aedf52..37ac6a2699d2d8aebe1997bba0f3dfa45c7c4658 100644
--- a/spec/helpers/submodule_helper_spec.rb
+++ b/spec/helpers/submodule_helper_spec.rb
@@ -17,35 +17,35 @@ describe SubmoduleHelper do
         allow(Gitlab.config.gitlab).to receive(:protocol).and_return('http') # set this just to be sure
       end
 
-      it 'should detect ssh on standard port' do
+      it 'detects ssh on standard port' do
         allow(Gitlab.config.gitlab_shell).to receive(:ssh_port).and_return(22) # set this just to be sure
         allow(Gitlab.config.gitlab_shell).to receive(:ssh_path_prefix).and_return(Settings.send(:build_gitlab_shell_ssh_path_prefix))
         stub_url([ config.user, '@', config.host, ':gitlab-org/gitlab-ce.git' ].join(''))
         expect(submodule_links(submodule_item)).to eq([ namespace_project_path('gitlab-org', 'gitlab-ce'), namespace_project_tree_path('gitlab-org', 'gitlab-ce', 'hash') ])
       end
 
-      it 'should detect ssh on non-standard port' do
+      it 'detects ssh on non-standard port' do
         allow(Gitlab.config.gitlab_shell).to receive(:ssh_port).and_return(2222)
         allow(Gitlab.config.gitlab_shell).to receive(:ssh_path_prefix).and_return(Settings.send(:build_gitlab_shell_ssh_path_prefix))
         stub_url([ 'ssh://', config.user, '@', config.host, ':2222/gitlab-org/gitlab-ce.git' ].join(''))
         expect(submodule_links(submodule_item)).to eq([ namespace_project_path('gitlab-org', 'gitlab-ce'), namespace_project_tree_path('gitlab-org', 'gitlab-ce', 'hash') ])
       end
 
-      it 'should detect http on standard port' do
+      it 'detects http on standard port' do
         allow(Gitlab.config.gitlab).to receive(:port).and_return(80)
         allow(Gitlab.config.gitlab).to receive(:url).and_return(Settings.send(:build_gitlab_url))
         stub_url([ 'http://', config.host, '/gitlab-org/gitlab-ce.git' ].join(''))
         expect(submodule_links(submodule_item)).to eq([ namespace_project_path('gitlab-org', 'gitlab-ce'), namespace_project_tree_path('gitlab-org', 'gitlab-ce', 'hash') ])
       end
 
-      it 'should detect http on non-standard port' do
+      it 'detects http on non-standard port' do
         allow(Gitlab.config.gitlab).to receive(:port).and_return(3000)
         allow(Gitlab.config.gitlab).to receive(:url).and_return(Settings.send(:build_gitlab_url))
         stub_url([ 'http://', config.host, ':3000/gitlab-org/gitlab-ce.git' ].join(''))
         expect(submodule_links(submodule_item)).to eq([ namespace_project_path('gitlab-org', 'gitlab-ce'), namespace_project_tree_path('gitlab-org', 'gitlab-ce', 'hash') ])
       end
 
-      it 'should work with relative_url_root' do
+      it 'works with relative_url_root' do
         allow(Gitlab.config.gitlab).to receive(:port).and_return(80) # set this just to be sure
         allow(Gitlab.config.gitlab).to receive(:relative_url_root).and_return('/gitlab/root')
         allow(Gitlab.config.gitlab).to receive(:url).and_return(Settings.send(:build_gitlab_url))
@@ -55,22 +55,22 @@ describe SubmoduleHelper do
     end
 
     context 'submodule on github.com' do
-      it 'should detect ssh' do
+      it 'detects ssh' do
         stub_url('git@github.com:gitlab-org/gitlab-ce.git')
         expect(submodule_links(submodule_item)).to eq([ 'https://github.com/gitlab-org/gitlab-ce', 'https://github.com/gitlab-org/gitlab-ce/tree/hash' ])
       end
 
-      it 'should detect http' do
+      it 'detects http' do
         stub_url('http://github.com/gitlab-org/gitlab-ce.git')
         expect(submodule_links(submodule_item)).to eq([ 'https://github.com/gitlab-org/gitlab-ce', 'https://github.com/gitlab-org/gitlab-ce/tree/hash' ])
       end
 
-      it 'should detect https' do
+      it 'detects https' do
         stub_url('https://github.com/gitlab-org/gitlab-ce.git')
         expect(submodule_links(submodule_item)).to eq([ 'https://github.com/gitlab-org/gitlab-ce', 'https://github.com/gitlab-org/gitlab-ce/tree/hash' ])
       end
 
-      it 'should return original with non-standard url' do
+      it 'returns original with non-standard url' do
         stub_url('http://github.com/gitlab-org/gitlab-ce')
         expect(submodule_links(submodule_item)).to eq([ repo.submodule_url_for, nil ])
 
@@ -80,22 +80,22 @@ describe SubmoduleHelper do
     end
 
     context 'submodule on gitlab.com' do
-      it 'should detect ssh' do
+      it 'detects ssh' do
         stub_url('git@gitlab.com:gitlab-org/gitlab-ce.git')
         expect(submodule_links(submodule_item)).to eq([ 'https://gitlab.com/gitlab-org/gitlab-ce', 'https://gitlab.com/gitlab-org/gitlab-ce/tree/hash' ])
       end
 
-      it 'should detect http' do
+      it 'detects http' do
         stub_url('http://gitlab.com/gitlab-org/gitlab-ce.git')
         expect(submodule_links(submodule_item)).to eq([ 'https://gitlab.com/gitlab-org/gitlab-ce', 'https://gitlab.com/gitlab-org/gitlab-ce/tree/hash' ])
       end
 
-      it 'should detect https' do
+      it 'detects https' do
         stub_url('https://gitlab.com/gitlab-org/gitlab-ce.git')
         expect(submodule_links(submodule_item)).to eq([ 'https://gitlab.com/gitlab-org/gitlab-ce', 'https://gitlab.com/gitlab-org/gitlab-ce/tree/hash' ])
       end
 
-      it 'should return original with non-standard url' do
+      it 'returns original with non-standard url' do
         stub_url('http://gitlab.com/gitlab-org/gitlab-ce')
         expect(submodule_links(submodule_item)).to eq([ repo.submodule_url_for, nil ])
 
@@ -105,7 +105,7 @@ describe SubmoduleHelper do
     end
 
     context 'submodule on unsupported' do
-      it 'should return original' do
+      it 'returns original' do
         stub_url('http://mygitserver.com/gitlab-org/gitlab-ce')
         expect(submodule_links(submodule_item)).to eq([ repo.submodule_url_for, nil ])
 
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/helpers/tree_helper_spec.rb b/spec/helpers/tree_helper_spec.rb
index c70dd8076e0e6c79c5049ea53ebb2443ef598629..8d6537ba4b54b6eced05bac9a9faa4ea84846a18 100644
--- a/spec/helpers/tree_helper_spec.rb
+++ b/spec/helpers/tree_helper_spec.rb
@@ -12,7 +12,7 @@ describe TreeHelper do
     context "on a directory containing more than one file/directory" do
       let(:tree_item) { double(name: "files", path: "files") }
 
-      it "should return the directory name" do
+      it "returns the directory name" do
         expect(flatten_tree(tree_item)).to match('files')
       end
     end
@@ -20,7 +20,7 @@ describe TreeHelper do
     context "on a directory containing only one directory" do
       let(:tree_item) { double(name: "foo", path: "foo") }
 
-      it "should return the flattened path" do
+      it "returns the flattened path" do
         expect(flatten_tree(tree_item)).to match('foo/bar')
       end
     end
diff --git a/spec/initializers/secret_token_spec.rb b/spec/initializers/secret_token_spec.rb
new file mode 100644
index 0000000000000000000000000000000000000000..837b0de9a4cba692e5127759940706926251200c
--- /dev/null
+++ b/spec/initializers/secret_token_spec.rb
@@ -0,0 +1,200 @@
+require 'spec_helper'
+require_relative '../../config/initializers/secret_token'
+
+describe 'create_tokens', lib: true do
+  let(:secrets) { ActiveSupport::OrderedOptions.new }
+
+  before do
+    allow(ENV).to receive(:[]).and_call_original
+    allow(File).to receive(:write)
+    allow(File).to receive(:delete)
+    allow(Rails).to receive_message_chain(:application, :secrets).and_return(secrets)
+    allow(Rails).to receive_message_chain(:root, :join) { |string| string }
+    allow(self).to receive(:warn)
+    allow(self).to receive(:exit)
+  end
+
+  context 'setting secret_key_base and otp_key_base' do
+    context 'when none of the secrets exist' do
+      before do
+        allow(ENV).to receive(:[]).with('SECRET_KEY_BASE').and_return(nil)
+        allow(File).to receive(:exist?).with('.secret').and_return(false)
+        allow(File).to receive(:exist?).with('config/secrets.yml').and_return(false)
+        allow(self).to receive(:warn_missing_secret)
+      end
+
+      it 'generates different secrets for secret_key_base, otp_key_base, and db_key_base' do
+        create_tokens
+
+        keys = secrets.values_at(:secret_key_base, :otp_key_base, :db_key_base)
+
+        expect(keys.uniq).to eq(keys)
+        expect(keys.map(&:length)).to all(eq(128))
+      end
+
+      it 'warns about the secrets to add to secrets.yml' do
+        expect(self).to receive(:warn_missing_secret).with('secret_key_base')
+        expect(self).to receive(:warn_missing_secret).with('otp_key_base')
+        expect(self).to receive(:warn_missing_secret).with('db_key_base')
+
+        create_tokens
+      end
+
+      it 'writes the secrets to secrets.yml' do
+        expect(File).to receive(:write).with('config/secrets.yml', any_args) do |filename, contents, options|
+          new_secrets = YAML.load(contents)[Rails.env]
+
+          expect(new_secrets['secret_key_base']).to eq(secrets.secret_key_base)
+          expect(new_secrets['otp_key_base']).to eq(secrets.otp_key_base)
+          expect(new_secrets['db_key_base']).to eq(secrets.db_key_base)
+        end
+
+        create_tokens
+      end
+
+      it 'does not write a .secret file' do
+        expect(File).not_to receive(:write).with('.secret')
+
+        create_tokens
+      end
+    end
+
+    context 'when the other secrets all exist' do
+      before do
+        secrets.db_key_base = 'db_key_base'
+
+        allow(File).to receive(:exist?).with('.secret').and_return(true)
+        allow(File).to receive(:read).with('.secret').and_return('file_key')
+      end
+
+      context 'when secret_key_base exists in the environment and secrets.yml' do
+        before do
+          allow(ENV).to receive(:[]).with('SECRET_KEY_BASE').and_return('env_key')
+          secrets.secret_key_base = 'secret_key_base'
+          secrets.otp_key_base = 'otp_key_base'
+        end
+
+        it 'does not issue a warning' do
+          expect(self).not_to receive(:warn)
+
+          create_tokens
+        end
+
+        it 'uses the environment variable' do
+          create_tokens
+
+          expect(secrets.secret_key_base).to eq('env_key')
+        end
+
+        it 'does not update secrets.yml' do
+          expect(File).not_to receive(:write)
+
+          create_tokens
+        end
+      end
+
+      context 'when secret_key_base and otp_key_base exist' do
+        before do
+          secrets.secret_key_base = 'secret_key_base'
+          secrets.otp_key_base = 'otp_key_base'
+        end
+
+        it 'does not write any files' do
+          expect(File).not_to receive(:write)
+
+          create_tokens
+        end
+
+        it 'sets the the keys to the values from the environment and secrets.yml' do
+          create_tokens
+
+          expect(secrets.secret_key_base).to eq('secret_key_base')
+          expect(secrets.otp_key_base).to eq('otp_key_base')
+          expect(secrets.db_key_base).to eq('db_key_base')
+        end
+
+        it 'deletes the .secret file' do
+          expect(File).to receive(:delete).with('.secret')
+
+          create_tokens
+        end
+      end
+
+      context 'when secret_key_base and otp_key_base do not exist' do
+        before do
+          allow(File).to receive(:exist?).with('config/secrets.yml').and_return(true)
+          allow(YAML).to receive(:load_file).with('config/secrets.yml').and_return('test' => secrets.to_h.stringify_keys)
+          allow(self).to receive(:warn_missing_secret)
+        end
+
+        it 'uses the file secret' do
+          expect(File).to receive(:write) do |filename, contents, options|
+            new_secrets = YAML.load(contents)[Rails.env]
+
+            expect(new_secrets['secret_key_base']).to eq('file_key')
+            expect(new_secrets['otp_key_base']).to eq('file_key')
+            expect(new_secrets['db_key_base']).to eq('db_key_base')
+          end
+
+          create_tokens
+
+          expect(secrets.otp_key_base).to eq('file_key')
+        end
+
+        it 'keeps the other secrets as they were' do
+          create_tokens
+
+          expect(secrets.db_key_base).to eq('db_key_base')
+        end
+
+        it 'warns about the missing secrets' do
+          expect(self).to receive(:warn_missing_secret).with('secret_key_base')
+          expect(self).to receive(:warn_missing_secret).with('otp_key_base')
+
+          create_tokens
+        end
+
+        it 'deletes the .secret file' do
+          expect(File).to receive(:delete).with('.secret')
+
+          create_tokens
+        end
+      end
+    end
+
+    context 'when db_key_base is blank but exists in secrets.yml' do
+      before do
+        secrets.otp_key_base = 'otp_key_base'
+        secrets.secret_key_base = 'secret_key_base'
+        yaml_secrets = secrets.to_h.stringify_keys.merge('db_key_base' => '<%= an_erb_expression %>')
+
+        allow(File).to receive(:exist?).with('.secret').and_return(false)
+        allow(File).to receive(:exist?).with('config/secrets.yml').and_return(true)
+        allow(YAML).to receive(:load_file).with('config/secrets.yml').and_return('test' => yaml_secrets)
+        allow(self).to receive(:warn_missing_secret)
+      end
+
+      it 'warns about updating db_key_base' do
+        expect(self).to receive(:warn_missing_secret).with('db_key_base')
+
+        create_tokens
+      end
+
+      it 'warns about the blank value existing in secrets.yml and exits' do
+        expect(self).to receive(:warn) do |warning|
+          expect(warning).to include('db_key_base')
+          expect(warning).to include('<%= an_erb_expression %>')
+        end
+
+        create_tokens
+      end
+
+      it 'does not update secrets.yml' do
+        expect(self).to receive(:exit).with(1).and_call_original
+        expect(File).not_to receive(:write)
+
+        expect { create_tokens }.to raise_error(SystemExit)
+      end
+    end
+  end
+end
diff --git a/spec/javascripts/abuse_reports_spec.js.es6 b/spec/javascripts/abuse_reports_spec.js.es6
new file mode 100644
index 0000000000000000000000000000000000000000..6bcfdf191c2cab88dc728703bc744889749c47f5
--- /dev/null
+++ b/spec/javascripts/abuse_reports_spec.js.es6
@@ -0,0 +1,41 @@
+/*= 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/awards_handler_spec.js b/spec/javascripts/awards_handler_spec.js
index 3ddc163033e4705c73d900967417553d05393577..fa32d0d7da5760f284e27bce7c2e334729102fcb 100644
--- a/spec/javascripts/awards_handler_spec.js
+++ b/spec/javascripts/awards_handler_spec.js
@@ -143,6 +143,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/boards/boards_store_spec.js.es6 b/spec/javascripts/boards/boards_store_spec.js.es6
new file mode 100644
index 0000000000000000000000000000000000000000..078e4b00023c2574509948a6806eb617b33c70e3
--- /dev/null
+++ b/spec/javascripts/boards/boards_store_spec.js.es6
@@ -0,0 +1,164 @@
+//= require jquery
+//= require jquery_ujs
+//= require jquery.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
+
+(() => {
+  beforeEach(() => {
+    gl.boardService = new BoardService('/test/issue-boards/board');
+    gl.issueBoards.BoardsStore.create();
+
+    $.cookie('issue_board_welcome_hidden', 'false');
+  });
+
+  describe('Store', () => {
+    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..3569d1b98bd12f0b3ab042d8b82b281113044d82
--- /dev/null
+++ b/spec/javascripts/boards/issue_spec.js.es6
@@ -0,0 +1,83 @@
+//= require jquery
+//= require jquery_ujs
+//= require jquery.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');
+    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..c206b794442f913c71a96290381e839cd0730ea8
--- /dev/null
+++ b/spec/javascripts/boards/list_spec.js.es6
@@ -0,0 +1,89 @@
+//= require jquery
+//= require jquery_ujs
+//= require jquery.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(() => {
+    gl.boardService = new BoardService('/test/issue-boards/board');
+    gl.issueBoards.BoardsStore.create();
+
+    list = new List(listObj);
+  });
+
+  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('can\'t search when not backlog', () => {
+    expect(list.canSearch()).toBe(false);
+  });
+
+  it('can search when backlog', () => {
+    list.type = 'backlog';
+    expect(list.canSearch()).toBe(true);
+  });
+
+  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..0c37ec8354f3efe9385e703b1225bee8240beb3b
--- /dev/null
+++ b/spec/javascripts/boards/mock_data.js.es6
@@ -0,0 +1,53 @@
+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/lists{/id}/issues': [{
+      title: 'Testing',
+      iid: 1,
+      confidential: false,
+      labels: []
+    }]
+  },
+  'POST': {
+    '/test/issue-boards/board/lists{/id}': listObj
+  },
+  'PUT': {
+    '/test/issue-boards/board/lists{/id}': {}
+  },
+  'DELETE': {
+    '/test/issue-boards/board/lists{/id}': {}
+  }
+};
+
+Vue.http.interceptors.push((request, next) => {
+  const body = BoardsMockData[request.method][request.url];
+
+  next(request.respondWith(JSON.stringify(body), {
+    status: 200
+  }));
+});
diff --git a/spec/javascripts/datetime_utility_spec.js.coffee b/spec/javascripts/datetime_utility_spec.js.coffee
new file mode 100644
index 0000000000000000000000000000000000000000..6b9617341fe7b34dc86fe374ebfa2ccb9bea4ee6
--- /dev/null
+++ b/spec/javascripts/datetime_utility_spec.js.coffee
@@ -0,0 +1,31 @@
+#= 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/diff_comments_store_spec.js.es6 b/spec/javascripts/diff_comments_store_spec.js.es6
new file mode 100644
index 0000000000000000000000000000000000000000..22293d4de878d992d341ca32d8b880f8503256f9
--- /dev/null
+++ b/spec/javascripts/diff_comments_store_spec.js.es6
@@ -0,0 +1,122 @@
+//= 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);
+      console.log(discussion.isResolved());
+
+      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/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/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/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/gl_dropdown_spec.js.es6 b/spec/javascripts/gl_dropdown_spec.js.es6
new file mode 100644
index 0000000000000000000000000000000000000000..b529ea6458d9a6663e9d74b270ddf8abd6f75695
--- /dev/null
+++ b/spec/javascripts/gl_dropdown_spec.js.es6
@@ -0,0 +1,119 @@
+/*= 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 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 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();
+    }
+  };
+
+  describe('Dropdown', function describeDropdown() {
+    fixture.preload('gl_dropdown.html');
+    fixture.preload('projects.json');
+
+    beforeEach(() => {
+      fixture.load('gl_dropdown.html');
+      this.dropdownContainerElement = $('.dropdown.inline');
+      this.dropdownMenuElement = $('.dropdown-menu', this.dropdownContainerElement);
+      this.projectsData = fixture.load('projects.json')[0];
+      this.dropdownButtonElement = $('#js-project-dropdown', this.dropdownContainerElement).glDropdown({
+        selectable: true,
+        data: this.projectsData,
+        text: (project) => {
+          (project.name_with_namespace || project.name);
+        },
+        id: (project) => {
+          project.id;
+        }
+      });
+    });
+
+    afterEach(() => {
+      $('body').unbind('keydown');
+      this.dropdownContainerElement.unbind('keyup');
+    });
+
+    it('should open on click', () => {
+      expect(this.dropdownContainerElement).not.toHaveClass('open');
+      this.dropdownButtonElement.click();
+      expect(this.dropdownContainerElement).toHaveClass('open');
+    });
+
+    describe('that is open', () => {
+      beforeEach(() => {
+        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');
+      });
+    });
+  });
+})();
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..840c7b6d015e1379eb7a333b14da1c5da9ae0edd
--- /dev/null
+++ b/spec/javascripts/labels_issue_sidebar_spec.js.es6
@@ -0,0 +1,89 @@
+//= 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);
+
+        $('.dropdow-content a').each((i, $link) => {
+          if (i < 5) {
+            $link.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);
+
+        $('.dropdow-content a').each((i, $link) => {
+          if (i < 5) {
+            $link.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/search_autocomplete_spec.js b/spec/javascripts/search_autocomplete_spec.js
index 68d64483d6777ce04521116b00e07785abdf80d1..324f5152780af71348dd786b931e8e60e6da5b8f 100644
--- a/spec/javascripts/search_autocomplete_spec.js
+++ b/spec/javascripts/search_autocomplete_spec.js
@@ -105,13 +105,13 @@
     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() {
diff --git a/spec/lib/banzai/filter/relative_link_filter_spec.rb b/spec/lib/banzai/filter/relative_link_filter_spec.rb
index 224baca803019cb850e18f44cafd86f59076f421..6b58f3e43ee1153f91461873052cfcd1a235d10d 100644
--- a/spec/lib/banzai/filter/relative_link_filter_spec.rb
+++ b/spec/lib/banzai/filter/relative_link_filter_spec.rb
@@ -3,7 +3,7 @@ require 'spec_helper'
 describe Banzai::Filter::RelativeLinkFilter, lib: true do
   def filter(doc, contexts = {})
     contexts.reverse_merge!({
-      commit:         project.commit,
+      commit:         commit,
       project:        project,
       project_wiki:   project_wiki,
       ref:            ref,
@@ -28,6 +28,7 @@ describe Banzai::Filter::RelativeLinkFilter, lib: true do
   let(:project)        { create(:project) }
   let(:project_path)   { project.path_with_namespace }
   let(:ref)            { 'markdown' }
+  let(:commit)         { project.commit(ref) }
   let(:project_wiki)   { nil }
   let(:requested_path) { '/' }
 
@@ -77,13 +78,24 @@ describe Banzai::Filter::RelativeLinkFilter, lib: true do
     expect { filter(act) }.not_to raise_error
   end
 
-  context 'with a valid repository' do
+  it 'ignores ref if commit is passed' do
+    doc = filter(link('non/existent.file'), commit: project.commit('empty-branch') )
+    expect(doc.at_css('a')['href']).
+      to eq "/#{project_path}/#{ref}/non/existent.file" # non-existent files have no leading blob/raw/tree
+  end
+
+  shared_examples :valid_repository do
     it 'rebuilds absolute URL for a file in the repo' do
       doc = filter(link('/doc/api/README.md'))
       expect(doc.at_css('a')['href']).
         to eq "/#{project_path}/blob/#{ref}/doc/api/README.md"
     end
 
+    it 'ignores absolute URLs with two leading slashes' do
+      doc = filter(link('//doc/api/README.md'))
+      expect(doc.at_css('a')['href']).to eq '//doc/api/README.md'
+    end
+
     it 'rebuilds relative URL for a file in the repo' do
       doc = filter(link('doc/api/README.md'))
       expect(doc.at_css('a')['href']).
@@ -184,4 +196,13 @@ describe Banzai::Filter::RelativeLinkFilter, lib: true do
       include_examples :relative_to_requested
     end
   end
+
+  context 'with a valid commit' do
+    include_examples :valid_repository
+  end
+
+  context 'with a valid ref' do
+    let(:commit) { nil } # force filter to use ref instead of commit
+    include_examples :valid_repository
+  end
 end
diff --git a/spec/lib/banzai/filter/video_link_filter_spec.rb b/spec/lib/banzai/filter/video_link_filter_spec.rb
index cc4349f80ba5aa18f2f60c44b53afd3b37062d1b..6ab1be9ccb70e879d0c23369cf1c2bd7812411e1 100644
--- a/spec/lib/banzai/filter/video_link_filter_spec.rb
+++ b/spec/lib/banzai/filter/video_link_filter_spec.rb
@@ -47,5 +47,4 @@ describe Banzai::Filter::VideoLinkFilter, lib: true do
       expect(element['src']).to eq '/path/my_image.jpg'
     end
   end
-
 end
diff --git a/spec/lib/ci/charts_spec.rb b/spec/lib/ci/charts_spec.rb
index 97f2e97b062711776540b709949b38c6372c9b9c..fb6cc398307807511dfb7e4d9fbba836f56aead5 100644
--- a/spec/lib/ci/charts_spec.rb
+++ b/spec/lib/ci/charts_spec.rb
@@ -2,21 +2,23 @@ require 'spec_helper'
 
 describe Ci::Charts, lib: true do
   context "build_times" do
+    let(:project) { create(:empty_project) }
+    let(:chart) { Ci::Charts::BuildTime.new(project) }
+
+    subject { chart.build_times }
+
     before do
-      @pipeline = FactoryGirl.create(:ci_pipeline)
-      FactoryGirl.create(:ci_build, pipeline: @pipeline)
+      create(:ci_empty_pipeline, project: project, duration: 120)
     end
 
-    it 'should return build times in minutes' do
-      chart = Ci::Charts::BuildTime.new(@pipeline.project)
-      expect(chart.build_times).to eq([2])
+    it 'returns build times in minutes' do
+      is_expected.to contain_exactly(2)
     end
 
-    it 'should handle nil build times' do
-      create(:ci_pipeline, duration: nil, project: @pipeline.project)
+    it 'handles nil build times' do
+      create(:ci_empty_pipeline, project: project, duration: nil)
 
-      chart = Ci::Charts::BuildTime.new(@pipeline.project)
-      expect(chart.build_times).to eq([2, 0])
+      is_expected.to contain_exactly(2, 0)
     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 61490555ff5a4039954e684d1148e5ccf90a825f..be51d942af7c140d00f5e5459e7fece59469b04c 100644
--- a/spec/lib/ci/gitlab_ci_yaml_processor_spec.rb
+++ b/spec/lib/ci/gitlab_ci_yaml_processor_spec.rb
@@ -19,7 +19,7 @@ module Ci
         expect(config_processor.builds_for_stage_and_ref(type, "master").first).to eq({
           stage: "test",
           stage_idx: 1,
-          name: :rspec,
+          name: "rspec",
           commands: "pwd\nrspec",
           tag_list: [],
           options: {},
@@ -433,7 +433,7 @@ module Ci
         expect(config_processor.builds_for_stage_and_ref("test", "master").first).to eq({
           stage: "test",
           stage_idx: 1,
-          name: :rspec,
+          name: "rspec",
           commands: "pwd\nrspec",
           tag_list: [],
           options: {
@@ -461,7 +461,7 @@ module Ci
         expect(config_processor.builds_for_stage_and_ref("test", "master").first).to eq({
           stage: "test",
           stage_idx: 1,
-          name: :rspec,
+          name: "rspec",
           commands: "pwd\nrspec",
           tag_list: [],
           options: {
@@ -533,10 +533,6 @@ module Ci
           }
         end
 
-        context 'when also global variables are defined' do
-
-        end
-
         context 'when syntax is correct' do
           let(:variables) do
             { VAR1: 'value1', VAR2: 'value2' }
@@ -704,7 +700,7 @@ module Ci
         expect(config_processor.builds_for_stage_and_ref("test", "master").first).to eq({
           stage: "test",
           stage_idx: 1,
-          name: :rspec,
+          name: "rspec",
           commands: "pwd\nrspec",
           tag_list: [],
           options: {
@@ -841,7 +837,7 @@ module Ci
           expect(subject.first).to eq({
             stage: "test",
             stage_idx: 1,
-            name: :normal_job,
+            name: "normal_job",
             commands: "test",
             tag_list: [],
             options: {},
@@ -886,7 +882,7 @@ module Ci
           expect(subject.first).to eq({
             stage: "build",
             stage_idx: 0,
-            name: :job1,
+            name: "job1",
             commands: "execute-script-for-job",
             tag_list: [],
             options: {},
@@ -898,7 +894,7 @@ module Ci
           expect(subject.second).to eq({
             stage: "build",
             stage_idx: 0,
-            name: :job2,
+            name: "job2",
             commands: "execute-script-for-job",
             tag_list: [],
             options: {},
diff --git a/spec/lib/disable_email_interceptor_spec.rb b/spec/lib/disable_email_interceptor_spec.rb
index 309a88151cf88ca6cf25f49a838124ce7ef75a53..8f51474476d7575ffec2fd29244d4d0b443e349c 100644
--- a/spec/lib/disable_email_interceptor_spec.rb
+++ b/spec/lib/disable_email_interceptor_spec.rb
@@ -5,7 +5,7 @@ describe DisableEmailInterceptor, lib: true do
     Mail.register_interceptor(DisableEmailInterceptor)
   end
 
-  it 'should not send emails' do
+  it 'does not send emails' do
     allow(Gitlab.config.gitlab).to receive(:email_enabled).and_return(false)
     expect { deliver_mail }.not_to change(ActionMailer::Base.deliveries, :count)
   end
diff --git a/spec/lib/extracts_path_spec.rb b/spec/lib/extracts_path_spec.rb
index 566035c60d0d24c46cbd1719d5456984758bc2e8..86d04ecfa36b0777b2ec5da99ecabdf53ba7381e 100644
--- a/spec/lib/extracts_path_spec.rb
+++ b/spec/lib/extracts_path_spec.rb
@@ -25,18 +25,30 @@ describe ExtractsPath, lib: true do
       @project = create(:project)
     end
 
-    it "log tree path should have no escape sequences" do
+    it "log tree path has no escape sequences" do
       assign_ref_vars
       expect(@logs_path).to eq("/#{@project.path_with_namespace}/refs/#{ref}/logs_tree/files/ruby/popen.rb")
     end
 
-    context 'escaped sequences in ref' do
-      let(:ref) { "improve%2Fawesome" }
+    context 'ref contains %20' do
+      let(:ref) { 'foo%20bar' }
 
-      it "id should have no escape sequences" do
+      it 'is not converted to a space in @id' do
+        @project.repository.add_branch(@project.owner, 'foo%20bar', 'master')
+
+        assign_ref_vars
+
+        expect(@id).to start_with('foo%20bar/')
+      end
+    end
+
+    context 'path contains space' do
+      let(:params) { { path: 'with space', ref: '38008cb17ce1466d8fec2dfa6f6ab8dcfe5cf49e' } }
+
+      it 'is not converted to %20 in @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(@path).to eq(params[:path])
       end
     end
   end
diff --git a/spec/lib/gitlab/akismet_helper_spec.rb b/spec/lib/gitlab/akismet_helper_spec.rb
deleted file mode 100644
index b08396da4d20cf85ca363a254e851c0a8aba76f1..0000000000000000000000000000000000000000
--- a/spec/lib/gitlab/akismet_helper_spec.rb
+++ /dev/null
@@ -1,35 +0,0 @@
-require 'spec_helper'
-
-describe Gitlab::AkismetHelper, type: :helper do
-  let(:project) { create(:project, :public) }
-  let(:user) { create(:user) }
-
-  before do
-    allow(Gitlab.config.gitlab).to receive(:url).and_return(Settings.send(:build_gitlab_url))
-    allow_any_instance_of(ApplicationSetting).to receive(:akismet_enabled).and_return(true)
-    allow_any_instance_of(ApplicationSetting).to receive(:akismet_api_key).and_return('12345')
-  end
-
-  describe '#check_for_spam?' do
-    it 'returns true for public project' do
-      expect(helper.check_for_spam?(project)).to eq(true)
-    end
-
-    it 'returns false for private project' do
-      project.update_attribute(:visibility_level, Gitlab::VisibilityLevel::PRIVATE)
-      expect(helper.check_for_spam?(project)).to eq(false)
-    end
-  end
-
-  describe '#is_spam?' do
-    it 'returns true for spam' do
-      environment = {
-        'action_dispatch.remote_ip' => '127.0.0.1',
-        'HTTP_USER_AGENT' => 'Test User Agent'
-      }
-
-      allow_any_instance_of(::Akismet::Client).to receive(:check).and_return([true, true])
-      expect(helper.is_spam?(environment, user, 'Is this spam?')).to eq(true)
-    end
-  end
-end
diff --git a/spec/lib/gitlab/asciidoc_spec.rb b/spec/lib/gitlab/asciidoc_spec.rb
index 32ca8239845ef2474d9193f7de177a1d457b4c08..4aba783dc334a1732bc2d9f01e9dbbc43155988b 100644
--- a/spec/lib/gitlab/asciidoc_spec.rb
+++ b/spec/lib/gitlab/asciidoc_spec.rb
@@ -8,7 +8,7 @@ module Gitlab
     let(:html) { 'H<sub>2</sub>O' }
 
     context "without project" do
-      it "should convert the input using Asciidoctor and default options" do
+      it "converts the input using Asciidoctor and default options" do
         expected_asciidoc_opts = {
             safe: :secure,
             backend: :html5,
@@ -24,7 +24,7 @@ module Gitlab
       context "with asciidoc_opts" do
         let(:asciidoc_opts) { { safe: :safe, attributes: ['foo'] } }
 
-        it "should merge the options with default ones" do
+        it "merges the options with default ones" do
           expected_asciidoc_opts = {
               safe: :safe,
               backend: :html5,
diff --git a/spec/lib/gitlab/auth_spec.rb b/spec/lib/gitlab/auth_spec.rb
index 7bec1367156e556f2aa58734338f48c0854a5296..b0772cad3123cf6ec9d78e34526cdbc263ce4570 100644
--- a/spec/lib/gitlab/auth_spec.rb
+++ b/spec/lib/gitlab/auth_spec.rb
@@ -51,24 +51,24 @@ describe Gitlab::Auth, lib: true do
     let(:username) { 'John' }     # username isn't lowercase, test this
     let(:password) { 'my-secret' }
 
-    it "should find user by valid login/password" do
+    it "finds user by valid login/password" do
       expect( gl_auth.find_with_user_password(username, password) ).to eql user
     end
 
-    it 'should find user by valid email/password with case-insensitive email' do
+    it 'finds user by valid email/password with case-insensitive email' do
       expect(gl_auth.find_with_user_password(user.email.upcase, password)).to eql user
     end
 
-    it 'should find user by valid username/password with case-insensitive username' do
+    it 'finds user by valid username/password with case-insensitive username' do
       expect(gl_auth.find_with_user_password(username.upcase, password)).to eql user
     end
 
-    it "should not find user with invalid password" do
+    it "does not find user with invalid password" do
       password = 'wrong'
       expect( gl_auth.find_with_user_password(username, password) ).not_to eql user
     end
 
-    it "should not find user with invalid login" do
+    it "does not find user with invalid login" do
       user = 'wrong'
       expect( gl_auth.find_with_user_password(username, password) ).not_to eql user
     end
diff --git a/spec/lib/gitlab/badge/build/metadata_spec.rb b/spec/lib/gitlab/badge/build/metadata_spec.rb
new file mode 100644
index 0000000000000000000000000000000000000000..d678e52272100948308aa3e920c0540a82dcbe49
--- /dev/null
+++ b/spec/lib/gitlab/badge/build/metadata_spec.rb
@@ -0,0 +1,27 @@
+require 'spec_helper'
+require 'lib/gitlab/badge/shared/metadata'
+
+describe Gitlab::Badge::Build::Metadata do
+  let(:badge) { double(project: create(:project), ref: 'feature') }
+  let(:metadata) { described_class.new(badge) }
+
+  it_behaves_like 'badge metadata'
+
+  describe '#title' do
+    it 'returns build status title' do
+      expect(metadata.title).to eq 'build status'
+    end
+  end
+
+  describe '#image_url' do
+    it 'returns valid url' do
+      expect(metadata.image_url).to include 'badges/feature/build.svg'
+    end
+  end
+
+  describe '#link_url' do
+    it 'returns valid link' do
+      expect(metadata.link_url).to include 'commits/feature'
+    end
+  end
+end
diff --git a/spec/lib/gitlab/badge/build/status_spec.rb b/spec/lib/gitlab/badge/build/status_spec.rb
new file mode 100644
index 0000000000000000000000000000000000000000..38eebb2a1769b2a25656e77b3c4239c08e9e25ca
--- /dev/null
+++ b/spec/lib/gitlab/badge/build/status_spec.rb
@@ -0,0 +1,94 @@
+require 'spec_helper'
+
+describe Gitlab::Badge::Build::Status do
+  let(:project) { create(:project) }
+  let(:sha) { project.commit.sha }
+  let(:branch) { 'master' }
+  let(:badge) { described_class.new(project, branch) }
+
+  describe '#entity' do
+    it 'always says build' do
+      expect(badge.entity).to eq 'build'
+    end
+  end
+
+  describe '#template' do
+    it 'returns badge template' do
+      expect(badge.template.key_text).to eq 'build'
+    end
+  end
+
+  describe '#metadata' do
+    it 'returns badge metadata' do
+      expect(badge.metadata.image_url)
+        .to include 'badges/master/build.svg'
+    end
+  end
+
+  context 'build exists' do
+    let!(:build) { create_build(project, sha, branch) }
+
+    context 'build success' do
+      before { build.success! }
+
+      describe '#status' do
+        it 'is successful' do
+          expect(badge.status).to eq 'success'
+        end
+      end
+    end
+
+    context 'build failed' do
+      before { build.drop! }
+
+      describe '#status' do
+        it 'failed' do
+          expect(badge.status).to eq 'failed'
+        end
+      end
+    end
+
+    context 'when outdated pipeline for given ref exists' do
+      before do
+        build.success!
+
+        old_build = create_build(project, '11eeffdd', branch)
+        old_build.drop!
+      end
+
+      it 'does not take outdated pipeline into account' do
+        expect(badge.status).to eq 'success'
+      end
+    end
+
+    context 'when multiple pipelines exist for given sha' do
+      before do
+        build.drop!
+
+        new_build = create_build(project, sha, branch)
+        new_build.success!
+      end
+
+      it 'reports the compound status' do
+        expect(badge.status).to eq 'failed'
+      end
+    end
+  end
+
+  context 'build does not exist' do
+    describe '#status' do
+      it 'is unknown' do
+        expect(badge.status).to eq 'unknown'
+      end
+    end
+  end
+
+  def create_build(project, sha, branch)
+    pipeline = create(:ci_empty_pipeline,
+                      project: project,
+                      sha: sha,
+                      ref: branch)
+
+    create(:ci_build, pipeline: pipeline, stage: 'notify')
+  end
+end
diff --git a/spec/lib/gitlab/badge/build/template_spec.rb b/spec/lib/gitlab/badge/build/template_spec.rb
new file mode 100644
index 0000000000000000000000000000000000000000..a7e21fb8bb1dd84e6cd10490948705c17f989a4e
--- /dev/null
+++ b/spec/lib/gitlab/badge/build/template_spec.rb
@@ -0,0 +1,82 @@
+require 'spec_helper'
+
+describe Gitlab::Badge::Build::Template do
+  let(:badge) { double(entity: 'build', status: 'success') }
+  let(:template) { described_class.new(badge) }
+
+  describe '#key_text' do
+    it 'is always says build' do
+      expect(template.key_text).to eq 'build'
+    end
+  end
+
+  describe '#value_text' do
+    it 'is status value' do
+      expect(template.value_text).to eq 'success'
+    end
+  end
+
+  describe 'widths and text anchors' do
+    it 'has fixed width and text anchors' do
+      expect(template.width).to eq 92
+      expect(template.key_width).to eq 38
+      expect(template.value_width).to eq 54
+      expect(template.key_text_anchor).to eq 19
+      expect(template.value_text_anchor).to eq 65
+    end
+  end
+
+  describe '#key_color' do
+    it 'is always the same' do
+      expect(template.key_color).to eq '#555'
+    end
+  end
+
+  describe '#value_color' do
+    context 'when status is success' do
+      it 'has expected color' do
+        expect(template.value_color).to eq '#4c1'
+      end
+    end
+
+    context 'when status is failed' do
+      before do
+        allow(badge).to receive(:status).and_return('failed')
+      end
+
+      it 'has expected color' do
+        expect(template.value_color).to eq '#e05d44'
+      end
+    end
+
+    context 'when status is running' do
+      before do
+        allow(badge).to receive(:status).and_return('running')
+      end
+
+      it 'has expected color' do
+        expect(template.value_color).to eq '#dfb317'
+      end
+    end
+
+    context 'when status is unknown' do
+      before do
+        allow(badge).to receive(:status).and_return('unknown')
+      end
+
+      it 'has expected color' do
+        expect(template.value_color).to eq '#9f9f9f'
+      end
+    end
+
+    context 'when status does not match any known statuses' do
+      before do
+        allow(badge).to receive(:status).and_return('invalid')
+      end
+
+      it 'has expected color' do
+        expect(template.value_color).to eq '#9f9f9f'
+      end
+    end
+  end
+end
diff --git a/spec/lib/gitlab/badge/build_spec.rb b/spec/lib/gitlab/badge/build_spec.rb
deleted file mode 100644
index f3b522a02f52d173e5fa06f0a8e09775c5dca69c..0000000000000000000000000000000000000000
--- a/spec/lib/gitlab/badge/build_spec.rb
+++ /dev/null
@@ -1,123 +0,0 @@
-require 'spec_helper'
-
-describe Gitlab::Badge::Build do
-  let(:project) { create(:project) }
-  let(:sha) { project.commit.sha }
-  let(:branch) { 'master' }
-  let(:badge) { described_class.new(project, branch) }
-
-  describe '#type' do
-    subject { badge.type }
-    it { is_expected.to eq 'image/svg+xml' }
-  end
-
-  describe '#to_html' do
-    let(:html) { Nokogiri::HTML.parse(badge.to_html) }
-    let(:a_href) { html.at('a') }
-
-    it 'points to link' do
-      expect(a_href[:href]).to eq badge.link_url
-    end
-
-    it 'contains clickable image' do
-      expect(a_href.children.first.name).to eq 'img'
-    end
-  end
-
-  describe '#to_markdown' do
-    subject { badge.to_markdown }
-
-    it { is_expected.to include badge.image_url }
-    it { is_expected.to include badge.link_url }
-  end
-
-  describe '#image_url' do
-    subject { badge.image_url }
-    it { is_expected.to include "badges/#{branch}/build.svg" }
-  end
-
-  describe '#link_url' do
-    subject { badge.link_url }
-    it { is_expected.to include "commits/#{branch}" }
-  end
-
-  context 'build exists' do
-    let!(:build) { create_build(project, sha, branch) }
-
-    context 'build success' do
-      before { build.success! }
-
-      describe '#to_s' do
-        subject { badge.to_s }
-        it { is_expected.to eq 'build-success' }
-      end
-
-      describe '#data' do
-        let(:data) { badge.data }
-
-        it 'contains information about success' do
-          expect(status_node(data, 'success')).to be_truthy
-        end
-      end
-    end
-
-    context 'build failed' do
-      before { build.drop! }
-
-      describe '#to_s' do
-        subject { badge.to_s }
-        it { is_expected.to eq 'build-failed' }
-      end
-
-      describe '#data' do
-        let(:data) { badge.data }
-
-        it 'contains information about failure' do
-          expect(status_node(data, 'failed')).to be_truthy
-        end
-      end
-    end
-  end
-
-  context 'build does not exist' do
-    describe '#to_s' do
-      subject { badge.to_s }
-      it { is_expected.to eq 'build-unknown' }
-    end
-
-    describe '#data' do
-      let(:data) { badge.data }
-
-      it 'contains infromation about unknown build' do
-        expect(status_node(data, 'unknown')).to be_truthy
-      end
-    end
-  end
-
-  context 'when outdated pipeline for given ref exists' do
-    before do
-      build = create_build(project, sha, branch)
-      build.success!
-
-      old_build = create_build(project, '11eeffdd', branch)
-      old_build.drop!
-    end
-
-    it 'does not take outdated pipeline into account' do
-      expect(badge.to_s).to eq 'build-success'
-    end
-  end
-
-  def create_build(project, sha, branch)
-    pipeline = create(:ci_pipeline, project: project,
-                                    sha: sha,
-                                    ref: branch)
-
-    create(:ci_build, pipeline: pipeline, stage: 'notify')
-  end
-
-  def status_node(data, status)
-    xml = Nokogiri::XML.parse(data)
-    xml.at(%Q{text:contains("#{status}")})
-  end
-end
diff --git a/spec/lib/gitlab/badge/coverage/metadata_spec.rb b/spec/lib/gitlab/badge/coverage/metadata_spec.rb
new file mode 100644
index 0000000000000000000000000000000000000000..74eaf7eaf8bdac93c5ed46749e3c31d4423a3ceb
--- /dev/null
+++ b/spec/lib/gitlab/badge/coverage/metadata_spec.rb
@@ -0,0 +1,30 @@
+require 'spec_helper'
+require 'lib/gitlab/badge/shared/metadata'
+
+describe Gitlab::Badge::Coverage::Metadata do
+  let(:badge) do
+    double(project: create(:project), ref: 'feature', job: 'test')
+  end
+
+  let(:metadata) { described_class.new(badge) }
+
+  it_behaves_like 'badge metadata'
+
+  describe '#title' do
+    it 'returns coverage report title' do
+      expect(metadata.title).to eq 'coverage report'
+    end
+  end
+
+  describe '#image_url' do
+    it 'returns valid url' do
+      expect(metadata.image_url).to include 'badges/feature/coverage.svg'
+    end
+  end
+
+  describe '#link_url' do
+    it 'returns valid link' do
+      expect(metadata.link_url).to include 'commits/feature'
+    end
+  end
+end
diff --git a/spec/lib/gitlab/badge/coverage/report_spec.rb b/spec/lib/gitlab/badge/coverage/report_spec.rb
new file mode 100644
index 0000000000000000000000000000000000000000..ab0cce6e09104a8a6de11e105f00cf1fa415014b
--- /dev/null
+++ b/spec/lib/gitlab/badge/coverage/report_spec.rb
@@ -0,0 +1,106 @@
+require 'spec_helper'
+
+describe Gitlab::Badge::Coverage::Report do
+  let(:project) { create(:project) }
+  let(:job_name) { nil }
+
+  let(:badge) do
+    described_class.new(project, 'master', job_name)
+  end
+
+  describe '#entity' do
+    it 'describes a coverage' do
+      expect(badge.entity).to eq 'coverage'
+    end
+  end
+
+  describe '#metadata' do
+    it 'returns correct metadata' do
+      expect(badge.metadata.image_url).to include 'coverage.svg'
+    end
+  end
+
+  describe '#template' do
+    it 'returns correct template' do
+      expect(badge.template.key_text).to eq 'coverage'
+    end
+  end
+
+  shared_examples 'unknown coverage report' do
+    context 'particular job specified' do
+      let(:job_name) { '' }
+
+      it 'returns nil' do
+        expect(badge.status).to be_nil
+      end
+    end
+
+    context 'particular job not specified' do
+      let(:job_name) { nil }
+
+      it 'returns nil' do
+        expect(badge.status).to be_nil
+      end
+    end
+  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
+
+      create_pipeline do |pipeline|
+        create(:ci_build, :failed, pipeline: pipeline, coverage: 10)
+      end
+    end
+
+    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
+    end
+
+    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
+
+  context 'when only failed pipeline exists' do
+    before do
+      create_pipeline do |pipeline|
+        create(:ci_build, :failed, pipeline: pipeline, coverage: 10)
+      end
+    end
+
+    it_behaves_like 'unknown coverage report'
+
+    context 'particular job specified' do
+      let(:job_name) { 'nonexistent' }
+
+      it 'retruns nil' do
+        expect(badge.status).to be_nil
+      end
+    end
+  end
+
+  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.build_updated
+    end
+  end
+end
diff --git a/spec/lib/gitlab/badge/coverage/template_spec.rb b/spec/lib/gitlab/badge/coverage/template_spec.rb
new file mode 100644
index 0000000000000000000000000000000000000000..383bae6e08734f84f3f2f8c4720e2a1d3ba04a43
--- /dev/null
+++ b/spec/lib/gitlab/badge/coverage/template_spec.rb
@@ -0,0 +1,130 @@
+require 'spec_helper'
+
+describe Gitlab::Badge::Coverage::Template do
+  let(:badge) { double(entity: 'coverage', status: 90) }
+  let(:template) { described_class.new(badge) }
+
+  describe '#key_text' do
+    it 'is always says coverage' do
+      expect(template.key_text).to eq 'coverage'
+    end
+  end
+
+  describe '#value_text' do
+    context 'when coverage is known' do
+      it 'returns coverage percentage' do
+        expect(template.value_text).to eq '90%'
+      end
+    end
+
+    context 'when coverage is unknown' do
+      before do
+        allow(badge).to receive(:status).and_return(nil)
+      end
+
+      it 'returns string that says coverage is unknown' do
+        expect(template.value_text).to eq 'unknown'
+      end
+    end
+  end
+
+  describe '#key_width' do
+    it 'has a fixed key width' do
+      expect(template.key_width).to eq 62
+    end
+  end
+
+  describe '#value_width' do
+    context 'when coverage is known' do
+      it 'is narrower when coverage is known' do
+        expect(template.value_width).to eq 36
+      end
+    end
+
+    context 'when coverage is unknown' do
+      before do
+        allow(badge).to receive(:status).and_return(nil)
+      end
+
+      it 'is wider when coverage is unknown to fit text' do
+        expect(template.value_width).to eq 58
+      end
+    end
+  end
+
+  describe '#key_color' do
+    it 'always has the same color' do
+      expect(template.key_color).to eq '#555'
+    end
+  end
+
+  describe '#value_color' do
+    context 'when coverage is good' do
+      before do
+        allow(badge).to receive(:status).and_return(98)
+      end
+
+      it 'is green' do
+        expect(template.value_color).to eq '#4c1'
+      end
+    end
+
+    context 'when coverage is acceptable' do
+      before do
+        allow(badge).to receive(:status).and_return(90)
+      end
+
+      it 'is green-orange' do
+        expect(template.value_color).to eq '#a3c51c'
+      end
+    end
+
+    context 'when coverage is medium' do
+      before do
+        allow(badge).to receive(:status).and_return(75)
+      end
+
+      it 'is orange-yellow' do
+        expect(template.value_color).to eq '#dfb317'
+      end
+    end
+
+    context 'when coverage is low' do
+      before do
+        allow(badge).to receive(:status).and_return(50)
+      end
+
+      it 'is red' do
+        expect(template.value_color).to eq '#e05d44'
+      end
+    end
+
+    context 'when coverage is unknown' do
+      before do
+        allow(badge).to receive(:status).and_return(nil)
+      end
+
+      it 'is grey' do
+        expect(template.value_color).to eq '#9f9f9f'
+      end
+    end
+  end
+
+  describe '#width' do
+    context 'when coverage is known' do
+      it 'returns the key width plus value width' do
+        expect(template.width).to eq 98
+      end
+    end
+
+    context 'when coverage is unknown' do
+      before do
+        allow(badge).to receive(:status).and_return(nil)
+      end
+
+      it 'returns key width plus wider value width' do
+        expect(template.width).to eq 120
+      end
+    end
+  end
+end
diff --git a/spec/lib/gitlab/badge/shared/metadata.rb b/spec/lib/gitlab/badge/shared/metadata.rb
new file mode 100644
index 0000000000000000000000000000000000000000..0cf1851425136356186812cd0d2a703ac3ce7ff3
--- /dev/null
+++ b/spec/lib/gitlab/badge/shared/metadata.rb
@@ -0,0 +1,21 @@
+shared_examples 'badge metadata' do
+  describe '#to_html' do
+    let(:html) { Nokogiri::HTML.parse(metadata.to_html) }
+    let(:a_href) { html.at('a') }
+
+    it 'points to link' do
+      expect(a_href[:href]).to eq metadata.link_url
+    end
+
+    it 'contains clickable image' do
+      expect(a_href.children.first.name).to eq 'img'
+    end
+  end
+
+  describe '#to_markdown' do
+    subject { metadata.to_markdown }
+
+    it { is_expected.to include metadata.image_url }
+    it { is_expected.to include metadata.link_url }
+  end
+end
diff --git a/spec/lib/gitlab/changes_list_spec.rb b/spec/lib/gitlab/changes_list_spec.rb
new file mode 100644
index 0000000000000000000000000000000000000000..69d86144e321fb4a73eff64f1ab2cf456a32b669
--- /dev/null
+++ b/spec/lib/gitlab/changes_list_spec.rb
@@ -0,0 +1,30 @@
+require "spec_helper"
+
+describe Gitlab::ChangesList do
+  let(:valid_changes_string) { "\n000000 570e7b2 refs/heads/my_branch\nd14d6c 6fd24d refs/heads/master" }
+  let(:invalid_changes) { 1 }
+
+  context 'when changes is a valid string' do
+    let(:changes_list) { Gitlab::ChangesList.new(valid_changes_string) }
+
+    it 'splits elements by newline character' do
+      expect(changes_list).to contain_exactly({
+        oldrev: "000000",
+        newrev: "570e7b2",
+        ref: "refs/heads/my_branch"
+      }, {
+        oldrev: "d14d6c",
+        newrev: "6fd24d",
+        ref: "refs/heads/master"
+      })
+    end
+
+    it 'behaves like a list' do
+      expect(changes_list.first).to eq({
+        oldrev: "000000",
+        newrev: "570e7b2",
+        ref: "refs/heads/my_branch"
+      })
+    end
+  end
+end
diff --git a/spec/lib/gitlab/checks/change_access_spec.rb b/spec/lib/gitlab/checks/change_access_spec.rb
new file mode 100644
index 0000000000000000000000000000000000000000..39069b49978e5a309a87f7f2d94cc7b110553e07
--- /dev/null
+++ b/spec/lib/gitlab/checks/change_access_spec.rb
@@ -0,0 +1,99 @@
+require 'spec_helper'
+
+describe Gitlab::Checks::ChangeAccess, lib: true do
+  describe '#exec' do
+    let(:user) { create(:user) }
+    let(:project) { create(:project) }
+    let(:user_access) { Gitlab::UserAccess.new(user, project: project) }
+    let(:changes) do
+      {
+        oldrev: 'be93687618e4b132087f430a4d8fc3a609c9b77c',
+        newrev: '54fcc214b94e78d7a41a9a8fe6d87a5e59500e51',
+        ref: 'refs/heads/master'
+      }
+    end
+
+    subject { described_class.new(changes, project: project, user_access: user_access).exec }
+
+    before { allow(user_access).to receive(:can_do_action?).with(:push_code).and_return(true) }
+
+    context 'without failed checks' do
+      it "doesn't return any error" do
+        expect(subject.status).to be(true)
+      end
+    end
+
+    context 'when the user is not allowed to push code' do
+      it 'returns an error' do
+        expect(user_access).to receive(:can_do_action?).with(:push_code).and_return(false)
+
+        expect(subject.status).to be(false)
+        expect(subject.message).to eq('You are not allowed to push code to this project.')
+      end
+    end
+
+    context 'tags check' do
+      let(:changes) do
+        {
+          oldrev: 'be93687618e4b132087f430a4d8fc3a609c9b77c',
+          newrev: '54fcc214b94e78d7a41a9a8fe6d87a5e59500e51',
+          ref: 'refs/tags/v1.0.0'
+        }
+      end
+
+      it 'returns an error if the user is not allowed to update tags' do
+        expect(user_access).to receive(:can_do_action?).with(:admin_project).and_return(false)
+
+        expect(subject.status).to be(false)
+        expect(subject.message).to eq('You are not allowed to change existing tags on this project.')
+      end
+    end
+
+    context 'protected branches check' do
+      before do
+        allow(project).to receive(:protected_branch?).with('master').and_return(true)
+      end
+
+      it 'returns an error if the user is not allowed to do forced pushes to protected branches' do
+        expect(Gitlab::Checks::ForcePush).to receive(:force_push?).and_return(true)
+        expect(user_access).to receive(:can_do_action?).with(:force_push_code_to_protected_branches).and_return(false)
+
+        expect(subject.status).to be(false)
+        expect(subject.message).to eq('You are not allowed to force push code to a protected branch on this project.')
+      end
+
+      it 'returns an error if the user is not allowed to merge to protected branches' do
+        expect_any_instance_of(Gitlab::Checks::MatchingMergeRequest).to receive(:match?).and_return(true)
+        expect(user_access).to receive(:can_merge_to_branch?).and_return(false)
+        expect(user_access).to receive(:can_push_to_branch?).and_return(false)
+
+        expect(subject.status).to be(false)
+        expect(subject.message).to eq('You are not allowed to merge code into protected branches on this project.')
+      end
+
+      it 'returns an error if the user is not allowed to push to protected branches' do
+        expect(user_access).to receive(:can_push_to_branch?).and_return(false)
+
+        expect(subject.status).to be(false)
+        expect(subject.message).to eq('You are not allowed to push code to protected branches on this project.')
+      end
+
+      context 'branch deletion' do
+        let(:changes) do
+          {
+            oldrev: 'be93687618e4b132087f430a4d8fc3a609c9b77c',
+            newrev: '0000000000000000000000000000000000000000',
+            ref: 'refs/heads/master'
+          }
+        end
+
+        it 'returns an error if the user is not allowed to delete protected branches' do
+          expect(user_access).to receive(:can_do_action?).with(:remove_protected_branches).and_return(false)
+
+          expect(subject.status).to be(false)
+          expect(subject.message).to eq('You are not allowed to delete protected branches from this project.')
+        end
+      end
+    end
+  end
+end
diff --git a/spec/lib/gitlab/ci/config/node/validatable_spec.rb b/spec/lib/gitlab/ci/config/node/validatable_spec.rb
index 10cd01afcd1da4e16314af3ab8a0ca7f34a493ff..64b77fd6e0356f00ec11ed2849d4b2c7845ada96 100644
--- a/spec/lib/gitlab/ci/config/node/validatable_spec.rb
+++ b/spec/lib/gitlab/ci/config/node/validatable_spec.rb
@@ -23,6 +23,10 @@ describe Gitlab::Ci::Config::Node::Validatable do
         .to be Gitlab::Ci::Config::Node::Validator
     end
 
+    it 'returns only one validator to mitigate leaks' do
+      expect { node.validator }.not_to change { node.validator }
+    end
+
     context 'when validating node instance' do
       let(:node_instance) { node.new }
 
diff --git a/spec/lib/gitlab/closing_issue_extractor_spec.rb b/spec/lib/gitlab/closing_issue_extractor_spec.rb
index e9b8ce6b5bb184523eb0f03d8c12b339ed13ff5f..de3f64249a25b29b63b01fbf95ecc6bff9b2c202 100644
--- a/spec/lib/gitlab/closing_issue_extractor_spec.rb
+++ b/spec/lib/gitlab/closing_issue_extractor_spec.rb
@@ -3,10 +3,12 @@ require 'spec_helper'
 describe Gitlab::ClosingIssueExtractor, lib: true do
   let(:project)   { create(:project) }
   let(:project2)   { create(:project) }
+  let(:forked_project) { Projects::ForkService.new(project, project.creator).execute }
   let(:issue)     { create(:issue, project: project) }
   let(:issue2)     { create(:issue, project: project2) }
   let(:reference) { issue.to_reference }
   let(:cross_reference) { issue2.to_reference(project) }
+  let(:fork_cross_reference) { issue.to_reference(forked_project) }
 
   subject { described_class.new(project, project.creator) }
 
@@ -278,6 +280,15 @@ describe Gitlab::ClosingIssueExtractor, lib: true do
       end
     end
 
+    context "with a cross-project fork reference" do
+      subject { described_class.new(forked_project, forked_project.creator) }
+
+      it do
+        message = "Closes #{fork_cross_reference}"
+        expect(subject.closed_by_message(message)).to be_empty
+      end
+    end
+
     context "with an invalid URL" do
       it do
         message = "Closes https://google.com#{urls.namespace_project_issue_path(issue2.project.namespace, issue2.project, issue2)}"
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..60020487061b849dcda0dd40dea6ca0c783eea37
--- /dev/null
+++ b/spec/lib/gitlab/conflict/file_spec.rb
@@ -0,0 +1,261 @@
+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
+  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..65a828accdec47c9d32567c9a2a1dcab8a8ad4d1
--- /dev/null
+++ b/spec/lib/gitlab/conflict/parser_spec.rb
@@ -0,0 +1,188 @@
+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 100 KB' do
+        expect { parse_text('a' * 102401) }.
+          to raise_error(Gitlab::Conflict::Parser::UnmergeableFile)
+      end
+    end
+  end
+end
diff --git a/spec/lib/gitlab/build_data_builder_spec.rb b/spec/lib/gitlab/data_builder/build_spec.rb
similarity index 88%
rename from spec/lib/gitlab/build_data_builder_spec.rb
rename to spec/lib/gitlab/data_builder/build_spec.rb
index 23ae5cfacc4c0e256cb8d449b01679ff06a1513c..6c71e98066bf0b90487fa8a8144269e6594d63d9 100644
--- a/spec/lib/gitlab/build_data_builder_spec.rb
+++ b/spec/lib/gitlab/data_builder/build_spec.rb
@@ -1,11 +1,11 @@
 require 'spec_helper'
 
-describe 'Gitlab::BuildDataBuilder' do
+describe Gitlab::DataBuilder::Build do
   let(:build) { create(:ci_build) }
 
   describe '.build' do
     let(:data) do
-      Gitlab::BuildDataBuilder.build(build)
+      described_class.build(build)
     end
 
     it { expect(data).to be_a(Hash) }
diff --git a/spec/lib/gitlab/note_data_builder_spec.rb b/spec/lib/gitlab/data_builder/note_spec.rb
similarity index 97%
rename from spec/lib/gitlab/note_data_builder_spec.rb
rename to spec/lib/gitlab/data_builder/note_spec.rb
index 3d6bcdfd873821e90176f937b8f3251d96455395..9a4dec91e56389a837c0aebfcb05391da8da64fd 100644
--- a/spec/lib/gitlab/note_data_builder_spec.rb
+++ b/spec/lib/gitlab/data_builder/note_spec.rb
@@ -1,9 +1,9 @@
 require 'spec_helper'
 
-describe 'Gitlab::NoteDataBuilder', lib: true do
+describe Gitlab::DataBuilder::Note, lib: true do
   let(:project) { create(:project) }
   let(:user) { create(:user) }
-  let(:data) { Gitlab::NoteDataBuilder.build(note, user) }
+  let(:data) { described_class.build(note, user) }
   let(:fixed_time) { Time.at(1425600000) } # Avoid time precision errors
 
   before(:each) do
diff --git a/spec/lib/gitlab/data_builder/pipeline_spec.rb b/spec/lib/gitlab/data_builder/pipeline_spec.rb
new file mode 100644
index 0000000000000000000000000000000000000000..a68f5943a6a91015338a69a24789480f6d259363
--- /dev/null
+++ b/spec/lib/gitlab/data_builder/pipeline_spec.rb
@@ -0,0 +1,36 @@
+require 'spec_helper'
+
+describe Gitlab::DataBuilder::Pipeline do
+  let(:user) { create(:user) }
+  let(:project) { create(:project) }
+
+  let(:pipeline) do
+    create(:ci_pipeline,
+           project: project,
+           status: 'success',
+           sha: project.commit.sha,
+           ref: project.default_branch)
+  end
+
+  let!(:build) { create(:ci_build, pipeline: pipeline) }
+
+  describe '.build' do
+    let(:data) { described_class.build(pipeline) }
+    let(:attributes) { data[:object_attributes] }
+    let(:build_data) { data[:builds].first }
+    let(:project_data) { data[:project] }
+
+    it { expect(attributes).to be_a(Hash) }
+    it { expect(attributes[:ref]).to eq(pipeline.ref) }
+    it { expect(attributes[:sha]).to eq(pipeline.sha) }
+    it { expect(attributes[:tag]).to eq(pipeline.tag) }
+    it { expect(attributes[:id]).to eq(pipeline.id) }
+    it { expect(attributes[:status]).to eq(pipeline.status) }
+
+    it { expect(build_data).to be_a(Hash) }
+    it { expect(build_data[:id]).to eq(build.id) }
+    it { expect(build_data[:status]).to eq(build.status) }
+
+    it { expect(project_data).to eq(project.hook_attrs(backward: false)) }
+  end
+end
diff --git a/spec/lib/gitlab/push_data_builder_spec.rb b/spec/lib/gitlab/data_builder/push_spec.rb
similarity index 97%
rename from spec/lib/gitlab/push_data_builder_spec.rb
rename to spec/lib/gitlab/data_builder/push_spec.rb
index 6bd7393aaa7bd85ae7539583229a493fc176a938..b73434e8dd787626cf95c50cf6b5080674de4c1a 100644
--- a/spec/lib/gitlab/push_data_builder_spec.rb
+++ b/spec/lib/gitlab/data_builder/push_spec.rb
@@ -1,6 +1,6 @@
 require 'spec_helper'
 
-describe Gitlab::PushDataBuilder, lib: true do
+describe Gitlab::DataBuilder::Push, lib: true do
   let(:project) { create(:project) }
   let(:user) { create(:user) }
 
diff --git a/spec/lib/gitlab/diff/file_spec.rb b/spec/lib/gitlab/diff/file_spec.rb
index e883a6eb9c2e120f0ecbefc7a0bedd75c6090580..0650cb291e5f2ce1880a55747e1250aa5b7a4887 100644
--- a/spec/lib/gitlab/diff/file_spec.rb
+++ b/spec/lib/gitlab/diff/file_spec.rb
@@ -5,7 +5,7 @@ describe Gitlab::Diff::File, lib: true do
 
   let(:project) { create(:project) }
   let(:commit) { project.commit(sample_commit.id) }
-  let(:diff) { commit.diffs.first }
+  let(:diff) { commit.raw_diffs.first }
   let(:diff_file) { Gitlab::Diff::File.new(diff, diff_refs: commit.diff_refs, repository: project.repository) }
 
   describe '#diff_lines' do
diff --git a/spec/lib/gitlab/diff/highlight_spec.rb b/spec/lib/gitlab/diff/highlight_spec.rb
index 88e4115c4532fb04c0cf3447a81ffb3ceae83011..1c2ddeed6922afc45e58649be1f3512f5c052738 100644
--- a/spec/lib/gitlab/diff/highlight_spec.rb
+++ b/spec/lib/gitlab/diff/highlight_spec.rb
@@ -5,7 +5,7 @@ describe Gitlab::Diff::Highlight, lib: true do
 
   let(:project) { create(:project) }
   let(:commit) { project.commit(sample_commit.id) }
-  let(:diff) { commit.diffs.first }
+  let(:diff) { commit.raw_diffs.first }
   let(:diff_file) { Gitlab::Diff::File.new(diff, diff_refs: commit.diff_refs, repository: project.repository) }
 
   describe '#highlight' do
diff --git a/spec/lib/gitlab/diff/line_mapper_spec.rb b/spec/lib/gitlab/diff/line_mapper_spec.rb
index 4e50e03bb7e6552c59573d3dc1bd54cc7bf55eee..4b943fa382d02e1b73073d57fae5ffb075e72080 100644
--- a/spec/lib/gitlab/diff/line_mapper_spec.rb
+++ b/spec/lib/gitlab/diff/line_mapper_spec.rb
@@ -6,7 +6,7 @@ describe Gitlab::Diff::LineMapper, lib: true do
   let(:project) { create(:project) }
   let(:repository) { project.repository }
   let(:commit) { project.commit(sample_commit.id) }
-  let(:diffs) { commit.diffs }
+  let(:diffs) { commit.raw_diffs }
   let(:diff) { diffs.first }
   let(:diff_file) { Gitlab::Diff::File.new(diff, diff_refs: commit.diff_refs, repository: repository) }
   subject { described_class.new(diff_file) }
diff --git a/spec/lib/gitlab/diff/parallel_diff_spec.rb b/spec/lib/gitlab/diff/parallel_diff_spec.rb
index 2aa5ae44f54ab2a32f5fbe8bd5ca5af65d667000..af18d3c25a674808771e654ab647432646bc74ba 100644
--- a/spec/lib/gitlab/diff/parallel_diff_spec.rb
+++ b/spec/lib/gitlab/diff/parallel_diff_spec.rb
@@ -6,7 +6,7 @@ describe Gitlab::Diff::ParallelDiff, lib: true do
   let(:project) { create(:project) }
   let(:repository) { project.repository }
   let(:commit) { project.commit(sample_commit.id) }
-  let(:diffs) { commit.diffs }
+  let(:diffs) { commit.raw_diffs }
   let(:diff) { diffs.first }
   let(:diff_file) { Gitlab::Diff::File.new(diff, diff_refs: commit.diff_refs, repository: repository) }
   subject { described_class.new(diff_file) }
diff --git a/spec/lib/gitlab/diff/parser_spec.rb b/spec/lib/gitlab/diff/parser_spec.rb
index c33596276522c9532f26a1f9c8f4fdb0bc02183f..b983d73f8be1df6b91cffd40ec9132dcdc8915d8 100644
--- a/spec/lib/gitlab/diff/parser_spec.rb
+++ b/spec/lib/gitlab/diff/parser_spec.rb
@@ -5,7 +5,7 @@ describe Gitlab::Diff::Parser, lib: true do
 
   let(:project) { create(:project) }
   let(:commit) { project.commit(sample_commit.id) }
-  let(:diff) { commit.diffs.first }
+  let(:diff) { commit.raw_diffs.first }
   let(:parser) { Gitlab::Diff::Parser.new }
 
   describe '#parse' do
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..a5cc7b02936f339b2fc5d6bd9789b9d8989af8dc 100644
--- a/spec/lib/gitlab/email/handler/create_issue_handler_spec.rb
+++ b/spec/lib/gitlab/email/handler/create_issue_handler_spec.rb
@@ -1,7 +1,7 @@
 require 'spec_helper'
 require_relative '../email_shared_blocks'
 
-describe Gitlab::Email::Handler::CreateIssueHandler, lib: true do
+xdescribe Gitlab::Email::Handler::CreateIssueHandler, lib: true do
   include_context :email_shared_context
   it_behaves_like :email_shared_examples
 
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..4909fed6b774a82eea3518aad56a65a549576cd6 100644
--- a/spec/lib/gitlab/email/handler/create_note_handler_spec.rb
+++ b/spec/lib/gitlab/email/handler/create_note_handler_spec.rb
@@ -60,6 +60,67 @@ 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(noteable.due_date).to eq(Date.tomorrow)
+          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(noteable.due_date).to be_nil
+        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(noteable.due_date).to eq(Date.tomorrow)
+        expect(TodoService.new.todo_exist?(noteable, user)).to be_truthy
+      end
+    end
   end
 
   context "when the reply is blank" do
diff --git a/spec/lib/gitlab/email/message/repository_push_spec.rb b/spec/lib/gitlab/email/message/repository_push_spec.rb
index c19f33e22241d1f76de324f1df57a2785cbefd00..5b966bddb6a6733a19b6db6800479a1713001c55 100644
--- a/spec/lib/gitlab/email/message/repository_push_spec.rb
+++ b/spec/lib/gitlab/email/message/repository_push_spec.rb
@@ -16,9 +16,12 @@ describe Gitlab::Email::Message::RepositoryPush do
       { author_id: author.id, ref: 'master', action: :push, compare: compare,
         send_from_committer_email: true }
     end
-    let(:compare) do
+    let(:raw_compare) do
       Gitlab::Git::Compare.new(project.repository.raw_repository,
-                               sample_image_commit.id, sample_commit.id)
+        sample_image_commit.id, sample_commit.id)
+    end
+    let(:compare) do
+      Compare.decorate(raw_compare, project)
     end
 
     describe '#project' do
@@ -62,17 +65,17 @@ describe Gitlab::Email::Message::RepositoryPush do
 
     describe '#diffs_count' do
       subject { message.diffs_count }
-      it { is_expected.to eq compare.diffs.count }
+      it { is_expected.to eq raw_compare.diffs.size }
     end
 
     describe '#compare' do
       subject { message.compare }
-      it { is_expected.to be_an_instance_of Gitlab::Git::Compare }
+      it { is_expected.to be_an_instance_of Compare }
     end
 
     describe '#compare_timeout' do
       subject { message.compare_timeout }
-      it { is_expected.to eq compare.diffs.overflow? }
+      it { is_expected.to eq raw_compare.diffs.overflow? }
     end
 
     describe '#reverse_compare?' do
diff --git a/spec/lib/gitlab/git/hook_spec.rb b/spec/lib/gitlab/git/hook_spec.rb
index a15aa173fbd314baf0435024a1ef9e6cfabcc12e..d1f947b68500940e51248da3b45039c40720c329 100644
--- a/spec/lib/gitlab/git/hook_spec.rb
+++ b/spec/lib/gitlab/git/hook_spec.rb
@@ -25,7 +25,6 @@ describe Gitlab::Git::Hook, lib: true do
     end
 
     ['pre-receive', 'post-receive', 'update'].each do |hook_name|
-
       context "when triggering a #{hook_name} hook" do
         context "when the hook is successful" do
           it "returns success with no errors" do
diff --git a/spec/lib/gitlab/git_access_spec.rb b/spec/lib/gitlab/git_access_spec.rb
index 8447305a316df18f55d809a5b2a627236350684f..f12c9a370f741c827e03e726ea17eb7d2947e8d7 100644
--- a/spec/lib/gitlab/git_access_spec.rb
+++ b/spec/lib/gitlab/git_access_spec.rb
@@ -19,11 +19,11 @@ describe Gitlab::GitAccess, lib: true do
       end
 
       it 'blocks ssh git push' do
-        expect(@acc.check('git-receive-pack').allowed?).to be_falsey
+        expect(@acc.check('git-receive-pack', '_any').allowed?).to be_falsey
       end
 
       it 'blocks ssh git pull' do
-        expect(@acc.check('git-upload-pack').allowed?).to be_falsey
+        expect(@acc.check('git-upload-pack', '_any').allowed?).to be_falsey
       end
     end
 
@@ -34,17 +34,17 @@ describe Gitlab::GitAccess, lib: true do
       end
 
       it 'blocks http push' do
-        expect(@acc.check('git-receive-pack').allowed?).to be_falsey
+        expect(@acc.check('git-receive-pack', '_any').allowed?).to be_falsey
       end
 
       it 'blocks http git pull' do
-        expect(@acc.check('git-upload-pack').allowed?).to be_falsey
+        expect(@acc.check('git-upload-pack', '_any').allowed?).to be_falsey
       end
     end
   end
 
   describe 'download_access_check' do
-    subject { access.check('git-upload-pack') }
+    subject { access.check('git-upload-pack', '_any') }
 
     describe 'master permissions' do
       before { project.team << [user, :master] }
@@ -288,7 +288,7 @@ describe Gitlab::GitAccess, lib: true do
     let(:actor) { key }
 
     context 'push code' do
-      subject { access.check('git-receive-pack') }
+      subject { access.check('git-receive-pack', '_any') }
 
       context 'when project is authorized' do
         before { key.projects << project }
diff --git a/spec/lib/gitlab/github_import/branch_formatter_spec.rb b/spec/lib/gitlab/github_import/branch_formatter_spec.rb
index fc9d5204148f31259ec76c386d42b9d758b917f3..e5300dbba1ee4951100965e66334d277dea00a35 100644
--- a/spec/lib/gitlab/github_import/branch_formatter_spec.rb
+++ b/spec/lib/gitlab/github_import/branch_formatter_spec.rb
@@ -32,20 +32,6 @@ describe Gitlab::GithubImport::BranchFormatter, lib: true do
     end
   end
 
-  describe '#name' do
-    it 'returns raw ref when branch exists' do
-      branch = described_class.new(project, double(raw))
-
-      expect(branch.name).to eq 'feature'
-    end
-
-    it 'returns formatted ref when branch does not exist' do
-      branch = described_class.new(project, double(raw.merge(ref: 'removed-branch', sha: '2e5d3239642f9161dcbbc4b70a211a68e5e45e2b')))
-
-      expect(branch.name).to eq 'removed-branch-2e5d3239'
-    end
-  end
-
   describe '#repo' do
     it 'returns raw repo' do
       branch = described_class.new(project, double(raw))
diff --git a/spec/lib/gitlab/github_import/hook_formatter_spec.rb b/spec/lib/gitlab/github_import/hook_formatter_spec.rb
deleted file mode 100644
index 110ba428258eceb74d2c2ebbdc6cc9747a2444ef..0000000000000000000000000000000000000000
--- a/spec/lib/gitlab/github_import/hook_formatter_spec.rb
+++ /dev/null
@@ -1,65 +0,0 @@
-require 'spec_helper'
-
-describe Gitlab::GithubImport::HookFormatter, lib: true do
-  describe '#id' do
-    it 'returns raw id' do
-      raw = double(id: 100000)
-      formatter = described_class.new(raw)
-      expect(formatter.id).to eq 100000
-    end
-  end
-
-  describe '#name' do
-    it 'returns raw id' do
-      raw = double(name: 'web')
-      formatter = described_class.new(raw)
-      expect(formatter.name).to eq 'web'
-    end
-  end
-
-  describe '#config' do
-    it 'returns raw config.attrs' do
-      raw = double(config: double(attrs: { url: 'http://something.com/webhook' }))
-      formatter = described_class.new(raw)
-      expect(formatter.config).to eq({ url: 'http://something.com/webhook' })
-    end
-  end
-
-  describe '#valid?' do
-    it 'returns true when events contains the wildcard event' do
-      raw = double(events: ['*', 'commit_comment'], active: true)
-      formatter = described_class.new(raw)
-      expect(formatter.valid?).to eq true
-    end
-
-    it 'returns true when events contains the create event' do
-      raw = double(events: ['create', 'commit_comment'], active: true)
-      formatter = described_class.new(raw)
-      expect(formatter.valid?).to eq true
-    end
-
-    it 'returns true when events contains delete event' do
-      raw = double(events: ['delete', 'commit_comment'], active: true)
-      formatter = described_class.new(raw)
-      expect(formatter.valid?).to eq true
-    end
-
-    it 'returns true when events contains pull_request event' do
-      raw = double(events: ['pull_request', 'commit_comment'], active: true)
-      formatter = described_class.new(raw)
-      expect(formatter.valid?).to eq true
-    end
-
-    it 'returns false when events does not contains branch related events' do
-      raw = double(events: ['member', 'commit_comment'], active: true)
-      formatter = described_class.new(raw)
-      expect(formatter.valid?).to eq false
-    end
-
-    it 'returns false when hook is not active' do
-      raw = double(events: ['pull_request', 'commit_comment'], active: false)
-      formatter = described_class.new(raw)
-      expect(formatter.valid?).to eq false
-    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 79931ecd134e9eb40da229119a01a3aaa4738daf..aa28e360993434ba264a3362a65a4d5f5ac07e75 100644
--- a/spec/lib/gitlab/github_import/pull_request_formatter_spec.rb
+++ b/spec/lib/gitlab/github_import/pull_request_formatter_spec.rb
@@ -9,6 +9,7 @@ describe Gitlab::GithubImport::PullRequestFormatter, lib: true do
   let(:source_branch) { double(ref: 'feature', repo: source_repo, sha: source_sha) }
   let(:target_repo) { repository }
   let(:target_branch) { double(ref: 'master', repo: target_repo, sha: target_sha) }
+  let(:removed_branch) { double(ref: 'removed-branch', repo: source_repo, sha: '2e5d3239642f9161dcbbc4b70a211a68e5e45e2b') }
   let(: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') }
@@ -165,6 +166,42 @@ describe Gitlab::GithubImport::PullRequestFormatter, lib: true do
     end
   end
 
+  describe '#source_branch_name' do
+    context 'when source branch exists' do
+      let(:raw_data) { double(base_data) }
+
+      it 'returns branch ref' do
+        expect(pull_request.source_branch_name).to eq 'feature'
+      end
+    end
+
+    context 'when source branch does not exist' do
+      let(:raw_data) { double(base_data.merge(head: removed_branch)) }
+
+      it 'prefixes branch name with pull request number' do
+        expect(pull_request.source_branch_name).to eq 'pull/1347/removed-branch'
+      end
+    end
+  end
+
+  describe '#target_branch_name' do
+    context 'when source branch exists' do
+      let(:raw_data) { double(base_data) }
+
+      it 'returns branch ref' do
+        expect(pull_request.target_branch_name).to eq 'master'
+      end
+    end
+
+    context 'when target branch does not exist' do
+      let(:raw_data) { double(base_data.merge(base: removed_branch)) }
+
+      it 'prefixes branch name with pull request number' do
+        expect(pull_request.target_branch_name).to eq 'pull/1347/removed-branch'
+      end
+    end
+  end
+
   describe '#valid?' do
     context 'when source, and target repos are not a fork' do
       let(:raw_data) { double(base_data) }
@@ -178,8 +215,8 @@ describe Gitlab::GithubImport::PullRequestFormatter, lib: true do
       let(:source_repo) { double(id: 2) }
       let(:raw_data) { double(base_data) }
 
-      it 'returns false' do
-        expect(pull_request.valid?).to eq false
+      it 'returns true' do
+        expect(pull_request.valid?).to eq true
       end
     end
 
@@ -187,8 +224,8 @@ describe Gitlab::GithubImport::PullRequestFormatter, lib: true do
       let(:target_repo) { double(id: 2) }
       let(:raw_data) { double(base_data) }
 
-      it 'returns false' do
-        expect(pull_request.valid?).to eq false
+      it 'returns true' do
+        expect(pull_request.valid?).to eq true
       end
     end
   end
diff --git a/spec/lib/gitlab/import_export/members_mapper_spec.rb b/spec/lib/gitlab/import_export/members_mapper_spec.rb
index 6d5aa0d04a22ea6750a0d5305fbaf0b197fa7cb3..770e8b0c2f46bd95758566d8a5df34b3dadc708b 100644
--- a/spec/lib/gitlab/import_export/members_mapper_spec.rb
+++ b/spec/lib/gitlab/import_export/members_mapper_spec.rb
@@ -26,6 +26,20 @@ describe Gitlab::ImportExport::MembersMapper, services: true do
              "email" => user2.email,
              "username" => user2.username
            }
+       },
+       {
+         "id" => 3,
+         "access_level" => 40,
+         "source_id" => 14,
+         "source_type" => "Project",
+         "user_id" => nil,
+         "notification_level" => 3,
+         "created_at" => "2016-03-11T10:21:44.822Z",
+         "updated_at" => "2016-03-11T10:21:44.822Z",
+         "created_by_id" => 1,
+         "invite_email" => 'invite@test.com',
+         "invite_token" => 'token',
+         "invite_accepted_at" => nil
        }]
     end
 
@@ -47,5 +61,11 @@ describe Gitlab::ImportExport::MembersMapper, services: true do
 
       expect(members_mapper.missing_author_ids.first).to eq(-1)
     end
+
+    it 'has invited members with no user' do
+      members_mapper.map
+
+      expect(ProjectMember.find_by_invite_email('invite@test.com')).not_to be_nil
+    end
   end
 end
diff --git a/spec/lib/gitlab/import_export/project.json b/spec/lib/gitlab/import_export/project.json
index b5550ca196322a88e4271e13e24941e68fdb7b51..cbbf98dca94f36490dc0e91d5b2c7cb32f17e859 100644
--- a/spec/lib/gitlab/import_export/project.json
+++ b/spec/lib/gitlab/import_export/project.json
@@ -2393,7 +2393,7 @@
       "source_project_id": 5,
       "author_id": 1,
       "assignee_id": null,
-      "title": "Cannot be automatically merged",
+      "title": "MR1",
       "created_at": "2016-06-14T15:02:36.568Z",
       "updated_at": "2016-06-14T15:02:56.815Z",
       "state": "opened",
@@ -2827,10 +2827,10 @@
       "id": 26,
       "target_branch": "master",
       "source_branch": "feature",
-      "source_project_id": 5,
+      "source_project_id": 4,
       "author_id": 1,
       "assignee_id": null,
-      "title": "Can be automatically merged",
+      "title": "MR2",
       "created_at": "2016-06-14T15:02:36.418Z",
       "updated_at": "2016-06-14T15:02:57.013Z",
       "state": "opened",
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 32c0d6462f14917758dbb71bfb356de5ccac0756..4d857945fdef667650806af7b437f344637b02c1 100644
--- a/spec/lib/gitlab/import_export/project_tree_restorer_spec.rb
+++ b/spec/lib/gitlab/import_export/project_tree_restorer_spec.rb
@@ -2,7 +2,6 @@ require 'spec_helper'
 
 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') }
@@ -72,6 +71,28 @@ describe Gitlab::ImportExport::ProjectTreeRestorer, services: true do
 
         expect(Milestone.find_by_description('test milestone').issues).not_to be_empty
       end
+
+      context 'Merge requests' do
+        before do
+          restored_project_json
+        end
+
+        it 'always has the new project as a target' do
+          expect(MergeRequest.find_by_title('MR1').target_project).to eq(project)
+        end
+
+        it 'has the same source project as originally if source/target are the same' do
+          expect(MergeRequest.find_by_title('MR1').source_project).to eq(project)
+        end
+
+        it 'has the new project as target if source/target differ' do
+          expect(MergeRequest.find_by_title('MR2').target_project).to eq(project)
+        end
+
+        it 'has no source if source/target differ' do
+          expect(MergeRequest.find_by_title('MR2').source_project_id).to eq(-1)
+        end
+      end
     end
   end
 end
diff --git a/spec/lib/gitlab/import_export/reader_spec.rb b/spec/lib/gitlab/import_export/reader_spec.rb
index b76e14deca1fd4e8bd2cb44e6b1d4dca22c66d30..b6dec41d218f095fc04cb4daf779c45cc7dd89b1 100644
--- a/spec/lib/gitlab/import_export/reader_spec.rb
+++ b/spec/lib/gitlab/import_export/reader_spec.rb
@@ -12,7 +12,8 @@ describe Gitlab::ImportExport::Reader, lib: true  do
                   except: [:iid],
                   include: [:merge_request_diff, :merge_request_test]
                 } },
-                { commit_statuses: { include: :commit } }]
+                { commit_statuses: { include: :commit } },
+                { project_members: { include: { user: { only: [:email] } } } }]
     }
   end
 
diff --git a/spec/lib/gitlab/import_export/version_checker_spec.rb b/spec/lib/gitlab/import_export/version_checker_spec.rb
new file mode 100644
index 0000000000000000000000000000000000000000..90c6d1c67f6ec920cc4e2feafeee930027aa8f2d
--- /dev/null
+++ b/spec/lib/gitlab/import_export/version_checker_spec.rb
@@ -0,0 +1,30 @@
+require 'spec_helper'
+
+describe Gitlab::ImportExport::VersionChecker, services: true do
+  describe 'bundle a project Git repo' do
+    let(:shared) { Gitlab::ImportExport::Shared.new(relative_path: '') }
+    let(:version) { Gitlab::ImportExport.version }
+
+    before do
+      allow(File).to receive(:open).and_return(version)
+    end
+
+    it 'returns true if Import/Export have the same version' do
+      expect(described_class.check!(shared: shared)).to be true
+    end
+
+    context 'newer version' do
+      let(:version) { '900.0'}
+
+      it 'returns false if export version is newer' do
+        expect(described_class.check!(shared: shared)).to be false
+      end
+
+      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}")
+      end
+    end
+  end
+end
diff --git a/spec/lib/gitlab/ldap/access_spec.rb b/spec/lib/gitlab/ldap/access_spec.rb
index acd5394382c72a77e5488498a2f5b9dd685a2952..534bcbf39febdbaf50426fdb90240a5058ee9b6b 100644
--- a/spec/lib/gitlab/ldap/access_spec.rb
+++ b/spec/lib/gitlab/ldap/access_spec.rb
@@ -64,7 +64,7 @@ describe Gitlab::LDAP::Access, lib: true do
             user.ldap_block
           end
 
-          it 'should unblock user in GitLab' do
+          it 'unblocks user in GitLab' do
             access.allowed?
             expect(user).not_to be_blocked
           end
diff --git a/spec/lib/gitlab/ldap/user_spec.rb b/spec/lib/gitlab/ldap/user_spec.rb
index 949f6e2b19a3ee76af0ef57b29455a5f2353f2c2..89790c9e1af45db0eab05055be93375fd33c11a8 100644
--- a/spec/lib/gitlab/ldap/user_spec.rb
+++ b/spec/lib/gitlab/ldap/user_spec.rb
@@ -36,7 +36,7 @@ describe Gitlab::LDAP::User, lib: true do
       expect(ldap_user.changed?).to be_truthy
     end
 
-    it "dont marks existing ldap user as changed" do
+    it "does not mark existing ldap user as changed" do
       create(:omniauth_user, email: 'john@example.com', extern_uid: 'my-uid', provider: 'ldapmain', ldap_email: true)
       expect(ldap_user.changed?).to be_falsey
     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..a30cb2a5e38220c41e8683062781542507cd15de 100644
--- a/spec/lib/gitlab/metrics/rack_middleware_spec.rb
+++ b/spec/lib/gitlab/metrics/rack_middleware_spec.rb
@@ -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
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/o_auth/user_spec.rb b/spec/lib/gitlab/o_auth/user_spec.rb
index 1fca8a13037875d601f183b773f0122bbb0a609a..78c669e8fa5a57bfd17a137e689779ff8ae93548 100644
--- a/spec/lib/gitlab/o_auth/user_spec.rb
+++ b/spec/lib/gitlab/o_auth/user_spec.rb
@@ -42,7 +42,7 @@ describe Gitlab::OAuth::User, lib: true do
     describe 'signup' do
       shared_examples 'to verify compliance with allow_single_sign_on' do
         context 'provider is marked as external' do
-          it 'should mark user as external' do
+          it 'marks user as external' do
             stub_omniauth_config(allow_single_sign_on: ['twitter'], external_providers: ['twitter'])
             oauth_user.save
             expect(gl_user).to be_valid
@@ -51,7 +51,7 @@ describe Gitlab::OAuth::User, lib: true do
         end
 
         context 'provider was external, now has been removed' do
-          it 'should not mark external user as internal' do
+          it 'does not mark external user as internal' do
             create(:omniauth_user, extern_uid: 'my-uid', provider: 'twitter', external: true)
             stub_omniauth_config(allow_single_sign_on: ['twitter'], external_providers: ['facebook'])
             oauth_user.save
@@ -62,7 +62,7 @@ describe Gitlab::OAuth::User, lib: true do
 
         context 'provider is not external' do
           context 'when adding a new OAuth identity' do
-            it 'should not promote an external user to internal' do
+            it 'does not promote an external user to internal' do
               user = create(:user, email: 'john@mail.com', external: true)
               user.identities.create(provider: provider, extern_uid: uid)
 
diff --git a/spec/lib/gitlab/project_search_results_spec.rb b/spec/lib/gitlab/project_search_results_spec.rb
index 270b89972d73d1a452e37152085810c84ea151c6..29abb4d4d077ac69ca82dbde5f6e07977eba32db 100644
--- a/spec/lib/gitlab/project_search_results_spec.rb
+++ b/spec/lib/gitlab/project_search_results_spec.rb
@@ -33,7 +33,7 @@ describe Gitlab::ProjectSearchResults, lib: true do
     let!(:security_issue_1) { create(:issue, :confidential, project: project, title: 'Security issue 1', author: author) }
     let!(:security_issue_2) { create(:issue, :confidential, title: 'Security issue 2', project: project, assignee: assignee) }
 
-    it 'should not list project confidential issues for non project members' do
+    it 'does not list project confidential issues for non project members' do
       results = described_class.new(non_member, project, query)
       issues = results.objects('issues')
 
@@ -43,7 +43,7 @@ describe Gitlab::ProjectSearchResults, lib: true do
       expect(results.issues_count).to eq 1
     end
 
-    it 'should not list project confidential issues for project members with guest role' do
+    it 'does not list project confidential issues for project members with guest role' do
       project.team << [member, :guest]
 
       results = described_class.new(member, project, query)
@@ -55,7 +55,7 @@ describe Gitlab::ProjectSearchResults, lib: true do
       expect(results.issues_count).to eq 1
     end
 
-    it 'should list project confidential issues for author' do
+    it 'lists project confidential issues for author' do
       results = described_class.new(author, project, query)
       issues = results.objects('issues')
 
@@ -65,7 +65,7 @@ describe Gitlab::ProjectSearchResults, lib: true do
       expect(results.issues_count).to eq 2
     end
 
-    it 'should list project confidential issues for assignee' do
+    it 'lists project confidential issues for assignee' do
       results = described_class.new(assignee, project.id, query)
       issues = results.objects('issues')
 
@@ -75,7 +75,7 @@ describe Gitlab::ProjectSearchResults, lib: true do
       expect(results.issues_count).to eq 2
     end
 
-    it 'should list project confidential issues for project members' do
+    it 'lists project confidential issues for project members' do
       project.team << [member, :developer]
 
       results = described_class.new(member, project, query)
@@ -87,7 +87,7 @@ describe Gitlab::ProjectSearchResults, lib: true do
       expect(results.issues_count).to eq 3
     end
 
-    it 'should list all project issues for admin' do
+    it 'lists all project issues for admin' do
       results = described_class.new(admin, project, query)
       issues = results.objects('issues')
 
diff --git a/spec/lib/gitlab/redis_spec.rb b/spec/lib/gitlab/redis_spec.rb
new file mode 100644
index 0000000000000000000000000000000000000000..e54f5ffb3124930a2d2aa9e4e4715ece5676dffc
--- /dev/null
+++ b/spec/lib/gitlab/redis_spec.rb
@@ -0,0 +1,79 @@
+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! }
+
+  describe '.params' do
+    subject { described_class.params }
+
+    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 }
+
+          is_expected.to include(path: '/path/to/old/redis.sock')
+          is_expected.not_to have_key(:url)
+        end
+      end
+
+      context 'with new format' do
+        it 'returns path key instead' do
+          expect_any_instance_of(described_class).to receive(:config_file) { config_new }
+
+          is_expected.to include(path: '/path/to/redis.sock')
+          is_expected.not_to have_key(:url)
+        end
+      end
+    end
+
+    context 'when url is host based' do
+      let(:config_old) { Rails.root.join('spec/fixtures/config/redis_old_format_host.yml') }
+      let(:config_new) { Rails.root.join('spec/fixtures/config/redis_new_format_host.yml') }
+
+      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 }
+
+          is_expected.to include(host: 'localhost', password: 'mypassword', port: 6379, db: 99)
+          is_expected.not_to have_key(:url)
+        end
+      end
+
+      context 'with new format' do
+        it 'returns hash with host, port, db, and password' do
+          expect_any_instance_of(described_class).to receive(:config_file) { config_new }
+
+          is_expected.to include(host: 'localhost', password: 'mynewpassword', port: 6379, db: 99)
+          is_expected.not_to have_key(:url)
+        end
+      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 }
+
+      expect(subject.send(:raw_config_hash)).to eq(url: Gitlab::Redis::DEFAULT_REDIS_URL)
+    end
+
+    it 'returns old-style single url config in a hash' do
+      expect(subject).to receive(:fetch_config) { 'redis://myredis:6379' }
+      expect(subject.send(:raw_config_hash)).to eq(url: 'redis://myredis:6379')
+    end
+  end
+
+  describe '#fetch_config' do
+    it 'returns false when no config file is present' do
+      allow(File).to receive(:exist?).with(redis_config) { false }
+
+      expect(subject.send(:fetch_config)).to be_falsey
+    end
+  end
+end
diff --git a/spec/lib/gitlab/saml/user_spec.rb b/spec/lib/gitlab/saml/user_spec.rb
index 56bf08e704170ed5d0c3811efa9f41e4018acdde..02c139f1a0d131e9f4eeae8cd0850b0dfe1dd732 100644
--- a/spec/lib/gitlab/saml/user_spec.rb
+++ b/spec/lib/gitlab/saml/user_spec.rb
@@ -67,7 +67,7 @@ describe Gitlab::Saml::User, lib: true do
         end
 
         context 'user was external, now should not be' do
-          it 'should make user internal' do
+          it 'makes user internal' do
             existing_user.update_attribute('external', true)
             saml_user.save
             expect(gl_user).to be_valid
@@ -94,14 +94,14 @@ describe Gitlab::Saml::User, lib: true do
 
         context 'with allow_single_sign_on default (["saml"])' do
           before { stub_omniauth_config(allow_single_sign_on: ['saml']) }
-          it 'should not throw an error' do
+          it 'does not throw an error' do
             expect{ saml_user.save }.not_to raise_error
           end
         end
 
         context 'with allow_single_sign_on disabled' do
           before { stub_omniauth_config(allow_single_sign_on: false) }
-          it 'should throw an error' do
+          it 'throws an error' do
             expect{ saml_user.save }.to raise_error StandardError
           end
         end
@@ -223,7 +223,7 @@ describe Gitlab::Saml::User, lib: true do
         context 'dont block on create' do
           before { stub_omniauth_config(block_auto_created_users: false) }
 
-          it 'should not block the user' do
+          it 'does not block the user' do
             saml_user.save
             expect(gl_user).to be_valid
             expect(gl_user).not_to be_blocked
@@ -233,7 +233,7 @@ describe Gitlab::Saml::User, lib: true do
         context 'block on create' do
           before { stub_omniauth_config(block_auto_created_users: true) }
 
-          it 'should block user' do
+          it 'blocks user' do
             saml_user.save
             expect(gl_user).to be_valid
             expect(gl_user).to be_blocked
diff --git a/spec/lib/gitlab/search_results_spec.rb b/spec/lib/gitlab/search_results_spec.rb
index 1bb444bf34fc2101d67e6b9e2c4ce6a5bc3fb0bc..8a656ab0ee9e1d45dfe19445d74367874e029fa1 100644
--- a/spec/lib/gitlab/search_results_spec.rb
+++ b/spec/lib/gitlab/search_results_spec.rb
@@ -73,7 +73,7 @@ describe Gitlab::SearchResults do
     let!(:security_issue_4) { create(:issue, :confidential, project: project_3, title: 'Security issue 4', assignee: assignee) }
     let!(:security_issue_5) { create(:issue, :confidential, project: project_4, title: 'Security issue 5') }
 
-    it 'should not list confidential issues for non project members' do
+    it 'does not list confidential issues for non project members' do
       results = described_class.new(non_member, limit_projects, query)
       issues = results.objects('issues')
 
@@ -86,7 +86,7 @@ describe Gitlab::SearchResults do
       expect(results.issues_count).to eq 1
     end
 
-    it 'should not list confidential issues for project members with guest role' do
+    it 'does not list confidential issues for project members with guest role' do
       project_1.team << [member, :guest]
       project_2.team << [member, :guest]
 
@@ -102,7 +102,7 @@ describe Gitlab::SearchResults do
       expect(results.issues_count).to eq 1
     end
 
-    it 'should list confidential issues for author' do
+    it 'lists confidential issues for author' do
       results = described_class.new(author, limit_projects, query)
       issues = results.objects('issues')
 
@@ -115,7 +115,7 @@ describe Gitlab::SearchResults do
       expect(results.issues_count).to eq 3
     end
 
-    it 'should list confidential issues for assignee' do
+    it 'lists confidential issues for assignee' do
       results = described_class.new(assignee, limit_projects, query)
       issues = results.objects('issues')
 
@@ -128,7 +128,7 @@ describe Gitlab::SearchResults do
       expect(results.issues_count).to eq 3
     end
 
-    it 'should list confidential issues for project members' do
+    it 'lists confidential issues for project members' do
       project_1.team << [member, :developer]
       project_2.team << [member, :developer]
 
@@ -144,7 +144,7 @@ describe Gitlab::SearchResults do
       expect(results.issues_count).to eq 4
     end
 
-    it 'should list all issues for admin' do
+    it 'lists all issues for admin' do
       results = described_class.new(admin, limit_projects, query)
       issues = results.objects('issues')
 
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/template/gitignore_spec.rb b/spec/lib/gitlab/template/gitignore_template_spec.rb
similarity index 88%
rename from spec/lib/gitlab/template/gitignore_spec.rb
rename to spec/lib/gitlab/template/gitignore_template_spec.rb
index bc0ec9325cc106368d866f62a4d49f49d5cce8bb..9750a012e22dc877a07426187c3e11d417b2077e 100644
--- a/spec/lib/gitlab/template/gitignore_spec.rb
+++ b/spec/lib/gitlab/template/gitignore_template_spec.rb
@@ -1,6 +1,6 @@
 require 'spec_helper'
 
-describe Gitlab::Template::Gitignore do
+describe Gitlab::Template::GitignoreTemplate do
   subject { described_class }
 
   describe '.all' do
@@ -24,7 +24,7 @@ describe Gitlab::Template::Gitignore do
     it 'returns the Gitignore object of a valid file' do
       ruby = subject.find('Ruby')
 
-      expect(ruby).to be_a Gitlab::Template::Gitignore
+      expect(ruby).to be_a Gitlab::Template::GitignoreTemplate
       expect(ruby.name).to eq('Ruby')
     end
   end
diff --git a/spec/lib/gitlab/template/gitlab_ci_yml_template_spec.rb b/spec/lib/gitlab/template/gitlab_ci_yml_template_spec.rb
new file mode 100644
index 0000000000000000000000000000000000000000..e3b8321eda3907f61eeea3fdb25cfc5263cb40c7
--- /dev/null
+++ b/spec/lib/gitlab/template/gitlab_ci_yml_template_spec.rb
@@ -0,0 +1,41 @@
+require 'spec_helper'
+
+describe Gitlab::Template::GitlabCiYmlTemplate do
+  subject { described_class }
+
+  describe '.all' do
+    it 'strips the gitlab-ci suffix' do
+      expect(subject.all.first.name).not_to end_with('.gitlab-ci.yml')
+    end
+
+    it 'combines the globals and rest' do
+      all = subject.all.map(&:name)
+
+      expect(all).to include('Elixir')
+      expect(all).to include('Docker')
+      expect(all).to include('Ruby')
+    end
+  end
+
+  describe '.find' do
+    it 'returns nil if the file does not exist' do
+      expect(subject.find('mepmep-yadida')).to be nil
+    end
+
+    it 'returns the GitlabCiYml object of a valid file' do
+      ruby = subject.find('Ruby')
+
+      expect(ruby).to be_a Gitlab::Template::GitlabCiYmlTemplate
+      expect(ruby.name).to eq('Ruby')
+    end
+  end
+
+  describe '#content' do
+    it 'loads the full file' do
+      gitignore = subject.new(Rails.root.join('vendor/gitlab-ci-yml/Ruby.gitlab-ci.yml'))
+
+      expect(gitignore.name).to eq 'Ruby'
+      expect(gitignore.content).to start_with('#')
+    end
+  end
+end
diff --git a/spec/lib/gitlab/template/issue_template_spec.rb b/spec/lib/gitlab/template/issue_template_spec.rb
new file mode 100644
index 0000000000000000000000000000000000000000..f770857e958817206b6687da1a5f1cd5ef66c73f
--- /dev/null
+++ b/spec/lib/gitlab/template/issue_template_spec.rb
@@ -0,0 +1,89 @@
+require 'spec_helper'
+
+describe Gitlab::Template::IssueTemplate do
+  subject { described_class }
+
+  let(:user) { create(:user) }
+  let(:project) { create(:project) }
+  let(:file_path_1) { '.gitlab/issue_templates/bug.md' }
+  let(:file_path_2) { '.gitlab/issue_templates/template_test.md' }
+  let(:file_path_3) { '.gitlab/issue_templates/feature_proposal.md' }
+
+  before do
+    project.team.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)
+  end
+
+  describe '.all' do
+    it 'strips the md suffix' do
+      expect(subject.all(project).first.name).not_to end_with('.issue_template')
+    end
+
+    it 'combines the globals and rest' do
+      all = subject.all(project).map(&:name)
+
+      expect(all).to include('bug')
+      expect(all).to include('feature_proposal')
+      expect(all).to include('template_test')
+    end
+  end
+
+  describe '.find' do
+    it 'returns nil if the file does not exist' do
+      expect { subject.find('mepmep-yadida', project) }.to raise_error(Gitlab::Template::Finders::RepoTemplateFinder::FileNotFoundError)
+    end
+
+    it 'returns the issue object of a valid file' do
+      ruby = subject.find('bug', project)
+
+      expect(ruby).to be_a Gitlab::Template::IssueTemplate
+      expect(ruby.name).to eq('bug')
+    end
+  end
+
+  describe '.by_category' do
+    it 'return array of templates' do
+      all = subject.by_category('', project).map(&:name)
+      expect(all).to include('bug')
+      expect(all).to include('feature_proposal')
+      expect(all).to include('template_test')
+    end
+
+    context 'when repo is bare or empty' do
+      let(:empty_project) { create(:empty_project) }
+      before { empty_project.team.add_user(user, Gitlab::Access::MASTER) }
+
+      it "returns empty array" do
+        templates = subject.by_category('', empty_project)
+        expect(templates).to be_empty
+      end
+    end
+  end
+
+  describe '#content' do
+    it 'loads the full file' do
+      issue_template = subject.new('.gitlab/issue_templates/bug.md', project)
+
+      expect(issue_template.name).to eq 'bug'
+      expect(issue_template.content).to eq('something valid')
+    end
+
+    it 'raises error when file is not found' do
+      issue_template = subject.new('.gitlab/issue_templates/bugnot.md', project)
+      expect { issue_template.content }.to raise_error(Gitlab::Template::Finders::RepoTemplateFinder::FileNotFoundError)
+    end
+
+    context "when repo is empty" do
+      let(:empty_project) { create(:empty_project) }
+
+      before { empty_project.team.add_user(user, Gitlab::Access::MASTER) }
+
+      it "raises file not found" do
+        issue_template = subject.new('.gitlab/issue_templates/not_existent.md', empty_project)
+        expect { issue_template.content }.to raise_error(Gitlab::Template::Finders::RepoTemplateFinder::FileNotFoundError)
+      end
+    end
+  end
+end
diff --git a/spec/lib/gitlab/template/merge_request_template_spec.rb b/spec/lib/gitlab/template/merge_request_template_spec.rb
new file mode 100644
index 0000000000000000000000000000000000000000..bb0f68043fa0ef7ae0f5b8c6b4c419dc87b6684c
--- /dev/null
+++ b/spec/lib/gitlab/template/merge_request_template_spec.rb
@@ -0,0 +1,89 @@
+require 'spec_helper'
+
+describe Gitlab::Template::MergeRequestTemplate do
+  subject { described_class }
+
+  let(:user) { create(:user) }
+  let(:project) { create(:project) }
+  let(:file_path_1) { '.gitlab/merge_request_templates/bug.md' }
+  let(:file_path_2) { '.gitlab/merge_request_templates/template_test.md' }
+  let(:file_path_3) { '.gitlab/merge_request_templates/feature_proposal.md' }
+
+  before do
+    project.team.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)
+  end
+
+  describe '.all' do
+    it 'strips the md suffix' do
+      expect(subject.all(project).first.name).not_to end_with('.issue_template')
+    end
+
+    it 'combines the globals and rest' do
+      all = subject.all(project).map(&:name)
+
+      expect(all).to include('bug')
+      expect(all).to include('feature_proposal')
+      expect(all).to include('template_test')
+    end
+  end
+
+  describe '.find' do
+    it 'returns nil if the file does not exist' do
+      expect { subject.find('mepmep-yadida', project) }.to raise_error(Gitlab::Template::Finders::RepoTemplateFinder::FileNotFoundError)
+    end
+
+    it 'returns the merge request object of a valid file' do
+      ruby = subject.find('bug', project)
+
+      expect(ruby).to be_a Gitlab::Template::MergeRequestTemplate
+      expect(ruby.name).to eq('bug')
+    end
+  end
+
+  describe '.by_category' do
+    it 'return array of templates' do
+      all = subject.by_category('', project).map(&:name)
+      expect(all).to include('bug')
+      expect(all).to include('feature_proposal')
+      expect(all).to include('template_test')
+    end
+
+    context 'when repo is bare or empty' do
+      let(:empty_project) { create(:empty_project) }
+      before { empty_project.team.add_user(user, Gitlab::Access::MASTER) }
+
+      it "returns empty array" do
+        templates = subject.by_category('', empty_project)
+        expect(templates).to be_empty
+      end
+    end
+  end
+
+  describe '#content' do
+    it 'loads the full file' do
+      issue_template = subject.new('.gitlab/merge_request_templates/bug.md', project)
+
+      expect(issue_template.name).to eq 'bug'
+      expect(issue_template.content).to eq('something valid')
+    end
+
+    it 'raises error when file is not found' do
+      issue_template = subject.new('.gitlab/merge_request_templates/bugnot.md', project)
+      expect { issue_template.content }.to raise_error(Gitlab::Template::Finders::RepoTemplateFinder::FileNotFoundError)
+    end
+
+    context "when repo is empty" do
+      let(:empty_project) { create(:empty_project) }
+
+      before { empty_project.team.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)
+        expect { issue_template.content }.to raise_error(Gitlab::Template::Finders::RepoTemplateFinder::FileNotFoundError)
+      end
+    end
+  end
+end
diff --git a/spec/lib/gitlab/upgrader_spec.rb b/spec/lib/gitlab/upgrader_spec.rb
index e958e087a80ff0a8a087c7f64877c285c7498421..edadab043d7bdf6189459ccc86f60f7027300669 100644
--- a/spec/lib/gitlab/upgrader_spec.rb
+++ b/spec/lib/gitlab/upgrader_spec.rb
@@ -9,19 +9,19 @@ describe Gitlab::Upgrader, lib: true do
   end
 
   describe 'latest_version?' do
-    it 'should be true if newest version' do
+    it 'is true if newest version' do
       allow(upgrader).to receive(:latest_version_raw).and_return(current_version)
       expect(upgrader.latest_version?).to be_truthy
     end
   end
 
   describe 'latest_version_raw' do
-    it 'should be latest version for GitLab 5' do
+    it 'is the latest version for GitLab 5' do
       allow(upgrader).to receive(:current_version_raw).and_return("5.3.0")
       expect(upgrader.latest_version_raw).to eq("v5.4.2")
     end
 
-    it 'should get the latest version from tags' do
+    it 'gets the latest version from tags' do
       allow(upgrader).to receive(:fetch_git_tags).and_return([
         '6f0733310546402c15d3ae6128a95052f6c8ea96  refs/tags/v7.1.1',
         'facfec4b242ce151af224e20715d58e628aa5e74  refs/tags/v7.1.1^{}',
diff --git a/spec/lib/gitlab/user_access_spec.rb b/spec/lib/gitlab/user_access_spec.rb
index 5bb095366fa1d7cc2650d4f3b90ad1e498a6e2ff..d3c3b800b94b3683d42e0fd0a432099f87d7f6d7 100644
--- a/spec/lib/gitlab/user_access_spec.rb
+++ b/spec/lib/gitlab/user_access_spec.rb
@@ -9,35 +9,80 @@ describe Gitlab::UserAccess, lib: true do
     describe 'push to none protected branch' do
       it 'returns true if user is a master' do
         project.team << [user, :master]
+
         expect(access.can_push_to_branch?('random_branch')).to be_truthy
       end
 
       it 'returns true if user is a developer' do
         project.team << [user, :developer]
+
         expect(access.can_push_to_branch?('random_branch')).to be_truthy
       end
 
       it 'returns false if user is a reporter' do
         project.team << [user, :reporter]
+
         expect(access.can_push_to_branch?('random_branch')).to be_falsey
       end
     end
 
+    describe 'push to empty project' do
+      let(:empty_project) { create(:project_empty_repo) }
+      let(:project_access) { Gitlab::UserAccess.new(user, project: empty_project) }
+
+      it 'returns true if user is master' do
+        empty_project.team << [user, :master]
+
+        expect(project_access.can_push_to_branch?('master')).to be_truthy
+      end
+
+      it 'returns false if user is developer and project is fully protected' do
+        empty_project.team << [user, :developer]
+        stub_application_setting(default_branch_protection: Gitlab::Access::PROTECTION_FULL)
+
+        expect(project_access.can_push_to_branch?('master')).to be_falsey
+      end
+
+      it 'returns false if user is developer and it is not allowed to push new commits but can merge into branch' do
+        empty_project.team << [user, :developer]
+        stub_application_setting(default_branch_protection: Gitlab::Access::PROTECTION_DEV_CAN_MERGE)
+
+        expect(project_access.can_push_to_branch?('master')).to be_falsey
+      end
+
+      it 'returns true if user is developer and project is unprotected' do
+        empty_project.team << [user, :developer]
+        stub_application_setting(default_branch_protection: Gitlab::Access::PROTECTION_NONE)
+
+        expect(project_access.can_push_to_branch?('master')).to be_truthy
+      end
+
+      it 'returns true if user is developer and project grants developers permission' do
+        empty_project.team << [user, :developer]
+        stub_application_setting(default_branch_protection: Gitlab::Access::PROTECTION_DEV_CAN_PUSH)
+
+        expect(project_access.can_push_to_branch?('master')).to be_truthy
+      end
+    end
+
     describe 'push to protected branch' do
       let(:branch) { create :protected_branch, project: project }
 
       it 'returns true if user is a master' do
         project.team << [user, :master]
+
         expect(access.can_push_to_branch?(branch.name)).to be_truthy
       end
 
       it 'returns false if user is a developer' do
         project.team << [user, :developer]
+
         expect(access.can_push_to_branch?(branch.name)).to be_falsey
       end
 
       it 'returns false if user is a reporter' do
         project.team << [user, :reporter]
+
         expect(access.can_push_to_branch?(branch.name)).to be_falsey
       end
     end
@@ -49,16 +94,19 @@ describe Gitlab::UserAccess, lib: true do
 
       it 'returns true if user is a master' do
         project.team << [user, :master]
+
         expect(access.can_push_to_branch?(@branch.name)).to be_truthy
       end
 
       it 'returns true if user is a developer' do
         project.team << [user, :developer]
+
         expect(access.can_push_to_branch?(@branch.name)).to be_truthy
       end
 
       it 'returns false if user is a reporter' do
         project.team << [user, :reporter]
+
         expect(access.can_push_to_branch?(@branch.name)).to be_falsey
       end
     end
@@ -70,19 +118,21 @@ describe Gitlab::UserAccess, lib: true do
 
       it 'returns true if user is a master' do
         project.team << [user, :master]
+
         expect(access.can_merge_to_branch?(@branch.name)).to be_truthy
       end
 
       it 'returns true if user is a developer' do
         project.team << [user, :developer]
+
         expect(access.can_merge_to_branch?(@branch.name)).to be_truthy
       end
 
       it 'returns false if user is a reporter' do
         project.team << [user, :reporter]
+
         expect(access.can_merge_to_branch?(@branch.name)).to be_falsey
       end
     end
-
   end
 end
diff --git a/spec/mailers/emails/merge_requests_spec.rb b/spec/mailers/emails/merge_requests_spec.rb
new file mode 100644
index 0000000000000000000000000000000000000000..4d3811af254acd2bf5aeb1184a6a86581218a09d
--- /dev/null
+++ b/spec/mailers/emails/merge_requests_spec.rb
@@ -0,0 +1,19 @@
+require 'spec_helper'
+require 'email_spec'
+require 'mailers/shared/notify'
+
+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 c6758ccad39a160112ef2876de8894c1b2101ead..781472d0c000841a465d4819ce0c15bb07b63bda 100644
--- a/spec/mailers/emails/profile_spec.rb
+++ b/spec/mailers/emails/profile_spec.rb
@@ -48,7 +48,7 @@ describe Notify do
       it_behaves_like 'it should not have Gmail Actions links'
       it_behaves_like 'a user cannot unsubscribe through footer link'
 
-      it 'should not contain the new user\'s password' do
+      it 'does not contain the new user\'s password' do
         is_expected.not_to have_body_text /password/
       end
     end
diff --git a/spec/mailers/notify_spec.rb b/spec/mailers/notify_spec.rb
index 3685b2b17b55f4cd6cedbd25d1b6b07917a6a751..eae9c060c38eeeecf68a6b75a2f2eed5659eb359 100644
--- a/spec/mailers/notify_spec.rb
+++ b/spec/mailers/notify_spec.rb
@@ -493,7 +493,12 @@ describe Notify do
     end
 
     def invite_to_project(project:, email:, inviter:)
-      ProjectMember.add_user(project.project_members, 'toto@example.com', Gitlab::Access::DEVELOPER, inviter)
+      Member.add_user(
+        project.project_members,
+        'toto@example.com',
+        Gitlab::Access::DEVELOPER,
+        current_user: inviter
+      )
 
       project.project_members.invite.last
     end
@@ -591,7 +596,7 @@ describe Notify do
           is_expected.to have_body_text /#{note.note}/
         end
 
-        it 'not contains note author' do
+        it 'does not contain note author' do
           is_expected.not_to have_body_text /wrote\:/
         end
 
@@ -740,7 +745,12 @@ describe Notify do
     end
 
     def invite_to_group(group:, email:, inviter:)
-      GroupMember.add_user(group.group_members, 'toto@example.com', Gitlab::Access::DEVELOPER, inviter)
+      Member.add_user(
+        group.group_members,
+        'toto@example.com',
+        Gitlab::Access::DEVELOPER,
+        current_user: inviter
+      )
 
       group.group_members.invite.last
     end
@@ -944,8 +954,9 @@ describe Notify do
   describe 'email on push with multiple commits' do
     let(:example_site_path) { root_path }
     let(:user) { create(:user) }
-    let(:compare) { Gitlab::Git::Compare.new(project.repository.raw_repository, sample_image_commit.id, sample_commit.id) }
-    let(:commits) { Commit.decorate(compare.commits, nil) }
+    let(:raw_compare) { Gitlab::Git::Compare.new(project.repository.raw_repository, sample_image_commit.id, sample_commit.id) }
+    let(:compare) { Compare.decorate(raw_compare, project) }
+    let(:commits) { compare.commits }
     let(:diff_path) { namespace_project_compare_path(project.namespace, project, from: Commit.new(compare.base, project), to: Commit.new(compare.head, project)) }
     let(:send_from_committer_email) { false }
     let(:diff_refs) { Gitlab::Diff::DiffRefs.new(base_sha: project.merge_base_commit(sample_image_commit.id, sample_commit.id).id, head_sha: sample_commit.id) }
@@ -1046,8 +1057,9 @@ describe Notify do
   describe 'email on push with a single commit' do
     let(:example_site_path) { root_path }
     let(:user) { create(:user) }
-    let(:compare) { Gitlab::Git::Compare.new(project.repository.raw_repository, sample_commit.parent_id, sample_commit.id) }
-    let(:commits) { Commit.decorate(compare.commits, nil) }
+    let(:raw_compare) { Gitlab::Git::Compare.new(project.repository.raw_repository, sample_commit.parent_id, sample_commit.id) }
+    let(:compare) { Compare.decorate(raw_compare, project) }
+    let(:commits) { compare.commits }
     let(:diff_path) { namespace_project_commit_path(project.namespace, project, commits.first) }
     let(:diff_refs) { Gitlab::Diff::DiffRefs.new(base_sha: project.merge_base_commit(sample_image_commit.id, sample_commit.id).id, head_sha: sample_commit.id) }
 
diff --git a/spec/models/ability_spec.rb b/spec/models/ability_spec.rb
index 853f6943cef87546ed3c7099b83501b49b8774a8..aa3b2bbf47140e7f1e4895eae53ca92487205b02 100644
--- a/spec/models/ability_spec.rb
+++ b/spec/models/ability_spec.rb
@@ -171,6 +171,70 @@ describe Ability, lib: true do
     end
   end
 
+  shared_examples_for ".project_abilities" do |enable_request_store|
+    before do
+      RequestStore.begin! if enable_request_store
+    end
+
+    after do
+      if enable_request_store
+        RequestStore.end!
+        RequestStore.clear!
+      end
+    end
+
+    describe '.project_abilities' do
+      let!(:project) { create(:empty_project, :public) }
+      let!(:user) { create(:user) }
+
+      it 'returns permissions for admin user' do
+        admin = create(:admin)
+
+        results = described_class.project_abilities(admin, project)
+
+        expect(results.count).to eq(68)
+      end
+
+      it 'returns permissions for an owner' do
+        results = described_class.project_abilities(project.owner, project)
+
+        expect(results.count).to eq(68)
+      end
+
+      it 'returns permissions for a master' do
+        project.team << [user, :master]
+
+        results = described_class.project_abilities(user, project)
+
+        expect(results.count).to eq(60)
+      end
+
+      it 'returns permissions for a developer' do
+        project.team << [user, :developer]
+
+        results = described_class.project_abilities(user, project)
+
+        expect(results.count).to eq(44)
+      end
+
+      it 'returns permissions for a guest' do
+        project.team << [user, :guest]
+
+        results = described_class.project_abilities(user, project)
+
+        expect(results.count).to eq(21)
+      end
+    end
+  end
+
+  describe '.project_abilities with RequestStore' do
+    it_behaves_like ".project_abilities", true
+  end
+
+  describe '.project_abilities without RequestStore' do
+    it_behaves_like ".project_abilities", false
+  end
+
   describe '.issues_readable_by_user' do
     context 'with an admin user' do
       it 'returns all given issues' do
diff --git a/spec/models/application_setting_spec.rb b/spec/models/application_setting_spec.rb
index fb040ba82bc729a2956037fb5272723f4257768b..cc215d252f9cec3e3118a32eb6a7b4e58c1108dd 100644
--- a/spec/models/application_setting_spec.rb
+++ b/spec/models/application_setting_spec.rb
@@ -53,59 +53,59 @@ describe ApplicationSetting, models: true do
   end
 
   context 'restricted signup domains' do
-    it 'set single domain' do
+    it 'sets single domain' do
       setting.domain_whitelist_raw = 'example.com'
       expect(setting.domain_whitelist).to eq(['example.com'])
     end
 
-    it 'set multiple domains with spaces' do
+    it 'sets multiple domains with spaces' do
       setting.domain_whitelist_raw = 'example.com *.example.com'
       expect(setting.domain_whitelist).to eq(['example.com', '*.example.com'])
     end
 
-    it 'set multiple domains with newlines and a space' do
+    it 'sets multiple domains with newlines and a space' do
       setting.domain_whitelist_raw = "example.com\n *.example.com"
       expect(setting.domain_whitelist).to eq(['example.com', '*.example.com'])
     end
 
-    it 'set multiple domains with commas' do
+    it 'sets multiple domains with commas' do
       setting.domain_whitelist_raw = "example.com, *.example.com"
       expect(setting.domain_whitelist).to eq(['example.com', '*.example.com'])
     end
   end
 
   context 'blacklisted signup domains' do
-    it 'set single domain' do
+    it 'sets single domain' do
       setting.domain_blacklist_raw = 'example.com'
       expect(setting.domain_blacklist).to contain_exactly('example.com')
     end
 
-    it 'set multiple domains with spaces' do
+    it 'sets multiple domains with spaces' do
       setting.domain_blacklist_raw = 'example.com *.example.com'
       expect(setting.domain_blacklist).to contain_exactly('example.com', '*.example.com')
     end
 
-    it 'set multiple domains with newlines and a space' do
+    it 'sets multiple domains with newlines and a space' do
       setting.domain_blacklist_raw = "example.com\n *.example.com"
       expect(setting.domain_blacklist).to contain_exactly('example.com', '*.example.com')
     end
 
-    it 'set multiple domains with commas' do
+    it 'sets multiple domains with commas' do
       setting.domain_blacklist_raw = "example.com, *.example.com"
       expect(setting.domain_blacklist).to contain_exactly('example.com', '*.example.com')
     end
 
-    it 'set multiple domains with semicolon' do
+    it 'sets multiple domains with semicolon' do
       setting.domain_blacklist_raw = "example.com; *.example.com"
       expect(setting.domain_blacklist).to contain_exactly('example.com', '*.example.com')
     end
 
-    it 'set multiple domains with mixture of everything' do
+    it 'sets multiple domains with mixture of everything' do
       setting.domain_blacklist_raw = "example.com; *.example.com\n test.com\sblock.com   yes.com"
       expect(setting.domain_blacklist).to contain_exactly('example.com', '*.example.com', 'test.com', 'block.com', 'yes.com')
     end
 
-    it 'set multiple domain with file' do
+    it 'sets multiple domain with file' do
       setting.domain_blacklist_file = File.open(Rails.root.join('spec/fixtures/', 'domain_blacklist.txt'))
       expect(setting.domain_blacklist).to contain_exactly('example.com', 'test.com', 'foo.bar')
     end
diff --git a/spec/models/blob_spec.rb b/spec/models/blob_spec.rb
index 1e5d6a34f83d7a4d631ca65da3a736278ec7a832..cee20234e1f8fae94929c46f503880627921d638 100644
--- a/spec/models/blob_spec.rb
+++ b/spec/models/blob_spec.rb
@@ -94,4 +94,26 @@ describe Blob do
       expect(blob.to_partial_path).to eq 'download'
     end
   end
+
+  describe '#size_within_svg_limits?' do
+    let(:blob) { described_class.decorate(double(:blob)) }
+
+    it 'returns true when the blob size is smaller than the SVG limit' do
+      expect(blob).to receive(:size).and_return(42)
+
+      expect(blob.size_within_svg_limits?).to eq(true)
+    end
+
+    it 'returns true when the blob size is equal to the SVG limit' do
+      expect(blob).to receive(:size).and_return(Blob::MAXIMUM_SVG_SIZE)
+
+      expect(blob.size_within_svg_limits?).to eq(true)
+    end
+
+    it 'returns false when the blob size is larger than the SVG limit' do
+      expect(blob).to receive(:size).and_return(1.terabyte)
+
+      expect(blob.size_within_svg_limits?).to eq(false)
+    end
+  end
 end
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 6ad8bfef4f22205ed2ab762b99f0d9e9495b9183..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 }
@@ -23,19 +21,19 @@ describe BroadcastMessage, models: true do
   end
 
   describe '.current' do
-    it "should return last message if time match" do
+    it "returns last message if time match" do
       message = create(:broadcast_message)
 
       expect(BroadcastMessage.current).to eq message
     end
 
-    it "should return nil if time not come" do
+    it "returns nil if time not come" do
       create(:broadcast_message, :future)
 
       expect(BroadcastMessage.current).to be_nil
     end
 
-    it "should return nil if time has passed" do
+    it "returns nil if time has passed" do
       create(:broadcast_message, :expired)
 
       expect(BroadcastMessage.current).to be_nil
diff --git a/spec/models/build_spec.rb b/spec/models/build_spec.rb
index 47c489e6af185e3a100b14cdc394948a29934420..f63183f5df19faca0c2913e047899f9a8da7633c 100644
--- a/spec/models/build_spec.rb
+++ b/spec/models/build_spec.rb
@@ -32,7 +32,7 @@ describe Ci::Build, models: true do
     end
     let(:create_from_build) { Ci::Build.create_from build }
 
-    it 'there should be a pending task' do
+    it 'exists a pending task' do
       expect(Ci::Build.pending.count(:all)).to eq 0
       create_from_build
       expect(Ci::Build.pending.count(:all)).to be > 0
@@ -42,7 +42,7 @@ describe Ci::Build, models: true do
   describe '#ignored?' do
     subject { build.ignored? }
 
-    context 'if build is not allowed to fail' do
+    context 'when build is not allowed to fail' do
       before do
         build.allow_failure = false
       end
@@ -64,7 +64,7 @@ describe Ci::Build, models: true do
       end
     end
 
-    context 'if build is allowed to fail' do
+    context 'when build is allowed to fail' do
       before do
         build.allow_failure = true
       end
@@ -92,7 +92,7 @@ describe Ci::Build, models: true do
 
     it { is_expected.to be_empty }
 
-    context 'if build.trace contains text' do
+    context 'when build.trace contains text' do
       let(:text) { 'example output' }
       before do
         build.trace = text
@@ -102,7 +102,7 @@ describe Ci::Build, models: true do
       it { expect(subject.length).to be >= text.length }
     end
 
-    context 'if build.trace hides token' do
+    context 'when build.trace hides token' do
       let(:token) { 'my_secret_token' }
 
       before do
@@ -284,13 +284,13 @@ describe Ci::Build, models: true do
           stub_ci_pipeline_yaml_file(config)
         end
 
-        context 'if config is not found' do
+        context 'when config is not found' do
           let(:config) { nil }
 
           it { is_expected.to eq(predefined_variables) }
         end
 
-        context 'if config does not have a questioned job' do
+        context 'when config does not have a questioned job' do
           let(:config) do
             YAML.dump({
               test_other: {
@@ -302,7 +302,7 @@ describe Ci::Build, models: true do
           it { is_expected.to eq(predefined_variables) }
         end
 
-        context 'if config has variables' do
+        context 'when config has variables' do
           let(:config) do
             YAML.dump({
               test: {
@@ -394,7 +394,7 @@ describe Ci::Build, models: true do
       it { is_expected.to be_falsey }
     end
 
-    context 'if there are runner' do
+    context 'when there are runners' do
       let(:runner) { create(:ci_runner) }
 
       before do
@@ -424,29 +424,27 @@ describe Ci::Build, models: true do
   describe '#stuck?' do
     subject { build.stuck? }
 
-    %w(pending).each do |state|
-      context "if commit_status.status is #{state}" do
-        before do
-          build.status = state
-        end
+    context "when commit_status.status is pending" do
+      before do
+        build.status = 'pending'
+      end
 
-        it { is_expected.to be_truthy }
+      it { is_expected.to be_truthy }
 
-        context "and there are specific runner" do
-          let(:runner) { create(:ci_runner, contacted_at: 1.second.ago) }
+      context "and there are specific runner" do
+        let(:runner) { create(:ci_runner, contacted_at: 1.second.ago) }
 
-          before do
-            build.project.runners << runner
-            runner.save
-          end
-
-          it { is_expected.to be_falsey }
+        before do
+          build.project.runners << runner
+          runner.save
         end
+
+        it { is_expected.to be_falsey }
       end
     end
 
-    %w(success failed canceled running).each do |state|
-      context "if commit_status.status is #{state}" do
+    %w[success failed canceled running].each do |state|
+      context "when commit_status.status is #{state}" do
         before do
           build.status = state
         end
@@ -574,19 +572,19 @@ describe Ci::Build, models: true do
     let!(:rubocop_test) { create(:ci_build, pipeline: pipeline, name: 'rubocop', stage_idx: 1, stage: 'test') }
     let!(:staging) { create(:ci_build, pipeline: pipeline, name: 'staging', stage_idx: 2, stage: 'deploy') }
 
-    it 'to have no dependents if this is first build' do
+    it 'expects to have no dependents if this is first build' do
       expect(build.depends_on_builds).to be_empty
     end
 
-    it 'to have one dependent if this is test' do
+    it 'expects to have one dependent if this is test' do
       expect(rspec_test.depends_on_builds.map(&:id)).to contain_exactly(build.id)
     end
 
-    it 'to have all builds from build and test stage if this is last' do
+    it 'expects to have all builds from build and test stage if this is last' do
       expect(staging.depends_on_builds.map(&:id)).to contain_exactly(build.id, rspec_test.id, rubocop_test.id)
     end
 
-    it 'to have retried builds instead the original ones' do
+    it 'expects to have retried builds instead the original ones' do
       retried_rspec = Ci::Build.retry(rspec_test)
       expect(staging.depends_on_builds.map(&:id)).to contain_exactly(build.id, retried_rspec.id, rubocop_test.id)
     end
@@ -656,23 +654,23 @@ describe Ci::Build, models: true do
 
   describe 'build erasable' do
     shared_examples 'erasable' do
-      it 'should remove artifact file' do
+      it 'removes artifact file' do
         expect(build.artifacts_file.exists?).to be_falsy
       end
 
-      it 'should remove artifact metadata file' do
+      it 'removes artifact metadata file' do
         expect(build.artifacts_metadata.exists?).to be_falsy
       end
 
-      it 'should erase build trace in trace file' do
+      it 'erases build trace in trace file' do
         expect(build.trace).to be_empty
       end
 
-      it 'should set erased to true' do
+      it 'sets erased to true' do
         expect(build.erased?).to be true
       end
 
-      it 'should set erase date' do
+      it 'sets erase date' do
         expect(build.erased_at).not_to be_falsy
       end
     end
@@ -705,7 +703,7 @@ describe Ci::Build, models: true do
 
           include_examples 'erasable'
 
-          it 'should record user who erased a build' do
+          it 'records user who erased a build' do
             expect(build.erased_by).to eq user
           end
         end
@@ -715,7 +713,7 @@ describe Ci::Build, models: true do
 
           include_examples 'erasable'
 
-          it 'should not set user who erased a build' do
+          it 'does not set user who erased a build' do
             expect(build.erased_by).to be_nil
           end
         end
@@ -751,7 +749,7 @@ describe Ci::Build, models: true do
         end
 
         describe '#erase' do
-          it 'should not raise error' do
+          it 'does not raise error' do
             expect { build.erase }.not_to raise_error
           end
         end
@@ -765,6 +763,53 @@ describe Ci::Build, models: true do
     end
   end
 
+  describe '#when' do
+    subject { build.when }
+
+    context 'when `when` is undefined' do
+      before do
+        build.when = nil
+      end
+
+      context 'use from gitlab-ci.yml' do
+        before do
+          stub_ci_pipeline_yaml_file(config)
+        end
+
+        context 'when config is not found' do
+          let(:config) { nil }
+
+          it { is_expected.to eq('on_success') }
+        end
+
+        context 'when config does not have a questioned job' do
+          let(:config) do
+            YAML.dump({
+                        test_other: {
+                          script: 'Hello World'
+                        }
+                      })
+          end
+
+          it { is_expected.to eq('on_success') }
+        end
+
+        context 'when config has `when`' do
+          let(:config) do
+            YAML.dump({
+                        test: {
+                          script: 'Hello World',
+                          when: 'always'
+                        }
+                      })
+          end
+
+          it { is_expected.to eq('always') }
+        end
+      end
+    end
+  end
+
   describe '#retryable?' do
     context 'when build is running' do
       before do
@@ -835,13 +880,15 @@ describe Ci::Build, models: true do
 
     subject { build.play }
 
-    it 'enques a build' do
+    it 'enqueues a build' do
       is_expected.to be_pending
       is_expected.to eq(build)
     end
 
-    context 'for success build' do
-      before { build.queue }
+    context 'for successful build' do
+      before do
+        build.update(status: 'success')
+      end
 
       it 'creates a new build' do
         is_expected.to be_pending
@@ -853,7 +900,7 @@ describe Ci::Build, models: true do
   describe '#when' do
     subject { build.when }
 
-    context 'if is undefined' do
+    context 'when `when` is undefined' do
       before do
         build.update(when: nil)
         build.reload # reload pipeline so that it resets config_processor
@@ -864,13 +911,13 @@ describe Ci::Build, models: true do
           stub_ci_pipeline_yaml_file(config)
         end
 
-        context 'if config is not found' do
+        context 'when config is not found' do
           let(:config) { nil }
 
           it { is_expected.to eq('on_success') }
         end
 
-        context 'if config does not have a questioned job' do
+        context 'when config does not have a questioned job' do
           let(:config) do
             YAML.dump({
                         test_other: {
@@ -882,7 +929,7 @@ describe Ci::Build, models: true do
           it { is_expected.to eq('on_success') }
         end
 
-        context 'if config has when' do
+        context 'when config has when' do
           let(:config) do
             YAML.dump({
                         test: {
@@ -902,7 +949,7 @@ describe Ci::Build, models: true do
     context 'when build is running' do
       before { build.run! }
 
-      it 'should return false' do
+      it 'returns false' do
         expect(build.retryable?).to be false
       end
     end
@@ -910,7 +957,7 @@ describe Ci::Build, models: true do
     context 'when build is finished' do
       before { build.success! }
 
-      it 'should return true' do
+      it 'returns true' do
         expect(build.retryable?).to be true
       end
     end
diff --git a/spec/models/ci/pipeline_spec.rb b/spec/models/ci/pipeline_spec.rb
index aa05fc78f9436096606cdc44f1d7ba34c1c4a634..721b20e0cb28d79c62446c6184e140c4e9bc9a26 100644
--- a/spec/models/ci/pipeline_spec.rb
+++ b/spec/models/ci/pipeline_spec.rb
@@ -2,7 +2,7 @@ require 'spec_helper'
 
 describe Ci::Pipeline, models: true do
   let(:project) { FactoryGirl.create :empty_project }
-  let(:pipeline) { FactoryGirl.create :ci_pipeline, project: project }
+  let(:pipeline) { FactoryGirl.create :ci_empty_pipeline, status: 'created', project: project }
 
   it { is_expected.to belong_to(:project) }
   it { is_expected.to belong_to(:user) }
@@ -18,6 +18,8 @@ describe Ci::Pipeline, models: true do
   it { is_expected.to respond_to :git_author_email }
   it { is_expected.to respond_to :short_sha }
 
+  it { is_expected.to delegate_method(:stages).to(:statuses) }
+
   describe '#valid_commit_sha' do
     context 'commit.sha can not start with 00000000' do
       before do
@@ -38,9 +40,6 @@ describe Ci::Pipeline, models: true do
     it { expect(pipeline.sha).to start_with(subject) }
   end
 
-  describe '#create_next_builds' do
-  end
-
   describe '#retried' do
     subject { pipeline.retried }
 
@@ -54,312 +53,9 @@ describe Ci::Pipeline, models: true do
     end
   end
 
-  describe '#create_builds' do
-    let!(:pipeline) { FactoryGirl.create :ci_pipeline, project: project, ref: 'master', tag: false }
-
-    def create_builds(trigger_request = nil)
-      pipeline.create_builds(nil, trigger_request)
-    end
-
-    def create_next_builds
-      pipeline.create_next_builds(pipeline.builds.order(:id).last)
-    end
-
-    it 'creates builds' do
-      expect(create_builds).to be_truthy
-      pipeline.builds.update_all(status: "success")
-      expect(pipeline.builds.count(:all)).to eq(2)
-
-      expect(create_next_builds).to be_truthy
-      pipeline.builds.update_all(status: "success")
-      expect(pipeline.builds.count(:all)).to eq(4)
-
-      expect(create_next_builds).to be_truthy
-      pipeline.builds.update_all(status: "success")
-      expect(pipeline.builds.count(:all)).to eq(5)
-
-      expect(create_next_builds).to be_falsey
-    end
-
-    context 'custom stage with first job allowed to fail' do
-      let(:yaml) do
-        {
-          stages: ['clean', 'test'],
-          clean_job: {
-            stage: 'clean',
-            allow_failure: true,
-            script: 'BUILD',
-          },
-          test_job: {
-            stage: 'test',
-            script: 'TEST',
-          },
-        }
-      end
-
-      before do
-        stub_ci_pipeline_yaml_file(YAML.dump(yaml))
-        create_builds
-      end
-
-      it 'properly schedules builds' do
-        expect(pipeline.builds.pluck(:status)).to contain_exactly('pending')
-        pipeline.builds.running_or_pending.each(&:drop)
-        expect(pipeline.builds.pluck(:status)).to contain_exactly('pending', 'failed')
-      end
-    end
-
-    context 'properly creates builds when "when" is defined' do
-      let(:yaml) do
-        {
-          stages: ["build", "test", "test_failure", "deploy", "cleanup"],
-          build: {
-            stage: "build",
-            script: "BUILD",
-          },
-          test: {
-            stage: "test",
-            script: "TEST",
-          },
-          test_failure: {
-            stage: "test_failure",
-            script: "ON test failure",
-            when: "on_failure",
-          },
-          deploy: {
-            stage: "deploy",
-            script: "PUBLISH",
-          },
-          cleanup: {
-            stage: "cleanup",
-            script: "TIDY UP",
-            when: "always",
-          }
-        }
-      end
-
-      before do
-        stub_ci_pipeline_yaml_file(YAML.dump(yaml))
-      end
-
-      context 'when builds are successful' do
-        it 'properly creates builds' do
-          expect(create_builds).to be_truthy
-          expect(pipeline.builds.pluck(:name)).to contain_exactly('build')
-          expect(pipeline.builds.pluck(:status)).to contain_exactly('pending')
-          pipeline.builds.running_or_pending.each(&:success)
-
-          expect(pipeline.builds.pluck(:name)).to contain_exactly('build', 'test')
-          expect(pipeline.builds.pluck(:status)).to contain_exactly('success', 'pending')
-          pipeline.builds.running_or_pending.each(&:success)
-
-          expect(pipeline.builds.pluck(:name)).to contain_exactly('build', 'test', 'deploy')
-          expect(pipeline.builds.pluck(:status)).to contain_exactly('success', 'success', 'pending')
-          pipeline.builds.running_or_pending.each(&:success)
-
-          expect(pipeline.builds.pluck(:name)).to contain_exactly('build', 'test', 'deploy', 'cleanup')
-          expect(pipeline.builds.pluck(:status)).to contain_exactly('success', 'success', 'success', 'pending')
-          pipeline.builds.running_or_pending.each(&:success)
-
-          expect(pipeline.builds.pluck(:status)).to contain_exactly('success', 'success', 'success', 'success')
-          pipeline.reload
-          expect(pipeline.status).to eq('success')
-        end
-      end
-
-      context 'when test job fails' do
-        it 'properly creates builds' do
-          expect(create_builds).to be_truthy
-          expect(pipeline.builds.pluck(:name)).to contain_exactly('build')
-          expect(pipeline.builds.pluck(:status)).to contain_exactly('pending')
-          pipeline.builds.running_or_pending.each(&:success)
-
-          expect(pipeline.builds.pluck(:name)).to contain_exactly('build', 'test')
-          expect(pipeline.builds.pluck(:status)).to contain_exactly('success', 'pending')
-          pipeline.builds.running_or_pending.each(&:drop)
-
-          expect(pipeline.builds.pluck(:name)).to contain_exactly('build', 'test', 'test_failure')
-          expect(pipeline.builds.pluck(:status)).to contain_exactly('success', 'failed', 'pending')
-          pipeline.builds.running_or_pending.each(&:success)
-
-          expect(pipeline.builds.pluck(:name)).to contain_exactly('build', 'test', 'test_failure', 'cleanup')
-          expect(pipeline.builds.pluck(:status)).to contain_exactly('success', 'failed', 'success', 'pending')
-          pipeline.builds.running_or_pending.each(&:success)
-
-          expect(pipeline.builds.pluck(:status)).to contain_exactly('success', 'failed', 'success', 'success')
-          pipeline.reload
-          expect(pipeline.status).to eq('failed')
-        end
-      end
-
-      context 'when test and test_failure jobs fail' do
-        it 'properly creates builds' do
-          expect(create_builds).to be_truthy
-          expect(pipeline.builds.pluck(:name)).to contain_exactly('build')
-          expect(pipeline.builds.pluck(:status)).to contain_exactly('pending')
-          pipeline.builds.running_or_pending.each(&:success)
-
-          expect(pipeline.builds.pluck(:name)).to contain_exactly('build', 'test')
-          expect(pipeline.builds.pluck(:status)).to contain_exactly('success', 'pending')
-          pipeline.builds.running_or_pending.each(&:drop)
-
-          expect(pipeline.builds.pluck(:name)).to contain_exactly('build', 'test', 'test_failure')
-          expect(pipeline.builds.pluck(:status)).to contain_exactly('success', 'failed', 'pending')
-          pipeline.builds.running_or_pending.each(&:drop)
-
-          expect(pipeline.builds.pluck(:name)).to contain_exactly('build', 'test', 'test_failure', 'cleanup')
-          expect(pipeline.builds.pluck(:status)).to contain_exactly('success', 'failed', 'failed', 'pending')
-          pipeline.builds.running_or_pending.each(&:success)
-
-          expect(pipeline.builds.pluck(:name)).to contain_exactly('build', 'test', 'test_failure', 'cleanup')
-          expect(pipeline.builds.pluck(:status)).to contain_exactly('success', 'failed', 'failed', 'success')
-          pipeline.reload
-          expect(pipeline.status).to eq('failed')
-        end
-      end
-
-      context 'when deploy job fails' do
-        it 'properly creates builds' do
-          expect(create_builds).to be_truthy
-          expect(pipeline.builds.pluck(:name)).to contain_exactly('build')
-          expect(pipeline.builds.pluck(:status)).to contain_exactly('pending')
-          pipeline.builds.running_or_pending.each(&:success)
-
-          expect(pipeline.builds.pluck(:name)).to contain_exactly('build', 'test')
-          expect(pipeline.builds.pluck(:status)).to contain_exactly('success', 'pending')
-          pipeline.builds.running_or_pending.each(&:success)
-
-          expect(pipeline.builds.pluck(:name)).to contain_exactly('build', 'test', 'deploy')
-          expect(pipeline.builds.pluck(:status)).to contain_exactly('success', 'success', 'pending')
-          pipeline.builds.running_or_pending.each(&:drop)
-
-          expect(pipeline.builds.pluck(:name)).to contain_exactly('build', 'test', 'deploy', 'cleanup')
-          expect(pipeline.builds.pluck(:status)).to contain_exactly('success', 'success', 'failed', 'pending')
-          pipeline.builds.running_or_pending.each(&:success)
-
-          expect(pipeline.builds.pluck(:status)).to contain_exactly('success', 'success', 'failed', 'success')
-          pipeline.reload
-          expect(pipeline.status).to eq('failed')
-        end
-      end
-
-      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(pipeline.builds.pluck(:name)).to contain_exactly('build')
-          expect(pipeline.builds.pluck(:status)).to contain_exactly('pending')
-          pipeline.builds.running_or_pending.each(&:success)
-
-          expect(pipeline.builds.running_or_pending).not_to be_empty
-
-          expect(pipeline.builds.pluck(:name)).to contain_exactly('build', 'test')
-          expect(pipeline.builds.pluck(:status)).to contain_exactly('success', 'pending')
-          pipeline.builds.running_or_pending.each(&:cancel)
-
-          expect(pipeline.builds.running_or_pending).to be_empty
-          expect(pipeline.reload.status).to eq('canceled')
-        end
-      end
-
-      context 'when listing manual actions' do
-        let(:yaml) do
-          {
-            stages: ["build", "test", "staging", "production", "cleanup"],
-            build: {
-              stage: "build",
-              script: "BUILD",
-            },
-            test: {
-              stage: "test",
-              script: "TEST",
-            },
-            staging: {
-              stage: "staging",
-              script: "PUBLISH",
-            },
-            production: {
-              stage: "production",
-              script: "PUBLISH",
-              when: "manual",
-            },
-            cleanup: {
-              stage: "cleanup",
-              script: "TIDY UP",
-              when: "always",
-            },
-            clear_cache: {
-              stage: "cleanup",
-              script: "CLEAR CACHE",
-              when: "manual",
-            }
-          }
-        end
-
-        it 'returns only for skipped builds' do
-          # currently all builds are created
-          expect(create_builds).to be_truthy
-          expect(manual_actions).to be_empty
-
-          # succeed stage build
-          pipeline.builds.running_or_pending.each(&:success)
-          expect(manual_actions).to be_empty
-
-          # succeed stage test
-          pipeline.builds.running_or_pending.each(&:success)
-          expect(manual_actions).to be_empty
-
-          # succeed stage staging and skip stage production
-          pipeline.builds.running_or_pending.each(&:success)
-          expect(manual_actions).to be_many # production and clear cache
-
-          # succeed stage cleanup
-          pipeline.builds.running_or_pending.each(&:success)
-
-          # after processing a pipeline we should have 6 builds, 5 succeeded
-          expect(pipeline.builds.count).to eq(6)
-          expect(pipeline.builds.success.count).to eq(4)
-        end
-
-        def manual_actions
-          pipeline.manual_actions
-        end
-      end
-    end
-
-    context 'when no builds created' do
-      let(:pipeline) { build(:ci_pipeline) }
-
-      before do
-        stub_ci_pipeline_yaml_file(YAML.dump(before_script: ['ls']))
-      end
-
-      it 'returns false' do
-        expect(pipeline.create_builds(nil)).to be_falsey
-        expect(pipeline).not_to be_persisted
-      end
-    end
-  end
-
-  describe "#finished_at" do
-    let(:pipeline) { FactoryGirl.create :ci_pipeline }
-
-    it "returns finished_at of latest build" do
-      build = FactoryGirl.create :ci_build, pipeline: pipeline, finished_at: Time.now - 60
-      FactoryGirl.create :ci_build, pipeline: pipeline, finished_at: Time.now - 120
-
-      expect(pipeline.finished_at.to_i).to eq(build.finished_at.to_i)
-    end
-
-    it "returns nil if there is no finished build" do
-      FactoryGirl.create :ci_not_started_build, pipeline: pipeline
-
-      expect(pipeline.finished_at).to be_nil
-    end
-  end
-
   describe "coverage" do
     let(:project) { FactoryGirl.create :empty_project, build_coverage_regex: "/.*/" }
-    let(:pipeline) { FactoryGirl.create :ci_pipeline, project: project }
+    let(:pipeline) { FactoryGirl.create :ci_empty_pipeline, project: project }
 
     it "calculates average when there are two builds with coverage" do
       FactoryGirl.create :ci_build, name: "rspec", coverage: 30, pipeline: pipeline
@@ -426,33 +122,51 @@ describe Ci::Pipeline, models: true do
     end
   end
 
-  describe '#update_state' do
-    it 'execute update_state after touching object' do
-      expect(pipeline).to receive(:update_state).and_return(true)
-      pipeline.touch
+  describe 'state machine' do
+    let(:current) { Time.now.change(usec: 0) }
+    let(:build) { create :ci_build, name: 'build1', pipeline: pipeline }
+
+    describe '#duration' do
+      before do
+        travel_to(current - 120) do
+          pipeline.run
+        end
+
+        travel_to(current) do
+          pipeline.succeed
+        end
+      end
+
+      it 'matches sum of builds duration' do
+        expect(pipeline.reload.duration).to eq(120)
+      end
     end
 
-    context 'dependent objects' do
-      let(:commit_status) { build :commit_status, pipeline: pipeline }
+    describe '#started_at' do
+      it 'updates on transitioning to running' do
+        build.run
+
+        expect(pipeline.reload.started_at).not_to be_nil
+      end
+
+      it 'does not update on transitioning to success' do
+        build.success
 
-      it 'execute update_state after saving dependent object' do
-        expect(pipeline).to receive(:update_state).and_return(true)
-        commit_status.save
+        expect(pipeline.reload.started_at).to be_nil
       end
     end
 
-    context 'update state' do
-      let(:current) { Time.now.change(usec: 0) }
-      let(:build) { FactoryGirl.create :ci_build, :success, pipeline: pipeline, started_at: current - 120, finished_at: current - 60 }
+    describe '#finished_at' do
+      it 'updates on transitioning to success' do
+        build.success
 
-      before do
-        build
+        expect(pipeline.reload.finished_at).not_to be_nil
       end
 
-      [:status, :started_at, :finished_at, :duration].each do |param|
-        it "update #{param}" do
-          expect(pipeline.send(param)).to eq(build.send(param))
-        end
+      it 'does not update on transitioning to running' do
+        build.run
+
+        expect(pipeline.reload.finished_at).to be_nil
       end
     end
   end
@@ -543,32 +257,146 @@ describe Ci::Pipeline, models: true do
     end
   end
 
+  describe '#status' do
+    let!(:build) { create(:ci_build, :created, pipeline: pipeline, name: 'test') }
+
+    subject { pipeline.reload.status }
+
+    context 'on queuing' do
+      before do
+        build.enqueue
+      end
+
+      it { is_expected.to eq('pending') }
+    end
+
+    context 'on run' do
+      before do
+        build.enqueue
+        build.run
+      end
+
+      it { is_expected.to eq('running') }
+    end
+
+    context 'on drop' do
+      before do
+        build.drop
+      end
+
+      it { is_expected.to eq('failed') }
+    end
+
+    context 'on success' do
+      before do
+        build.success
+      end
+
+      it { is_expected.to eq('success') }
+    end
+
+    context 'on cancel' do
+      before do
+        build.cancel
+      end
+
+      it { is_expected.to eq('canceled') }
+    end
+
+    context 'on failure and build retry' do
+      before do
+        build.drop
+        Ci::Build.retry(build)
+      end
+
+      # We are changing a state: created > failed > running
+      # Instead of: created > failed > pending
+      # Since the pipeline already run, so it should not be pending anymore
+
+      it { is_expected.to eq('running') }
+    end
+  end
+
   describe '#execute_hooks' do
+    let!(:build_a) { create_build('a') }
+    let!(:build_b) { create_build('b') }
+
     let!(:hook) do
       create(:project_hook, project: project, pipeline_events: enabled)
     end
-    let(:enabled) { raise NotImplementedError }
 
     before do
-      WebMock.stub_request(:post, hook.url)
-      pipeline.touch
       ProjectWebHookWorker.drain
     end
 
     context 'with pipeline hooks enabled' do
       let(:enabled) { true }
 
-      it 'executes pipeline_hook after touched' do
-        expect(WebMock).to have_requested(:post, hook.url).once
+      before do
+        WebMock.stub_request(:post, hook.url)
+      end
+
+      context 'with multiple builds' do
+        context 'when build is queued' do
+          before do
+            build_a.enqueue
+            build_b.enqueue
+          end
+
+          it 'receive a pending event once' do
+            expect(WebMock).to have_requested_pipeline_hook('pending').once
+          end
+        end
+
+        context 'when build is run' do
+          before do
+            build_a.enqueue
+            build_a.run
+            build_b.enqueue
+            build_b.run
+          end
+
+          it 'receive a running event once' do
+            expect(WebMock).to have_requested_pipeline_hook('running').once
+          end
+        end
+
+        context 'when all builds succeed' do
+          before do
+            build_a.success
+            build_b.success
+          end
+
+          it 'receive a success event once' do
+            expect(WebMock).to have_requested_pipeline_hook('success').once
+          end
+        end
+
+        def have_requested_pipeline_hook(status)
+          have_requested(:post, hook.url).with do |req|
+            json_body = JSON.parse(req.body)
+            json_body['object_attributes']['status'] == status &&
+              json_body['builds'].length == 2
+          end
+        end
       end
     end
 
     context 'with pipeline hooks disabled' do
       let(:enabled) { false }
 
+      before do
+        build_a.enqueue
+        build_b.enqueue
+      end
+
       it 'did not execute pipeline_hook after touched' do
         expect(WebMock).not_to have_requested(:post, hook.url)
       end
     end
+
+    def create_build(name)
+      create(:ci_build, :created, pipeline: pipeline, name: name)
+    end
   end
 end
diff --git a/spec/models/ci/trigger_spec.rb b/spec/models/ci/trigger_spec.rb
index 474b0b1621de3f94006f3ef4275f64d76b6bed08..3ca9231f58e3ea88cafbc7ee858f8ef5d91fdb39 100644
--- a/spec/models/ci/trigger_spec.rb
+++ b/spec/models/ci/trigger_spec.rb
@@ -4,12 +4,12 @@ describe Ci::Trigger, models: true do
   let(:project) { FactoryGirl.create :empty_project }
 
   describe 'before_validation' do
-    it 'should set an random token if none provided' do
+    it 'sets an random token if none provided' do
       trigger = FactoryGirl.create :ci_trigger_without_token, project: project
       expect(trigger.token).not_to be_nil
     end
 
-    it 'should not set an random token if one provided' do
+    it 'does not set an random token if one provided' do
       trigger = FactoryGirl.create :ci_trigger, project: project
       expect(trigger.token).to eq('token')
     end
diff --git a/spec/models/commit_spec.rb b/spec/models/commit_spec.rb
index c3392ee7440ca67a7d57df48e4fb47ab9527223b..d3e6a6648cc647cc9c7aeac11e7edbbdcef6553b 100644
--- a/spec/models/commit_spec.rb
+++ b/spec/models/commit_spec.rb
@@ -86,6 +86,27 @@ eos
     end
   end
 
+  describe '#full_title' do
+    it "returns no_commit_message when safe_message is blank" do
+      allow(commit).to receive(:safe_message).and_return('')
+      expect(commit.full_title).to eq("--no commit message")
+    end
+
+    it "returns entire message if there is no newline" do
+      message = 'Lorem ipsum dolor sit amet, consectetur adipiscing elit. Donec sodales id felis id blandit. Vivamus egestas lacinia lacus, sed rutrum mauris.'
+
+      allow(commit).to receive(:safe_message).and_return(message)
+      expect(commit.full_title).to eq(message)
+    end
+
+    it "returns first line of message if there is a newLine" do
+      message = commit.safe_message.split(" ").first
+
+      allow(commit).to receive(:safe_message).and_return(message + "\n" + message)
+      expect(commit.full_title).to eq(message)
+    end
+  end
+
   describe "delegation" do
     subject { commit }
 
diff --git a/spec/models/commit_status_spec.rb b/spec/models/commit_status_spec.rb
index ff6371ad68540677ac639f1090d4bbeb8dc9935a..fcfa3138ce50b16cc2b0f5e7ae34acf15da63e5d 100644
--- a/spec/models/commit_status_spec.rb
+++ b/spec/models/commit_status_spec.rb
@@ -133,7 +133,7 @@ describe CommitStatus, models: true do
       @commit5 = FactoryGirl.create :commit_status, pipeline: pipeline, name: 'aa', ref: 'bb', status: 'success'
     end
 
-    it 'return unique statuses' do
+    it 'returns unique statuses' do
       is_expected.to eq([@commit4, @commit5])
     end
   end
@@ -149,7 +149,7 @@ describe CommitStatus, models: true do
       @commit5 = FactoryGirl.create :commit_status, pipeline: pipeline, name: 'ee', ref: nil, status: 'canceled'
     end
 
-    it 'return statuses that are running or pending' do
+    it 'returns statuses that are running or pending' do
       is_expected.to eq([@commit1, @commit2])
     end
   end
@@ -160,7 +160,7 @@ describe CommitStatus, models: true do
     context 'when no before_sha is set for pipeline' do
       before { pipeline.before_sha = nil }
 
-      it 'return blank sha' do
+      it 'returns blank sha' do
         is_expected.to eq(Gitlab::Git::BLANK_SHA)
       end
     end
@@ -169,7 +169,7 @@ describe CommitStatus, models: true do
       let(:value) { '1234' }
       before { pipeline.before_sha = value }
 
-      it 'return the set value' do
+      it 'returns the set value' do
         is_expected.to eq(value)
       end
     end
@@ -186,7 +186,7 @@ describe CommitStatus, models: true do
     context 'stages list' do
       subject { CommitStatus.where(pipeline: pipeline).stages }
 
-      it 'return ordered list of stages' do
+      it 'returns ordered list of stages' do
         is_expected.to eq(%w(build test deploy))
       end
     end
@@ -194,7 +194,7 @@ describe CommitStatus, models: true do
     context 'stages with statuses' do
       subject { CommitStatus.where(pipeline: pipeline).latest.stages_status }
 
-      it 'return list of stages with statuses' do
+      it 'returns list of stages with statuses' do
         is_expected.to eq({
           'build' => 'failed',
           'test' => 'success',
diff --git a/spec/models/compare_spec.rb b/spec/models/compare_spec.rb
new file mode 100644
index 0000000000000000000000000000000000000000..49ab3c4b6e99ef2fe000afd3ebf88c28a4d0cce6
--- /dev/null
+++ b/spec/models/compare_spec.rb
@@ -0,0 +1,77 @@
+require 'spec_helper'
+
+describe Compare, models: true do
+  include RepoHelpers
+
+  let(:project) { create(:project, :public) }
+  let(:commit)  { project.commit }
+
+  let(:start_commit) { sample_image_commit }
+  let(:head_commit) { sample_commit }
+
+  let(:raw_compare) { Gitlab::Git::Compare.new(project.repository.raw_repository, start_commit.id, head_commit.id) }
+
+  subject { described_class.new(raw_compare, project) }
+
+  describe '#start_commit' do
+    it 'returns raw compare base commit' do
+      expect(subject.start_commit.id).to eq(start_commit.id)
+    end
+
+    it 'returns nil if compare base commit is nil' do
+      expect(raw_compare).to receive(:base).and_return(nil)
+
+      expect(subject.start_commit).to eq(nil)
+    end
+  end
+
+  describe '#commit' do
+    it 'returns raw compare head commit' do
+      expect(subject.commit.id).to eq(head_commit.id)
+    end
+
+    it 'returns nil if compare head commit is nil' do
+      expect(raw_compare).to receive(:head).and_return(nil)
+
+      expect(subject.commit).to eq(nil)
+    end
+  end
+
+  describe '#base_commit' do
+    let(:base_commit) { Commit.new(another_sample_commit, project) }
+
+    it 'returns project merge base commit' do
+      expect(project).to receive(:merge_base_commit).with(start_commit.id, head_commit.id).and_return(base_commit)
+
+      expect(subject.base_commit).to eq(base_commit)
+    end
+
+    it 'returns nil if there is no start_commit' do
+      expect(subject).to receive(:start_commit).and_return(nil)
+
+      expect(subject.base_commit).to eq(nil)
+    end
+
+    it 'returns nil if there is no head commit' do
+      expect(subject).to receive(:head_commit).and_return(nil)
+
+      expect(subject.base_commit).to eq(nil)
+    end
+  end
+
+  describe '#diff_refs' do
+    it 'uses base_commit sha as base_sha' do
+      expect(subject).to receive(:base_commit).at_least(:once).and_call_original
+
+      expect(subject.diff_refs.base_sha).to eq(subject.base_commit.id)
+    end
+
+    it 'uses start_commit sha as start_sha' do
+      expect(subject.diff_refs.start_sha).to eq(start_commit.id)
+    end
+
+    it 'uses commit sha as head sha' do
+      expect(subject.diff_refs.head_sha).to eq(head_commit.id)
+    end
+  end
+end
diff --git a/spec/models/concerns/faster_cache_keys_spec.rb b/spec/models/concerns/faster_cache_keys_spec.rb
new file mode 100644
index 0000000000000000000000000000000000000000..8d3f94267fa41c7539777b88db96a428b7e6c8e2
--- /dev/null
+++ b/spec/models/concerns/faster_cache_keys_spec.rb
@@ -0,0 +1,17 @@
+require 'spec_helper'
+
+describe FasterCacheKeys do
+  describe '#cache_key' do
+    it 'returns a String' do
+      # We're using a fixed string here so it's easier to set an expectation for
+      # the resulting cache key.
+      time = '2016-08-08 16:39:00+02'
+      issue = build(:issue, updated_at: time)
+      issue.extend(described_class)
+
+      expect(issue).to receive(:id).and_return(1)
+
+      expect(issue.cache_key).to eq("issues/1-#{time}")
+    end
+  end
+end
diff --git a/spec/models/concerns/milestoneish_spec.rb b/spec/models/concerns/milestoneish_spec.rb
index 7e9ab8940cfa112792d1ec13b320c54f8578dce7..b7e973798a36e77dd00df75ce1d9a5c52cd4d98a 100644
--- a/spec/models/concerns/milestoneish_spec.rb
+++ b/spec/models/concerns/milestoneish_spec.rb
@@ -26,53 +26,53 @@ describe Milestone, 'Milestoneish' do
   end
 
   describe '#closed_items_count' do
-    it 'should not count confidential issues for non project members' do
+    it 'does not count confidential issues for non project members' do
       expect(milestone.closed_items_count(non_member)).to eq 2
     end
 
-    it 'should not count confidential issues for project members with guest role' do
+    it 'does not count confidential issues for project members with guest role' do
       expect(milestone.closed_items_count(guest)).to eq 2
     end
 
-    it 'should count confidential issues for author' do
+    it 'counts confidential issues for author' do
       expect(milestone.closed_items_count(author)).to eq 4
     end
 
-    it 'should count confidential issues for assignee' do
+    it 'counts confidential issues for assignee' do
       expect(milestone.closed_items_count(assignee)).to eq 4
     end
 
-    it 'should count confidential issues for project members' do
+    it 'counts confidential issues for project members' do
       expect(milestone.closed_items_count(member)).to eq 6
     end
 
-    it 'should count all issues for admin' do
+    it 'counts all issues for admin' do
       expect(milestone.closed_items_count(admin)).to eq 6
     end
   end
 
   describe '#total_items_count' do
-    it 'should not count confidential issues for non project members' do
+    it 'does not count confidential issues for non project members' do
       expect(milestone.total_items_count(non_member)).to eq 4
     end
 
-    it 'should not count confidential issues for project members with guest role' do
+    it 'does not count confidential issues for project members with guest role' do
       expect(milestone.total_items_count(guest)).to eq 4
     end
 
-    it 'should count confidential issues for author' do
+    it 'counts confidential issues for author' do
       expect(milestone.total_items_count(author)).to eq 7
     end
 
-    it 'should count confidential issues for assignee' do
+    it 'counts confidential issues for assignee' do
       expect(milestone.total_items_count(assignee)).to eq 7
     end
 
-    it 'should count confidential issues for project members' do
+    it 'counts confidential issues for project members' do
       expect(milestone.total_items_count(member)).to eq 10
     end
 
-    it 'should count all issues for admin' do
+    it 'counts all issues for admin' do
       expect(milestone.total_items_count(admin)).to eq 10
     end
   end
@@ -91,27 +91,27 @@ describe Milestone, 'Milestoneish' do
   end
 
   describe '#percent_complete' do
-    it 'should not count confidential issues for non project members' do
+    it 'does not count confidential issues for non project members' do
       expect(milestone.percent_complete(non_member)).to eq 50
     end
 
-    it 'should not count confidential issues for project members with guest role' do
+    it 'does not count confidential issues for project members with guest role' do
       expect(milestone.percent_complete(guest)).to eq 50
     end
 
-    it 'should count confidential issues for author' do
+    it 'counts confidential issues for author' do
       expect(milestone.percent_complete(author)).to eq 57
     end
 
-    it 'should count confidential issues for assignee' do
+    it 'counts confidential issues for assignee' do
       expect(milestone.percent_complete(assignee)).to eq 57
     end
 
-    it 'should count confidential issues for project members' do
+    it 'counts confidential issues for project members' do
       expect(milestone.percent_complete(member)).to eq 60
     end
 
-    it 'should count confidential issues for admin' do
+    it 'counts confidential issues for admin' do
       expect(milestone.percent_complete(admin)).to eq 60
     end
   end
diff --git a/spec/models/concerns/spammable_spec.rb b/spec/models/concerns/spammable_spec.rb
new file mode 100644
index 0000000000000000000000000000000000000000..32935bc0b09343404f7db11ea92c52a544d60588
--- /dev/null
+++ b/spec/models/concerns/spammable_spec.rb
@@ -0,0 +1,33 @@
+require 'spec_helper'
+
+describe Issue, 'Spammable' do
+  let(:issue) { create(:issue, description: 'Test Desc.') }
+
+  describe 'Associations' do
+    it { is_expected.to have_one(:user_agent_detail).dependent(:destroy) }
+  end
+
+  describe 'ClassMethods' do
+    it 'should return correct attr_spammable' do
+      expect(issue.spammable_text).to eq("#{issue.title}\n#{issue.description}")
+    end
+  end
+
+  describe 'InstanceMethods' do
+    it 'should be invalid if spam' do
+      issue = build(:issue, spam: true)
+      expect(issue.valid?).to be_falsey
+    end
+
+    describe '#check_for_spam?' do
+      it 'returns true for public project' do
+        issue.project.update_attribute(:visibility_level, Gitlab::VisibilityLevel::PUBLIC)
+        expect(issue.check_for_spam?).to eq(true)
+      end
+
+      it 'returns false for other visibility levels' do
+        expect(issue.check_for_spam?).to eq(false)
+      end
+    end
+  end
+end
diff --git a/spec/models/concerns/token_authenticatable_spec.rb b/spec/models/concerns/token_authenticatable_spec.rb
index 9e8ebc56a316f14bedc409a4c8fdf098b9ea591d..eb64f3d0c83291796004dcf70468e546bd291bed 100644
--- a/spec/models/concerns/token_authenticatable_spec.rb
+++ b/spec/models/concerns/token_authenticatable_spec.rb
@@ -41,7 +41,7 @@ describe ApplicationSetting, 'TokenAuthenticatable' do
       describe 'ensured! token' do
         subject { described_class.new.send("ensure_#{token_field}!") }
 
-        it 'should persist new token' do
+        it 'persists new token' do
           expect(subject).to eq described_class.current[token_field]
         end
       end
diff --git a/spec/models/deployment_spec.rb b/spec/models/deployment_spec.rb
index 7df3df4bb9e652016122d0e6ed99b605a15ca28e..bfff639ad78c55961c1707caec1649a48768b0f0 100644
--- a/spec/models/deployment_spec.rb
+++ b/spec/models/deployment_spec.rb
@@ -15,4 +15,28 @@ describe Deployment, models: true do
 
   it { is_expected.to validate_presence_of(:ref) }
   it { is_expected.to validate_presence_of(:sha) }
+
+  describe '#includes_commit?' do
+    let(:project)     { create(:project) }
+    let(:environment) { create(:environment, project: project) }
+    let(:deployment) do
+      create(:deployment, environment: environment, sha: project.commit.id)
+    end
+
+    context 'when there is no project commit' do
+      it 'returns false' do
+        commit = project.commit('feature')
+
+        expect(deployment.includes_commit?(commit)).to be false
+      end
+    end
+
+    context 'when they share the same tree branch' do
+      it 'returns true' do
+        commit = project.commit
+
+        expect(deployment.includes_commit?(commit)).to be true
+      end
+    end
+  end
 end
diff --git a/spec/models/diff_note_spec.rb b/spec/models/diff_note_spec.rb
index af8e890ca951b2e39b450dd3e4d2391877e136d0..6a640474cfe04f6b057af2f4c91e514b359db77e 100644
--- a/spec/models/diff_note_spec.rb
+++ b/spec/models/diff_note_spec.rb
@@ -103,7 +103,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
@@ -119,7 +119,7 @@ describe DiffNote, models: true do
 
       context "when the merge request's diff refs don't match that of the diff note" do
         before do
-          allow(subject.noteable).to receive(:diff_refs).and_return(commit.diff_refs)
+          allow(subject.noteable).to receive(:diff_sha_refs).and_return(commit.diff_refs)
         end
 
         it "returns false" do
@@ -168,7 +168,7 @@ describe DiffNote, models: true do
 
         context "when the note is outdated" do
           before do
-            allow(merge_request).to receive(:diff_refs).and_return(commit.diff_refs)
+            allow(merge_request).to receive(:diff_sha_refs).and_return(commit.diff_refs)
           end
 
           it "uses the DiffPositionUpdateService" do
@@ -188,4 +188,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..179f2e7366247a1b684035e0ff9045f730d3f6b8
--- /dev/null
+++ b/spec/models/discussion_spec.rb
@@ -0,0 +1,615 @@
+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) }
+
+      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 "calls resolve! on every resolvable note" do
+          expect(first_note).to receive(:resolve!).with(current_user)
+          expect(second_note).not_to receive(:resolve!)
+          expect(third_note).to receive(:resolve!).with(current_user)
+
+          subject.resolve!(current_user)
+        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 "calls resolve! on every resolvable note" do
+          expect(first_note).to receive(:resolve!).with(current_user)
+          expect(second_note).not_to receive(:resolve!)
+          expect(third_note).to receive(:resolve!).with(current_user)
+
+          subject.resolve!(current_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.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.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.resolved? }
+        end
+
+        it "sets resolved_at on the unresolved note" do
+          subject.resolve!(current_user)
+
+          expect(third_note.resolved_at).not_to be_nil
+        end
+
+        it "sets resolved_by on the unresolved note" do
+          subject.resolve!(current_user)
+
+          expect(third_note.resolved_by).to eq(current_user)
+        end
+
+        it "marks the unresolved note as resolved" do
+          subject.resolve!(current_user)
+
+          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 "calls resolve! on every resolvable note" do
+          expect(first_note).to receive(:resolve!).with(current_user)
+          expect(second_note).not_to receive(:resolve!)
+          expect(third_note).to receive(:resolve!).with(current_user)
+
+          subject.resolve!(current_user)
+        end
+
+        it "sets resolved_at on the unresolved notes" do
+          subject.resolve!(current_user)
+
+          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)
+
+          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)
+
+          expect(first_note.resolved?).to be true
+          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
+    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 "calls unresolve! on every resolvable note" do
+          expect(first_note).to receive(:unresolve!)
+          expect(second_note).not_to receive(:unresolve!)
+          expect(third_note).to receive(:unresolve!)
+
+          subject.unresolve!
+        end
+
+        it "unsets resolved_at on the resolved notes" do
+          subject.unresolve!
+
+          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!
+
+          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!
+
+          expect(first_note.resolved?).to be false
+          expect(third_note.resolved?).to be false
+        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 some resolvable notes are resolved" do
+        before do
+          first_note.resolve!(user)
+        end
+
+        it "calls unresolve! on every resolvable note" do
+          expect(first_note).to receive(:unresolve!)
+          expect(second_note).not_to receive(:unresolve!)
+          expect(third_note).to receive(:unresolve!)
+
+          subject.unresolve!
+        end
+
+        it "unsets resolved_at on the resolved note" do
+          subject.unresolve!
+
+          expect(first_note.resolved_at).to be_nil
+        end
+
+        it "unsets resolved_by on the resolved note" do
+          subject.unresolve!
+
+          expect(first_note.resolved_by).to be_nil
+        end
+
+        it "unmarks the resolved note as resolved" do
+          subject.unresolve!
+
+          expect(first_note.resolved?).to be false
+        end
+      end
+
+      context "when no resolvable notes are resolved" do
+        it "calls unresolve! on every resolvable note" do
+          expect(first_note).to receive(:unresolve!)
+          expect(second_note).not_to receive(:unresolve!)
+          expect(third_note).to receive(:unresolve!)
+
+          subject.unresolve!
+        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/environment_spec.rb b/spec/models/environment_spec.rb
index 8a84ac0a7c7cb601cc695a2d8b55a97b8ca75ddb..c881897926e36b4172dc977fa8dc3fec10f97c71 100644
--- a/spec/models/environment_spec.rb
+++ b/spec/models/environment_spec.rb
@@ -30,4 +30,37 @@ describe Environment, models: true do
       expect(env.external_url).to be_nil
     end
   end
+
+  describe '#includes_commit?' do
+    context 'without a last deployment' do
+      it "returns false" do
+        expect(environment.includes_commit?('HEAD')).to be false
+      end
+    end
+
+    context 'with a last deployment' do
+      let(:project)     { create(:project) }
+      let(:environment) { create(:environment, project: project) }
+
+      let!(:deployment) do
+        create(:deployment, environment: environment, sha: project.commit('master').id)
+      end
+
+      context 'in the same branch' do
+        it 'returns true' do
+          expect(environment.includes_commit?(RepoHelpers.sample_commit)).to be true
+        end
+      end
+
+      context 'not in the same branch' do
+        before do
+          deployment.update(sha: project.commit('feature').id)
+        end
+
+        it 'returns false' do
+          expect(environment.includes_commit?(RepoHelpers.sample_commit)).to be false
+        end
+      end
+    end
+  end
 end
diff --git a/spec/models/forked_project_link_spec.rb b/spec/models/forked_project_link_spec.rb
index f94987dcaff81ad42f78fb7878d3bf16f36dfbb8..9c81d159cdf6da4684bad60b17ab1016dc5e73e9 100644
--- a/spec/models/forked_project_link_spec.rb
+++ b/spec/models/forked_project_link_spec.rb
@@ -9,11 +9,11 @@ describe ForkedProjectLink, "add link on fork" do
     @project_to = fork_project(project_from, user)
   end
 
-  it "project_to should know it is forked" do
+  it "project_to knows it is forked" do
     expect(@project_to.forked?).to be_truthy
   end
 
-  it "project should know who it is forked from" do
+  it "project knows who it is forked from" do
     expect(@project_to.forked_from_project).to eq(project_from)
   end
 end
@@ -29,15 +29,15 @@ describe '#forked?' do
     forked_project_link.save!
   end
 
-  it "project_to should know it is forked" do
+  it "project_to knows it is forked" do
     expect(project_to.forked?).to be_truthy
   end
 
-  it "project_from should not be forked" do
+  it "project_from is not forked" do
     expect(project_from.forked?).to be_falsey
   end
 
-  it "project_to.destroy should destroy fork_link" do
+  it "project_to.destroy destroys fork_link" do
     expect(forked_project_link).to receive(:destroy)
     project_to.destroy
   end
diff --git a/spec/models/global_milestone_spec.rb b/spec/models/global_milestone_spec.rb
index ae77ec5b3489e2064c93940559a24142a2197321..92e0f7f27cecc1939d45b375499b1bb6e5277d53 100644
--- a/spec/models/global_milestone_spec.rb
+++ b/spec/models/global_milestone_spec.rb
@@ -29,15 +29,15 @@ describe GlobalMilestone, models: true do
       @global_milestones = GlobalMilestone.build_collection(milestones)
     end
 
-    it 'should have all project milestones' do
+    it 'has all project milestones' do
       expect(@global_milestones.count).to eq(2)
     end
 
-    it 'should have all project milestones titles' do
+    it 'has all project milestones titles' do
       expect(@global_milestones.map(&:title)).to match_array(['Milestone v1.2', 'VD-123'])
     end
 
-    it 'should have all project milestones' do
+    it 'has all project milestones' do
       expect(@global_milestones.map { |group_milestone| group_milestone.milestones.count }.sum).to eq(6)
     end
   end
@@ -54,11 +54,11 @@ describe GlobalMilestone, models: true do
       @global_milestone = GlobalMilestone.new(milestone1_project1.title, milestones)
     end
 
-    it 'should have exactly one group milestone' do
+    it 'has exactly one group milestone' do
       expect(@global_milestone.title).to eq('Milestone v1.2')
     end
 
-    it 'should have all project milestones with the same title' do
+    it 'has all project milestones with the same title' do
       expect(@global_milestone.milestones.count).to eq(3)
     end
   end
@@ -66,7 +66,7 @@ describe GlobalMilestone, models: true do
   describe '#safe_title' do
     let(:milestone) { create(:milestone, title: "git / test", project: project1) }
 
-    it 'should strip out slashes and spaces' do
+    it 'strips out slashes and spaces' do
       global_milestone = GlobalMilestone.new(milestone.title, [milestone])
 
       expect(global_milestone.safe_title).to eq('git-test')
diff --git a/spec/models/group_spec.rb b/spec/models/group_spec.rb
index 266c46213a695645246134f271861e4226571317..ea4b59c26b1e6d8274a3669b3f868e59d13e8533 100644
--- a/spec/models/group_spec.rb
+++ b/spec/models/group_spec.rb
@@ -116,7 +116,7 @@ describe Group, models: true do
     let(:user) { create(:user) }
     before { group.add_users([user.id], GroupMember::GUEST) }
 
-    it "should update the group permission" do
+    it "updates the group permission" do
       expect(group.group_members.guests.map(&:user)).to include(user)
       group.add_users([user.id], GroupMember::DEVELOPER)
       expect(group.group_members.developers.map(&:user)).to include(user)
@@ -128,12 +128,12 @@ describe Group, models: true do
     let(:user) { create(:user) }
     before { group.add_user(user, GroupMember::MASTER) }
 
-    it "should be true if avatar is image" do
+    it "is true if avatar is image" do
       group.update_attribute(:avatar, 'uploads/avatar.png')
       expect(group.avatar_type).to be_truthy
     end
 
-    it "should be false if avatar is html page" do
+    it "is false if avatar is html page" do
       group.update_attribute(:avatar, 'uploads/avatar.html')
       expect(group.avatar_type).to eq(["only images allowed"])
     end
diff --git a/spec/models/hooks/project_hook_spec.rb b/spec/models/hooks/project_hook_spec.rb
index 983848392b7eb52048649b89e7464ffd19471540..4a457997a4fa9dd8997bc8ed3049e1a0ffb2ed21 100644
--- a/spec/models/hooks/project_hook_spec.rb
+++ b/spec/models/hooks/project_hook_spec.rb
@@ -24,7 +24,7 @@ describe ProjectHook, models: true do
   end
 
   describe '.push_hooks' do
-    it 'should return hooks for push events only' do
+    it 'returns hooks for push events only' do
       hook = create(:project_hook, push_events: true)
       create(:project_hook, push_events: false)
       expect(ProjectHook.push_hooks).to eq([hook])
@@ -32,7 +32,7 @@ describe ProjectHook, models: true do
   end
 
   describe '.tag_push_hooks' do
-    it 'should return hooks for tag push events only' do
+    it 'returns hooks for tag push events only' do
       hook = create(:project_hook, tag_push_events: true)
       create(:project_hook, tag_push_events: false)
       expect(ProjectHook.tag_push_hooks).to eq([hook])
diff --git a/spec/models/hooks/system_hook_spec.rb b/spec/models/hooks/system_hook_spec.rb
index 4078b9e4ff54a57f07ce000b4127cbc07edac238..cbdf7eec082d4832c99691babb7cd96396f377eb 100644
--- a/spec/models/hooks/system_hook_spec.rb
+++ b/spec/models/hooks/system_hook_spec.rb
@@ -38,7 +38,7 @@ describe SystemHook, models: true do
     end
 
     it "project_destroy hook" do
-      Projects::DestroyService.new(project, user, {}).pending_delete!
+      Projects::DestroyService.new(project, user, {}).async_execute
 
       expect(WebMock).to have_requested(:post, system_hook.url).with(
         body: /project_destroy/,
diff --git a/spec/models/key_spec.rb b/spec/models/key_spec.rb
index 49cf3d8633ad9ea4464311b83818ee2beac5dc3e..fd4a2beff586f191d4f8f6ab2155efd8e502e239 100644
--- a/spec/models/key_spec.rb
+++ b/spec/models/key_spec.rb
@@ -16,12 +16,13 @@ describe Key, models: true do
   end
 
   describe "Methods" do
+    let(:user) { create(:user) }
     it { is_expected.to respond_to :projects }
     it { is_expected.to respond_to :publishable_key }
 
     describe "#publishable_keys" do
-      it 'strips all personal information' do
-        expect(build(:key).publishable_key).not_to match(/dummy@gitlab/)
+      it 'replaces SSH key comment with simple identifier of username + hostname' do
+        expect(build(:key, user: user).publishable_key).to include("#{user.name} (localhost)")
       end
     end
   end
@@ -72,13 +73,13 @@ describe Key, models: true do
   end
 
   context 'callbacks' do
-    it 'should add new key to authorized_file' do
+    it 'adds new key to authorized_file' do
       @key = build(:personal_key, id: 7)
       expect(GitlabShellWorker).to receive(:perform_async).with(:add_key, @key.shell_id, @key.key)
       @key.save
     end
 
-    it 'should remove key from authorized_file' do
+    it 'removes key from authorized_file' do
       @key = create(:personal_key)
       expect(GitlabShellWorker).to receive(:perform_async).with(:remove_key, @key.shell_id, @key.key)
       @key.destroy
diff --git a/spec/models/label_spec.rb b/spec/models/label_spec.rb
index f37f44a608e53c3aac5fa28a32a2e661f912afd5..5a5d1a5d60c23183eb2948a03fdd5f913a635726 100644
--- a/spec/models/label_spec.rb
+++ b/spec/models/label_spec.rb
@@ -5,8 +5,10 @@ describe Label, models: true do
 
   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) }
+    it { is_expected.to have_many(:lists).dependent(:destroy) }
   end
 
   describe 'modules' do
@@ -18,7 +20,7 @@ describe Label, models: true do
   describe 'validation' do
     it { is_expected.to validate_presence_of(:project) }
 
-    it 'should validate color code' do
+    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)
@@ -30,7 +32,7 @@ describe Label, models: true do
       expect(label).to allow_value('#abcdef').for(:color)
     end
 
-    it 'should validate title' do
+    it 'validates title' do
       expect(label).not_to allow_value('G,ITLAB').for(:title)
       expect(label).not_to allow_value('').for(:title)
 
diff --git a/spec/models/legacy_diff_note_spec.rb b/spec/models/legacy_diff_note_spec.rb
index d23fc06c3adf163f54e5978a1d5760a81a40b7e3..81517a18b748604cfaefcaca70ac6c5b9a226cab 100644
--- a/spec/models/legacy_diff_note_spec.rb
+++ b/spec/models/legacy_diff_note_spec.rb
@@ -5,12 +5,12 @@ describe LegacyDiffNote, models: true do
     let!(:note) { create(:legacy_diff_note_on_commit, note: "+1 from me") }
     let!(:commit) { note.noteable }
 
-    it "should save a valid note" do
+    it "saves a valid note" do
       expect(note.commit_id).to eq(commit.id)
       expect(note.noteable.id).to eq(commit.id)
     end
 
-    it "should be recognized by #legacy_diff_note?" do
+    it "is recognized by #legacy_diff_note?" do
       expect(note).to be_legacy_diff_note
     end
   end
@@ -58,7 +58,7 @@ describe LegacyDiffNote, models: true do
 
         # Generate a real line_code value so we know it will match. We use a
         # random line from a random diff just for funsies.
-        diff = merge.diffs.to_a.sample
+        diff = merge.raw_diffs.to_a.sample
         line = Gitlab::Diff::Parser.new.parse(diff.diff.each_line).to_a.sample
         code = Gitlab::Diff::LineCode.generate(diff.new_path, line.new_pos, line.old_pos)
 
@@ -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 44cd3c08718fd577d4d4ba0bb9589be42f912743..fef90d9b5cb8368c1d31a04c448cc8ddc0b442fe 100644
--- a/spec/models/member_spec.rb
+++ b/spec/models/member_spec.rb
@@ -10,7 +10,7 @@ describe Member, models: true do
 
     it { is_expected.to validate_presence_of(:user) }
     it { is_expected.to validate_presence_of(:source) }
-    it { is_expected.to validate_inclusion_of(:access_level).in_array(Gitlab::Access.values) }
+    it { is_expected.to validate_inclusion_of(:access_level).in_array(Gitlab::Access.all_values) }
 
     it_behaves_like 'an object with email-formated attributes', :invite_email do
       subject { build(:project_member) }
@@ -65,11 +65,21 @@ 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)
+      Member.add_user(
+        project.members,
+        'toto1@example.com',
+        Gitlab::Access::DEVELOPER,
+        current_user: @master_user
+      )
       @invited_member = project.members.invite.find_by_invite_email('toto1@example.com')
 
       accepted_invite_user = build(:user)
-      ProjectMember.add_user(project.members, 'toto2@example.com', Gitlab::Access::DEVELOPER, @master_user)
+      Member.add_user(
+        project.members,
+        'toto2@example.com',
+        Gitlab::Access::DEVELOPER,
+        current_user: @master_user
+      )
       @accepted_invite_member = project.members.invite.find_by_invite_email('toto2@example.com').tap { |u| u.accept_invite!(accepted_invite_user) }
 
       requested_user = create(:user).tap { |u| project.request_access(u) }
diff --git a/spec/models/members/group_member_spec.rb b/spec/models/members/group_member_spec.rb
index 18439cac2a4cb62a5d3397ab41a19c60ae5b1168..4f875fd257a5fa18e155e082ca8dcacc5686f07a 100644
--- a/spec/models/members/group_member_spec.rb
+++ b/spec/models/members/group_member_spec.rb
@@ -22,7 +22,7 @@ require 'spec_helper'
 describe GroupMember, models: true do
   describe 'notifications' do
     describe "#after_create" do
-      it "should send email to user" do
+      it "sends email to user" do
         membership = build(:group_member)
 
         allow(membership).to receive(:notification_service).
@@ -40,7 +40,7 @@ describe GroupMember, models: true do
           and_return(double('NotificationService').as_null_object)
       end
 
-      it "should send email to user" do
+      it "sends email to user" do
         expect(@group_member).to receive(:notification_service)
         @group_member.update_attribute(:access_level, GroupMember::MASTER)
       end
diff --git a/spec/models/members/project_member_spec.rb b/spec/models/members/project_member_spec.rb
index ba622dfb9becdc4309d22872e8b851333a36bd8e..913d74645a7fd8ac04eb78a2358c0fa61e24c073 100644
--- a/spec/models/members/project_member_spec.rb
+++ b/spec/models/members/project_member_spec.rb
@@ -27,6 +27,7 @@ describe ProjectMember, models: true do
   describe 'validations' do
     it { is_expected.to allow_value('Project').for(:source_type) }
     it { is_expected.not_to allow_value('project').for(:source_type) }
+    it { is_expected.to validate_inclusion_of(:access_level).in_array(Gitlab::Access.values) }
   end
 
   describe 'modules' do
@@ -40,7 +41,7 @@ describe ProjectMember, models: true do
   end
 
   describe "#destroy" do
-    let(:owner)   { create(:project_member, access_level: ProjectMember::OWNER) }
+    let(:owner)   { create(:project_member, access_level: ProjectMember::MASTER) }
     let(:project) { owner.project }
     let(:master)  { create(:project_member, project: project) }
 
@@ -52,7 +53,7 @@ describe ProjectMember, models: true do
       master_todos
     end
 
-    it "destroy itself and delete associated todos" do
+    it "destroys itself and delete associated todos" do
       expect(owner.user.todos.size).to eq(2)
       expect(master.user.todos.size).to eq(3)
       expect(Todo.count).to eq(5)
@@ -101,7 +102,7 @@ describe ProjectMember, models: true do
     end
   end
 
-  describe '.add_users_into_projects' do
+  describe '.add_users_to_projects' do
     before do
       @project_1 = create :project
       @project_2 = create :project
@@ -109,7 +110,7 @@ describe ProjectMember, models: true do
       @user_1 = create :user
       @user_2 = create :user
 
-      ProjectMember.add_users_into_projects(
+      ProjectMember.add_users_to_projects(
         [@project_1.id, @project_2.id],
         [@user_1.id, @user_2.id],
         ProjectMember::MASTER
diff --git a/spec/models/merge_request_diff_spec.rb b/spec/models/merge_request_diff_spec.rb
index 9a637c94fbe11599322034bb2121cb0e029d3908..29f7396f862822fcbaf7647353e8be12aacc9e5b 100644
--- a/spec/models/merge_request_diff_spec.rb
+++ b/spec/models/merge_request_diff_spec.rb
@@ -10,7 +10,7 @@ describe MergeRequestDiff, models: true do
         expect(mr_diff).not_to receive(:load_diffs)
         expect(Gitlab::Git::Compare).to receive(:new).and_call_original
 
-        mr_diff.diffs(ignore_whitespace_change: true)
+        mr_diff.raw_diffs(ignore_whitespace_change: true)
       end
     end
 
@@ -18,19 +18,19 @@ describe MergeRequestDiff, models: true do
       before { mr_diff.update_attributes(st_diffs: '') }
 
       it 'returns an empty DiffCollection' do
-        expect(mr_diff.diffs).to be_a(Gitlab::Git::DiffCollection)
-        expect(mr_diff.diffs).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.diffs).to be_a(Gitlab::Git::DiffCollection)
-        expect(mr_diff.diffs).not_to be_empty
+        expect(mr_diff.raw_diffs).to be_a(Gitlab::Git::DiffCollection)
+        expect(mr_diff.raw_diffs).not_to be_empty
       end
 
       context 'when the :paths option is set' do
-        let(:diffs) { mr_diff.diffs(paths: ['files/ruby/popen.rb', 'files/ruby/popen.rb']) }
+        let(:diffs) { mr_diff.raw_diffs(paths: ['files/ruby/popen.rb', 'files/ruby/popen.rb']) }
 
         it 'only returns diffs that match the (old path, new path) given' do
           expect(diffs.map(&:new_path)).to contain_exactly('files/ruby/popen.rb')
diff --git a/spec/models/merge_request_spec.rb b/spec/models/merge_request_spec.rb
index a0e3c26e542a068d7a176804a5104986b9468baa..64c56d922fff11468bffa82acbd06920041b107e 100644
--- a/spec/models/merge_request_spec.rb
+++ b/spec/models/merge_request_spec.rb
@@ -128,6 +128,31 @@ describe MergeRequest, models: true do
     end
   end
 
+  describe '#raw_diffs' do
+    let(:merge_request) { build(:merge_request) }
+    let(:options) { { paths: ['a/b', 'b/a', 'c/*'] } }
+
+    context 'when there are MR diffs' do
+      it 'delegates to the MR diffs' do
+        merge_request.merge_request_diff = MergeRequestDiff.new
+
+        expect(merge_request.merge_request_diff).to receive(:raw_diffs).with(options)
+
+        merge_request.raw_diffs(options)
+      end
+    end
+
+    context 'when there are no MR diffs' do
+      it 'delegates to the compare object' do
+        merge_request.compare = double(:compare)
+
+        expect(merge_request.compare).to receive(:raw_diffs).with(options)
+
+        merge_request.raw_diffs(options)
+      end
+    end
+  end
+
   describe '#diffs' do
     let(:merge_request) { build(:merge_request) }
     let(:options) { { paths: ['a/b', 'b/a', 'c/*'] } }
@@ -136,7 +161,7 @@ describe MergeRequest, models: true do
       it 'delegates to the MR diffs' do
         merge_request.merge_request_diff = MergeRequestDiff.new
 
-        expect(merge_request.merge_request_diff).to receive(:diffs).with(options)
+        expect(merge_request.merge_request_diff).to receive(:raw_diffs).with(hash_including(options))
 
         merge_request.diffs(options)
       end
@@ -163,12 +188,12 @@ describe MergeRequest, models: true do
       create(:note, noteable: merge_request, project: merge_request.project)
     end
 
-    it "should include notes for commits" do
+    it "includes notes for commits" do
       expect(merge_request.commits).not_to be_empty
       expect(merge_request.mr_and_commit_notes.count).to eq(2)
     end
 
-    it "should include notes for commits from target project as well" do
+    it "includes notes for commits from target project as well" do
       create(:note_on_commit, commit_id: merge_request.commits.first.id,
                               project: merge_request.target_project)
 
@@ -279,7 +304,7 @@ describe MergeRequest, models: true do
       expect(subject.can_remove_source_branch?(user)).to be_falsey
     end
 
-    it "cant remove a root ref" do
+    it "can't remove a root ref" do
       subject.source_branch = "master"
       subject.target_branch = "feature"
 
@@ -431,6 +456,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
@@ -455,6 +494,19 @@ describe MergeRequest, models: true do
     end
   end
 
+  describe '#all_pipelines' do
+    let!(:pipelines) do
+      subject.merge_request_diff.commits.map do |commit|
+        create(:ci_empty_pipeline, project: subject.source_project, sha: commit.id, ref: subject.source_branch)
+      end
+    end
+
+    it 'returns a pipelines from source projects with proper ordering' do
+      expect(subject.all_pipelines).not_to be_empty
+      expect(subject.all_pipelines).to eq(pipelines.reverse)
+    end
+  end
+
   describe '#participants' do
     let(:project) { create(:project, :public) }
 
@@ -649,6 +701,21 @@ describe MergeRequest, models: true do
     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)
+
+      expect(merge_request.environments).to eq [environment]
+    end
+  end
+
   describe "#reload_diff" do
     let(:note) { create(:diff_note_on_merge_request, project: subject.project, noteable: subject) }
 
@@ -660,6 +727,12 @@ describe MergeRequest, models: true do
       subject.reload_diff
     end
 
+    it "executs diff cache service" do
+      expect_any_instance_of(MergeRequests::MergeRequestDiffCacheService).to receive(:execute).with(subject)
+
+      subject.reload_diff
+    end
+
     it "updates diff note positions" do
       old_diff_refs = subject.diff_refs
 
@@ -686,4 +759,172 @@ describe MergeRequest, models: true do
       subject.reload_diff
     end
   end
+
+  describe "#diff_sha_refs" do
+    context "with diffs" do
+      subject { create(:merge_request, :with_diffs) }
+
+      it "does not touch the repository" do
+        subject # Instantiate the object
+
+        expect_any_instance_of(Repository).not_to receive(:commit)
+
+        subject.diff_sha_refs
+      end
+
+      it "returns expected diff_refs" do
+        expected_diff_refs = Gitlab::Diff::DiffRefs.new(
+          base_sha:  subject.merge_request_diff.base_commit_sha,
+          start_sha: subject.merge_request_diff.start_commit_sha,
+          head_sha:  subject.merge_request_diff.head_commit_sha
+        )
+
+        expect(subject.diff_sha_refs).to eq(expected_diff_refs)
+      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 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 with ambiguous conflict markers' do
+      merge_request = create_merge_request('conflict-contains-conflict-markers')
+
+      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
+  end
 end
diff --git a/spec/models/milestone_spec.rb b/spec/models/milestone_spec.rb
index d661dc0e59ab1067486b1f31c3a37f1fe45b1da8..d64d6cde2b517bc9b0547c8ca35ce2bbd72998ca 100644
--- a/spec/models/milestone_spec.rb
+++ b/spec/models/milestone_spec.rb
@@ -28,12 +28,12 @@ describe Milestone, models: true do
   end
 
   describe "unique milestone title per project" do
-    it "shouldn't accept the same title in a project twice" do
+    it "does not accept the same title in a project twice" do
       new_milestone = Milestone.new(project: milestone.project, title: milestone.title)
       expect(new_milestone).not_to be_valid
     end
 
-    it "should accept the same title in another project" do
+    it "accepts the same title in another project" do
       project = build(:project)
       new_milestone = Milestone.new(project: project, title: milestone.title)
 
@@ -42,29 +42,29 @@ describe Milestone, models: true do
   end
 
   describe "#percent_complete" do
-    it "should not count open issues" do
+    it "does not count open issues" do
       milestone.issues << issue
       expect(milestone.percent_complete(user)).to eq(0)
     end
 
-    it "should count closed issues" do
+    it "counts closed issues" do
       issue.close
       milestone.issues << issue
       expect(milestone.percent_complete(user)).to eq(100)
     end
 
-    it "should recover from dividing by zero" do
+    it "recovers from dividing by zero" do
       expect(milestone.percent_complete(user)).to eq(0)
     end
   end
 
   describe "#expires_at" do
-    it "should be nil when due_date is unset" do
+    it "is nil when due_date is unset" do
       milestone.update_attributes(due_date: nil)
       expect(milestone.expires_at).to be_nil
     end
 
-    it "should not be nil when due_date is set" do
+    it "is not nil when due_date is set" do
       milestone.update_attributes(due_date: Date.tomorrow)
       expect(milestone.expires_at).to be_present
     end
@@ -121,7 +121,7 @@ describe Milestone, models: true do
       create :merge_request, milestone: milestone
     end
 
-    it 'Should return total count of issues and merge requests assigned to milestone' do
+    it 'returns total count of issues and merge requests assigned to milestone' do
       expect(milestone.total_items_count(user)).to eq 2
     end
   end
@@ -134,11 +134,11 @@ describe Milestone, models: true do
       create :issue
     end
 
-    it 'should be true if milestone active and all nested issues closed' do
+    it 'returns true if milestone active and all nested issues closed' do
       expect(milestone.can_be_closed?).to be_truthy
     end
 
-    it 'should be false if milestone active and not all nested issues closed' do
+    it 'returns false if milestone active and not all nested issues closed' do
       issue.milestone = milestone
       issue.save
 
diff --git a/spec/models/namespace_spec.rb b/spec/models/namespace_spec.rb
index a162da0208e96a10ce1627c933875d0d4e0ce028..544920d18240b4b902f213b1fc3c5346f130eb6a 100644
--- a/spec/models/namespace_spec.rb
+++ b/spec/models/namespace_spec.rb
@@ -61,11 +61,11 @@ describe Namespace, models: true do
       allow(@namespace).to receive(:path_changed?).and_return(true)
     end
 
-    it "should raise error when directory exists" do
+    it "raises error when directory exists" do
       expect { @namespace.move_dir }.to raise_error("namespace directory cannot be moved")
     end
 
-    it "should move dir if path changed" do
+    it "moves dir if path changed" do
       new_path = @namespace.path + "_new"
       allow(@namespace).to receive(:path_was).and_return(@namespace.path)
       allow(@namespace).to receive(:path).and_return(new_path)
@@ -93,7 +93,7 @@ describe Namespace, models: true do
 
     before { namespace.destroy }
 
-    it "should remove its dirs when deleted" do
+    it "removes its dirs when deleted" do
       expect(File.exist?(path)).to be(false)
     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 1243f5420a74a588381c338249326d153de1711f..ef2747046b96131c35e9f8fd9d402f5417cd70b0 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) }
@@ -56,18 +58,18 @@ describe Note, models: true do
     let!(:note) { create(:note_on_commit, note: "+1 from me") }
     let!(:commit) { note.noteable }
 
-    it "should be accessible through #noteable" do
+    it "is accessible through #noteable" do
       expect(note.commit_id).to eq(commit.id)
       expect(note.noteable).to be_a(Commit)
       expect(note.noteable).to eq(commit)
     end
 
-    it "should save a valid note" do
+    it "saves a valid note" do
       expect(note.commit_id).to eq(commit.id)
       note.noteable == commit
     end
 
-    it "should be recognized by #for_commit?" do
+    it "is recognized by #for_commit?" do
       expect(note).to be_for_commit
     end
 
@@ -267,4 +269,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_security_spec.rb b/spec/models/project_security_spec.rb
index 2142c7c13ef9fa5587f324eb33100ed862bf9a83..36379074ea0b522cd5d794972b730ae53c59b771 100644
--- a/spec/models/project_security_spec.rb
+++ b/spec/models/project_security_spec.rb
@@ -21,7 +21,7 @@ describe Project, models: true do
     let(:owner_actions) { Ability.project_owner_rules }
 
     describe "Non member rules" do
-      it "should deny for non-project users any actions" 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
@@ -33,7 +33,7 @@ describe Project, models: true do
         @p1.project_members.create(project: @p1, user: @u2, access_level: ProjectMember::GUEST)
       end
 
-      it "should allow for project user any guest actions" do
+      it "allows for project user any guest actions" do
         guest_actions.each do |action|
           expect(@abilities.allowed?(@u2, action, @p1)).to be_truthy
         end
@@ -45,7 +45,7 @@ describe Project, models: true do
         @p1.project_members.create(project: @p1, user: @u2, access_level: ProjectMember::REPORTER)
       end
 
-      it "should allow for project user any report actions" do
+      it "allows for project user any report actions" do
         report_actions.each do |action|
           expect(@abilities.allowed?(@u2, action, @p1)).to be_truthy
         end
@@ -58,13 +58,13 @@ describe Project, models: true do
         @p1.project_members.create(project: @p1, user: @u3, access_level: ProjectMember::DEVELOPER)
       end
 
-      it "should deny for developer master-specific actions" do
+      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 "should allow for project user any dev actions" do
+      it "allows for project user any dev actions" do
         dev_actions.each do |action|
           expect(@abilities.allowed?(@u3, action, @p1)).to be_truthy
         end
@@ -77,13 +77,13 @@ describe Project, models: true do
         @p1.project_members.create(project: @p1, user: @u3, access_level: ProjectMember::MASTER)
       end
 
-      it "should deny for developer master-specific actions" do
+      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 "should allow for project user any master actions" do
+      it "allows for project user any master actions" do
         master_actions.each do |action|
           expect(@abilities.allowed?(@u3, action, @p1)).to be_truthy
         end
@@ -96,13 +96,13 @@ describe Project, models: true do
         @p1.project_members.create(project: @p1, user: @u3, access_level: ProjectMember::MASTER)
       end
 
-      it "should deny for masters admin-specific actions" do
+      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 "should allow for project owner any admin actions" do
+      it "allows for project owner any admin actions" do
         owner_actions.each do |action|
           expect(@abilities.allowed?(@u4, action, @p1)).to be_truthy
         end
diff --git a/spec/models/project_services/asana_service_spec.rb b/spec/models/project_services/asana_service_spec.rb
index f3d15f3c1ea7095a9e0648c0a49512d7c3b490b8..dc702cfc42c5c88619937ba4d767c654ee3c9593 100644
--- a/spec/models/project_services/asana_service_spec.rb
+++ b/spec/models/project_services/asana_service_spec.rb
@@ -65,7 +65,7 @@ describe AsanaService, models: true do
       )
     end
 
-    it 'should call Asana service to create a story' do
+    it 'calls Asana service to create a story' do
       data = create_data_for_commits('Message from commit. related to #123456')
       expected_message = "#{data[:user_name]} pushed to branch #{data[:ref]} of #{project.name_with_namespace} ( #{data[:commits][0][:url]} ): #{data[:commits][0][:message]}"
 
@@ -76,7 +76,7 @@ describe AsanaService, models: true do
       @asana.execute(data)
     end
 
-    it 'should call Asana service to create a story and close a task' do
+    it 'calls Asana service to create a story and close a task' do
       data = create_data_for_commits('fix #456789')
       d1 = double('Asana::Task')
       expect(d1).to receive(:add_comment)
@@ -86,7 +86,7 @@ describe AsanaService, models: true do
       @asana.execute(data)
     end
 
-    it 'should be able to close via url' do
+    it 'is able to close via url' do
       data = create_data_for_commits('closes https://app.asana.com/19292/956299/42')
       d1 = double('Asana::Task')
       expect(d1).to receive(:add_comment)
@@ -96,7 +96,7 @@ describe AsanaService, models: true do
       @asana.execute(data)
     end
 
-    it 'should allow multiple matches per line' do
+    it 'allows multiple matches per line' do
       message = <<-EOF
       minor bigfix, refactoring, fixed #123 and Closes #456 work on #789
       ref https://app.asana.com/19292/956299/42 and closing https://app.asana.com/19292/956299/12
diff --git a/spec/models/project_services/assembla_service_spec.rb b/spec/models/project_services/assembla_service_spec.rb
index 17e9361dd5cd00d854c9db517e9823fe5a908d7f..d672d80156c91090c32a84dc7df732f44a406465 100644
--- a/spec/models/project_services/assembla_service_spec.rb
+++ b/spec/models/project_services/assembla_service_spec.rb
@@ -39,12 +39,12 @@ describe AssemblaService, models: true do
         token: 'verySecret',
         subdomain: 'project_name'
       )
-      @sample_data = Gitlab::PushDataBuilder.build_sample(project, user)
+      @sample_data = Gitlab::DataBuilder::Push.build_sample(project, user)
       @api_url = 'https://atlas.assembla.com/spaces/project_name/github_tool?secret_key=verySecret'
       WebMock.stub_request(:post, @api_url)
     end
 
-    it "should call Assembla API" do
+    it "calls Assembla API" do
       @assembla_service.execute(@sample_data)
       expect(WebMock).to have_requested(:post, @api_url).with(
         body: /#{@sample_data[:before]}.*#{@sample_data[:after]}.*#{project.path}/
diff --git a/spec/models/project_services/builds_email_service_spec.rb b/spec/models/project_services/builds_email_service_spec.rb
index ca2cd8aa551f7727cb045a1a83ca5da0275fe6a7..0194f9e256306bf63ae140b48029eea97259eccd 100644
--- a/spec/models/project_services/builds_email_service_spec.rb
+++ b/spec/models/project_services/builds_email_service_spec.rb
@@ -1,7 +1,9 @@
 require 'spec_helper'
 
 describe BuildsEmailService do
-  let(:data) { Gitlab::BuildDataBuilder.build(create(:ci_build)) }
+  let(:data) do
+    Gitlab::DataBuilder::Build.build(create(:ci_build))
+  end
 
   describe 'Validations' do
     context 'when service is active' do
@@ -39,7 +41,7 @@ describe BuildsEmailService do
 
   describe '#test' do
     it 'sends email' do
-      data = Gitlab::BuildDataBuilder.build(create(:ci_build))
+      data = Gitlab::DataBuilder::Build.build(create(:ci_build))
       subject.recipients = 'test@gitlab.com'
 
       expect(BuildEmailWorker).to receive(:perform_async)
@@ -49,7 +51,7 @@ describe BuildsEmailService do
 
     context 'notify only failed builds is true' do
       it 'sends email' do
-        data = Gitlab::BuildDataBuilder.build(create(:ci_build))
+        data = Gitlab::DataBuilder::Build.build(create(:ci_build))
         data[:build_status] = "success"
         subject.recipients = 'test@gitlab.com'
 
diff --git a/spec/models/project_services/campfire_service_spec.rb b/spec/models/project_services/campfire_service_spec.rb
index 3e6da42803b800822d640ae8c7f163fb1a751d5b..c76ae21421b3cbd58b3bb4e1f614bd8596aa3733 100644
--- a/spec/models/project_services/campfire_service_spec.rb
+++ b/spec/models/project_services/campfire_service_spec.rb
@@ -39,4 +39,62 @@ describe CampfireService, models: true do
       it { is_expected.not_to validate_presence_of(:token) }
     end
   end
+
+  describe "#execute" do
+    let(:user)    { create(:user) }
+    let(:project) { create(:project) }
+
+    before do
+      @campfire_service = CampfireService.new
+      allow(@campfire_service).to receive_messages(
+        project_id: project.id,
+        project: project,
+        service_hook: true,
+        token: 'verySecret',
+        subdomain: 'project-name',
+        room: 'test-room'
+      )
+      @sample_data = Gitlab::DataBuilder::Push.build_sample(project, user)
+      @rooms_url = 'https://verySecret:X@project-name.campfirenow.com/rooms.json'
+      @headers = { 'Content-Type' => 'application/json; charset=utf-8' }
+    end
+
+    it "calls Campfire API to get a list of rooms and speak in a room" do
+      # make sure a valid list of rooms is returned
+      body = File.read(Rails.root + 'spec/fixtures/project_services/campfire/rooms.json')
+      WebMock.stub_request(:get, @rooms_url).to_return(
+        body: body,
+        status: 200,
+        headers: @headers
+      )
+      # stub the speak request with the room id found in the previous request's response
+      speak_url = 'https://verySecret:X@project-name.campfirenow.com/room/123/speak.json'
+      WebMock.stub_request(:post, speak_url)
+
+      @campfire_service.execute(@sample_data)
+
+      expect(WebMock).to have_requested(:get, @rooms_url).once
+      expect(WebMock).to have_requested(:post, speak_url).with(
+        body: /#{project.path}.*#{@sample_data[:before]}.*#{@sample_data[:after]}/
+      ).once
+    end
+
+    it "calls Campfire API to get a list of rooms but shouldn't speak in a room" do
+      # return a list of rooms that do not contain a room named 'test-room'
+      body = File.read(Rails.root + 'spec/fixtures/project_services/campfire/rooms2.json')
+      WebMock.stub_request(:get, @rooms_url).to_return(
+        body: body,
+        status: 200,
+        headers: @headers
+      )
+      # we want to make sure no request is sent to the /speak endpoint, here is a basic
+      # regexp that matches this endpoint
+      speak_url = 'https://verySecret:X@project-name.campfirenow.com/room/.*/speak.json'
+
+      @campfire_service.execute(@sample_data)
+
+      expect(WebMock).to have_requested(:get, @rooms_url).once
+      expect(WebMock).not_to have_requested(:post, /#{speak_url}/)
+    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 3a8e67438fc308f98b4a03b5f5aac9757a48907a..8ef892259f260b8692b1b6f3319911d753371fe0 100644
--- a/spec/models/project_services/drone_ci_service_spec.rb
+++ b/spec/models/project_services/drone_ci_service_spec.rb
@@ -84,7 +84,9 @@ describe DroneCiService, models: true do
     include_context :drone_ci_service
 
     let(:user)    { create(:user, username: 'username') }
-    let(:push_sample_data) { Gitlab::PushDataBuilder.build_sample(project, user) }
+    let(:push_sample_data) do
+      Gitlab::DataBuilder::Push.build_sample(project, user)
+    end
 
     it do
       service_hook = double
diff --git a/spec/models/project_services/external_wiki_service_spec.rb b/spec/models/project_services/external_wiki_service_spec.rb
index 5fe5ea7d2dfc518e677f4a39e5ad363e6d7da050..d7c5ea95d71af9f6fda4401a9503e4a8acc4def7 100644
--- a/spec/models/project_services/external_wiki_service_spec.rb
+++ b/spec/models/project_services/external_wiki_service_spec.rb
@@ -56,7 +56,7 @@ describe ExternalWikiService, models: true do
         @service.destroy!
       end
 
-      it 'should replace the wiki url' do
+      it 'replaces the wiki url' do
         wiki_path = get_project_wiki_path(project)
         expect(wiki_path).to match('https://gitlab.com')
       end
diff --git a/spec/models/project_services/flowdock_service_spec.rb b/spec/models/project_services/flowdock_service_spec.rb
index b7e627e6518cb9e6584e5ed3dd022cd1f9189c32..d25570197567cadb8e05481741503fbc5a05f65a 100644
--- a/spec/models/project_services/flowdock_service_spec.rb
+++ b/spec/models/project_services/flowdock_service_spec.rb
@@ -52,12 +52,12 @@ describe FlowdockService, models: true do
         service_hook: true,
         token: 'verySecret'
       )
-      @sample_data = Gitlab::PushDataBuilder.build_sample(project, user)
+      @sample_data = Gitlab::DataBuilder::Push.build_sample(project, user)
       @api_url = 'https://api.flowdock.com/v1/messages'
       WebMock.stub_request(:post, @api_url)
     end
 
-    it "should call FlowDock API" do
+    it "calls FlowDock API" do
       @flowdock_service.execute(@sample_data)
       @sample_data[:commits].each do |commit|
         # One request to Flowdock per new commit
diff --git a/spec/models/project_services/gemnasium_service_spec.rb b/spec/models/project_services/gemnasium_service_spec.rb
index a08f1ac229f1ed8c090bfbad65197b1bac684f3f..3d0b6c9816bdc45beed5a12d426767db5d7d8b61 100644
--- a/spec/models/project_services/gemnasium_service_spec.rb
+++ b/spec/models/project_services/gemnasium_service_spec.rb
@@ -55,9 +55,9 @@ describe GemnasiumService, models: true do
         token: 'verySecret',
         api_key: 'GemnasiumUserApiKey'
       )
-      @sample_data = Gitlab::PushDataBuilder.build_sample(project, user)
+      @sample_data = Gitlab::DataBuilder::Push.build_sample(project, user)
     end
-    it "should call Gemnasium service" do
+    it "calls Gemnasium service" do
       expect(Gemnasium::GitlabService).to receive(:execute).with(an_instance_of(Hash)).once
       @gemnasium_service.execute(@sample_data)
     end
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 7a1f106d6e3a3a7b4d2065c790a2b998220d93a3..8ef79a17d502a05cd5a899eecd00bea3537f4363 100644
--- a/spec/models/project_services/gitlab_issue_tracker_service_spec.rb
+++ b/spec/models/project_services/gitlab_issue_tracker_service_spec.rb
@@ -54,7 +54,7 @@ describe GitlabIssueTrackerService, models: true do
         @service.destroy!
       end
 
-      it 'should give the correct path' do
+      it 'gives the correct path' do
         expect(@service.project_url).to eq("http://localhost/gitlab/root/#{project.path_with_namespace}/issues")
         expect(@service.new_issue_url).to eq("http://localhost/gitlab/root/#{project.path_with_namespace}/issues/new")
         expect(@service.issue_url(432)).to eq("http://localhost/gitlab/root/#{project.path_with_namespace}/issues/432")
@@ -71,7 +71,7 @@ describe GitlabIssueTrackerService, models: true do
         @service.destroy!
       end
 
-      it 'should give the correct path' do
+      it 'gives the correct path' do
         expect(@service.project_path).to eq("/gitlab/root/#{project.path_with_namespace}/issues")
         expect(@service.new_issue_path).to eq("/gitlab/root/#{project.path_with_namespace}/issues/new")
         expect(@service.issue_path(432)).to eq("/gitlab/root/#{project.path_with_namespace}/issues/432")
diff --git a/spec/models/project_services/hipchat_service_spec.rb b/spec/models/project_services/hipchat_service_spec.rb
index 62ae5f6cf7474512f195290c1e5fc9783c0d66d7..34eafbe555d7234d75af619d96689db5bd991031 100644
--- a/spec/models/project_services/hipchat_service_spec.rb
+++ b/spec/models/project_services/hipchat_service_spec.rb
@@ -48,7 +48,9 @@ describe HipchatService, models: true do
     let(:project_name) { project.name_with_namespace.gsub(/\s/, '') }
     let(:token) { 'verySecret' }
     let(:server_url) { 'https://hipchat.example.com'}
-    let(:push_sample_data) { Gitlab::PushDataBuilder.build_sample(project, user) }
+    let(:push_sample_data) do
+      Gitlab::DataBuilder::Push.build_sample(project, user)
+    end
 
     before(:each) do
       allow(hipchat).to receive_messages(
@@ -61,7 +63,7 @@ describe HipchatService, models: true do
       WebMock.stub_request(:post, api_url)
     end
 
-    it 'should test and return errors' do
+    it 'tests and return errors' do
       allow(hipchat).to receive(:execute).and_raise(StandardError, 'no such room')
       result = hipchat.test(push_sample_data)
 
@@ -69,7 +71,7 @@ describe HipchatService, models: true do
       expect(result[:result].to_s).to eq('no such room')
     end
 
-    it 'should use v1 if version is provided' do
+    it 'uses v1 if version is provided' do
       allow(hipchat).to receive(:api_version).and_return('v1')
       expect(HipChat::Client).to receive(:new).with(
         token,
@@ -79,7 +81,7 @@ describe HipchatService, models: true do
       hipchat.execute(push_sample_data)
     end
 
-    it 'should use v2 as the version when nothing is provided' do
+    it 'uses v2 as the version when nothing is provided' do
       allow(hipchat).to receive(:api_version).and_return('')
       expect(HipChat::Client).to receive(:new).with(
         token,
@@ -90,13 +92,13 @@ describe HipchatService, models: true do
     end
 
     context 'push events' do
-      it "should call Hipchat API for push events" do
+      it "calls Hipchat API for push events" do
         hipchat.execute(push_sample_data)
 
         expect(WebMock).to have_requested(:post, api_url).once
       end
 
-      it "should create a push message" do
+      it "creates a push message" do
         message = hipchat.send(:create_push_message, push_sample_data)
 
         push_sample_data[:object_attributes]
@@ -108,15 +110,23 @@ describe HipchatService, models: true do
     end
 
     context 'tag_push events' do
-      let(:push_sample_data) { Gitlab::PushDataBuilder.build(project, user, Gitlab::Git::BLANK_SHA, '1' * 40, 'refs/tags/test', []) }
+      let(:push_sample_data) do
+        Gitlab::DataBuilder::Push.build(
+          project,
+          user,
+          Gitlab::Git::BLANK_SHA,
+          '1' * 40,
+          'refs/tags/test',
+          [])
+      end
 
-      it "should call Hipchat API for tag push events" do
+      it "calls Hipchat API for tag push events" do
         hipchat.execute(push_sample_data)
 
         expect(WebMock).to have_requested(:post, api_url).once
       end
 
-      it "should create a tag push message" do
+      it "creates a tag push message" do
         message = hipchat.send(:create_push_message, push_sample_data)
 
         push_sample_data[:object_attributes]
@@ -131,13 +141,13 @@ describe HipchatService, models: true do
       let(:issue_service) { Issues::CreateService.new(project, user) }
       let(:issues_sample_data) { issue_service.hook_data(issue, 'open') }
 
-      it "should call Hipchat API for issue events" do
+      it "calls Hipchat API for issue events" do
         hipchat.execute(issues_sample_data)
 
         expect(WebMock).to have_requested(:post, api_url).once
       end
 
-      it "should create an issue message" do
+      it "creates an issue message" do
         message = hipchat.send(:create_issue_message, issues_sample_data)
 
         obj_attr = issues_sample_data[:object_attributes]
@@ -154,13 +164,13 @@ describe HipchatService, models: true do
       let(:merge_service) { MergeRequests::CreateService.new(project, user) }
       let(:merge_sample_data) { merge_service.hook_data(merge_request, 'open') }
 
-      it "should call Hipchat API for merge requests events" do
+      it "calls Hipchat API for merge requests events" do
         hipchat.execute(merge_sample_data)
 
         expect(WebMock).to have_requested(:post, api_url).once
       end
 
-      it "should create a merge request message" do
+      it "creates a merge request message" do
         message = hipchat.send(:create_merge_request_message,
                                merge_sample_data)
 
@@ -184,8 +194,8 @@ describe HipchatService, models: true do
                                   note: 'a comment on a commit')
         end
 
-        it "should call Hipchat API for commit comment events" do
-          data = Gitlab::NoteDataBuilder.build(commit_note, user)
+        it "calls Hipchat API for commit comment events" do
+          data = Gitlab::DataBuilder::Note.build(commit_note, user)
           hipchat.execute(data)
 
           expect(WebMock).to have_requested(:post, api_url).once
@@ -216,8 +226,8 @@ describe HipchatService, models: true do
                                          note: "merge request note")
         end
 
-        it "should call Hipchat API for merge request comment events" do
-          data = Gitlab::NoteDataBuilder.build(merge_request_note, user)
+        it "calls Hipchat API for merge request comment events" do
+          data = Gitlab::DataBuilder::Note.build(merge_request_note, user)
           hipchat.execute(data)
 
           expect(WebMock).to have_requested(:post, api_url).once
@@ -243,8 +253,8 @@ describe HipchatService, models: true do
                                  note: "issue note")
         end
 
-        it "should call Hipchat API for issue comment events" do
-          data = Gitlab::NoteDataBuilder.build(issue_note, user)
+        it "calls Hipchat API for issue comment events" do
+          data = Gitlab::DataBuilder::Note.build(issue_note, user)
           hipchat.execute(data)
 
           message = hipchat.send(:create_message, data)
@@ -269,8 +279,8 @@ describe HipchatService, models: true do
                                            note: "snippet note")
         end
 
-        it "should call Hipchat API for snippet comment events" do
-          data = Gitlab::NoteDataBuilder.build(snippet_note, user)
+        it "calls Hipchat API for snippet comment events" do
+          data = Gitlab::DataBuilder::Note.build(snippet_note, user)
           hipchat.execute(data)
 
           expect(WebMock).to have_requested(:post, api_url).once
@@ -291,19 +301,20 @@ describe HipchatService, models: true do
     end
 
     context 'build events' do
-      let(:build) { create(:ci_build) }
-      let(:data) { Gitlab::BuildDataBuilder.build(build) }
+      let(:pipeline) { create(:ci_empty_pipeline) }
+      let(:build) { create(:ci_build, pipeline: pipeline) }
+      let(:data) { Gitlab::DataBuilder::Build.build(build) }
 
       context 'for failed' do
         before { build.drop }
 
-        it "should call Hipchat API" do
+        it "calls Hipchat API" do
           hipchat.execute(data)
 
           expect(WebMock).to have_requested(:post, api_url).once
         end
 
-        it "should create a build message" do
+        it "creates a build message" do
           message = hipchat.send(:create_build_message, data)
 
           project_url = project.web_url
@@ -325,13 +336,13 @@ describe HipchatService, models: true do
           build.success
         end
 
-        it "should call Hipchat API" do
+        it "calls Hipchat API" do
           hipchat.notify_only_broken_builds = false
           hipchat.execute(data)
           expect(WebMock).to have_requested(:post, api_url).once
         end
 
-        it "should notify only broken" do
+        it "notifies only broken" do
           hipchat.notify_only_broken_builds = true
           hipchat.execute(data)
           expect(WebMock).not_to have_requested(:post, api_url).once
diff --git a/spec/models/project_services/irker_service_spec.rb b/spec/models/project_services/irker_service_spec.rb
index 4ee022a51710ec490efbc85eb824de543b1e9df1..ffb17fd3259d3bbb4f0810d1619db131a3a5dead 100644
--- a/spec/models/project_services/irker_service_spec.rb
+++ b/spec/models/project_services/irker_service_spec.rb
@@ -46,32 +46,35 @@ describe IrkerService, models: true do
     let(:irker) { IrkerService.new }
     let(:user) { create(:user) }
     let(:project) { create(:project) }
-    let(:sample_data) { Gitlab::PushDataBuilder.build_sample(project, user) }
+    let(:sample_data) do
+      Gitlab::DataBuilder::Push.build_sample(project, user)
+    end
 
     let(:recipients) { '#commits irc://test.net/#test ftp://bad' }
     let(:colorize_messages) { '1' }
 
     before do
+      @irker_server = TCPServer.new 'localhost', 0
+
       allow(irker).to receive_messages(
         active: true,
         project: project,
         project_id: project.id,
         service_hook: true,
-        server_host: 'localhost',
-        server_port: 6659,
+        server_host: @irker_server.addr[2],
+        server_port: @irker_server.addr[1],
         default_irc_uri: 'irc://chat.freenode.net/',
         recipients: recipients,
         colorize_messages: colorize_messages)
 
       irker.valid?
-      @irker_server = TCPServer.new 'localhost', 6659
     end
 
     after do
       @irker_server.close
     end
 
-    it 'should send valid JSON messages to an Irker listener' do
+    it 'sends valid JSON messages to an Irker listener' do
       irker.execute(sample_data)
 
       conn = @irker_server.accept
diff --git a/spec/models/project_services/jira_service_spec.rb b/spec/models/project_services/jira_service_spec.rb
index 5a97cf370dae20dab8f3b05e1149db16ef70f3ac..9037ca5cc2026513c9794247d51f8bd6c320a61b 100644
--- a/spec/models/project_services/jira_service_spec.rb
+++ b/spec/models/project_services/jira_service_spec.rb
@@ -66,7 +66,7 @@ describe JiraService, models: true do
         password: 'gitlab_jira_password'
       )
       @jira_service.save # will build API URL, as api_url was not specified above
-      @sample_data = Gitlab::PushDataBuilder.build_sample(project, user)
+      @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'
@@ -75,7 +75,7 @@ describe JiraService, models: true do
       WebMock.stub_request(:post, @comment_url)
     end
 
-    it "should call JIRA API" do
+    it "calls JIRA API" do
       @jira_service.execute(merge_request,
                             ExternalIssue.new("JIRA-123", project))
       expect(WebMock).to have_requested(:post, @comment_url).with(
@@ -128,7 +128,7 @@ describe JiraService, models: true do
         expect(@jira_service.api_url).to eq("http://jira_edited.example.com/rest/api/2")
       end
 
-      it "should reset password if url changed, even if setter called multiple times" do
+      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.save
@@ -181,7 +181,7 @@ describe JiraService, models: true do
         @service.destroy!
       end
 
-      it 'should be initialized' do
+      it 'is initialized' do
         expect(@service.title).to eq('JIRA')
         expect(@service.description).to eq("Jira issue tracker")
       end
@@ -197,7 +197,7 @@ describe JiraService, models: true do
         @service.destroy!
       end
 
-      it "should be correct" do
+      it "is correct" do
         expect(@service.title).to eq('Jira One')
         expect(@service.description).to eq('Jira One issue tracker')
       end
@@ -225,7 +225,7 @@ describe JiraService, models: true do
         @service.destroy!
       end
 
-      it 'should be prepopulated with the settings' do
+      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")
diff --git a/spec/models/project_services/pivotaltracker_service_spec.rb b/spec/models/project_services/pivotaltracker_service_spec.rb
index f37edd4d970967086a38870a2aff1c5fb809cc10..d098d988521188c8cb1694f52523d4b10216d595 100644
--- a/spec/models/project_services/pivotaltracker_service_spec.rb
+++ b/spec/models/project_services/pivotaltracker_service_spec.rb
@@ -39,4 +39,75 @@ describe PivotaltrackerService, models: true do
       it { is_expected.not_to validate_presence_of(:token) }
     end
   end
+
+  describe 'Execute' do
+    let(:service) do
+      PivotaltrackerService.new.tap do |service|
+        service.token = 'secret_api_token'
+      end
+    end
+
+    let(:url) { PivotaltrackerService::API_ENDPOINT }
+
+    def push_data(branch: 'master')
+      {
+        object_kind: 'push',
+        ref: "refs/heads/#{branch}",
+        commits: [
+          {
+            id: '21c12ea',
+            author: {
+              name: 'Some User'
+            },
+            url: 'https://example.com/commit',
+            message: 'commit message',
+          }
+        ]
+      }
+    end
+
+    before do
+      WebMock.stub_request(:post, url)
+    end
+
+    it 'should post correct message' do
+      service.execute(push_data)
+      expect(WebMock).to have_requested(:post, url).with(
+        body: {
+          'source_commit' => {
+            'commit_id' => '21c12ea',
+            'author' => 'Some User',
+            'url' => 'https://example.com/commit',
+            'message' => 'commit message'
+          }
+        },
+        headers: {
+          'Content-Type' => 'application/json',
+          'X-TrackerToken' => 'secret_api_token'
+        }
+      ).once
+    end
+
+    context 'when allowed branches is specified' do
+      let(:service) do
+        super().tap do |service|
+          service.restrict_to_branch = 'master,v10'
+        end
+      end
+
+      it 'should post message if branch is in the list' do
+        service.execute(push_data(branch: 'master'))
+        service.execute(push_data(branch: 'v10'))
+
+        expect(WebMock).to have_requested(:post, url).twice
+      end
+
+      it 'should not post message if branch is not in the list' do
+        service.execute(push_data(branch: 'mas'))
+        service.execute(push_data(branch: 'v11'))
+
+        expect(WebMock).not_to have_requested(:post, url)
+      end
+    end
+  end
 end
diff --git a/spec/models/project_services/pushover_service_spec.rb b/spec/models/project_services/pushover_service_spec.rb
index 555d9757b47a7bf55ab7979f7fbafdecdb6b5f3b..5959c81577d93a6a8ec4ccfd3b1f73a56fccac6f 100644
--- a/spec/models/project_services/pushover_service_spec.rb
+++ b/spec/models/project_services/pushover_service_spec.rb
@@ -48,7 +48,9 @@ describe PushoverService, models: true do
     let(:pushover) { PushoverService.new }
     let(:user) { create(:user) }
     let(:project) { create(:project) }
-    let(:sample_data) { Gitlab::PushDataBuilder.build_sample(project, user) }
+    let(:sample_data) do
+      Gitlab::DataBuilder::Push.build_sample(project, user)
+    end
 
     let(:api_key) { 'verySecret' }
     let(:user_key) { 'verySecret' }
@@ -72,7 +74,7 @@ describe PushoverService, models: true do
       WebMock.stub_request(:post, api_url)
     end
 
-    it 'should call Pushover API' do
+    it 'calls Pushover API' do
       pushover.execute(sample_data)
 
       expect(WebMock).to have_requested(:post, api_url).once
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 379c3e1219c3b99d341fa855c57fdc8274aea6de..41b93f08050833499baf90ca88a9ced8332f2399 100644
--- a/spec/models/project_services/slack_service/note_message_spec.rb
+++ b/spec/models/project_services/slack_service/note_message_spec.rb
@@ -60,6 +60,7 @@ describe SlackService::NoteMessage, models: true do
           title: "merge request title\ndetails\n"
       }
     end
+
     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 " \
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 46dedb66c7c112b6e279b57ddc2ab173eb1e10c3..13aea0b0600675a331eb883b889b298ce826f5d2 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
@@ -47,7 +47,7 @@ describe SlackService::WikiPageMessage, models: true do
     context 'when :action == "create"' do
       before { args[:object_attributes][:action] = 'create' }
 
-      it 'it returns the attachment for a new wiki page' do
+      it 'returns the attachment for a new wiki page' do
         expect(subject.attachments).to eq([
           {
             text: "Wiki page description",
@@ -60,7 +60,7 @@ describe SlackService::WikiPageMessage, models: true do
     context 'when :action == "update"' do
       before { args[:object_attributes][:action] = 'update' }
 
-      it 'it returns the attachment for an updated wiki page' do
+      it 'returns the attachment for an updated wiki page' do
         expect(subject.attachments).to eq([
           {
             text: "Wiki page description",
diff --git a/spec/models/project_services/slack_service_spec.rb b/spec/models/project_services/slack_service_spec.rb
index 9fc097f2ce044f58ebe5f2f8cd1997c8ddad69c2..9b7899270d0dffbd10cc8200e522d9eb0e8e4d39 100644
--- a/spec/models/project_services/slack_service_spec.rb
+++ b/spec/models/project_services/slack_service_spec.rb
@@ -47,9 +47,12 @@ describe SlackService, models: true do
   describe "Execute" do
     let(:user)    { create(:user) }
     let(:project) { create(:project) }
-    let(:push_sample_data) { Gitlab::PushDataBuilder.build_sample(project, user) }
     let(:username) { 'slack_username' }
-    let(:channel) { 'slack_channel' }
+    let(:channel)  { 'slack_channel' }
+
+    let(:push_sample_data) do
+      Gitlab::DataBuilder::Push.build_sample(project, user)
+    end
 
     before do
       allow(slack).to receive_messages(
@@ -94,31 +97,31 @@ describe SlackService, models: true do
       @wiki_page_sample_data = wiki_page_service.hook_data(@wiki_page, 'create')
     end
 
-    it "should call Slack API for push events" do
+    it "calls Slack API for push events" do
       slack.execute(push_sample_data)
 
       expect(WebMock).to have_requested(:post, webhook_url).once
     end
 
-    it "should call Slack API for issue events" do
+    it "calls Slack API for issue events" do
       slack.execute(@issues_sample_data)
 
       expect(WebMock).to have_requested(:post, webhook_url).once
     end
 
-    it "should call Slack API for merge requests events" do
+    it "calls Slack API for merge requests events" do
       slack.execute(@merge_sample_data)
 
       expect(WebMock).to have_requested(:post, webhook_url).once
     end
 
-    it "should call Slack API for wiki page events" do
+    it "calls Slack API for wiki page events" do
       slack.execute(@wiki_page_sample_data)
 
       expect(WebMock).to have_requested(:post, webhook_url).once
     end
 
-    it 'should use the username as an option for slack when configured' do
+    it 'uses the username as an option for slack when configured' do
       allow(slack).to receive(:username).and_return(username)
       expect(Slack::Notifier).to receive(:new).
        with(webhook_url, username: username).
@@ -129,7 +132,7 @@ describe SlackService, models: true do
       slack.execute(push_sample_data)
     end
 
-    it 'should use the channel as an option when it is configured' do
+    it 'uses the channel as an option when it is configured' do
       allow(slack).to receive(:channel).and_return(channel)
       expect(Slack::Notifier).to receive(:new).
         with(webhook_url, channel: channel).
@@ -196,7 +199,7 @@ describe SlackService, models: true do
         it "uses the right channel" do
           slack.update_attributes(note_channel: "random")
 
-          note_data = Gitlab::NoteDataBuilder.build(issue_note, user)
+          note_data = Gitlab::DataBuilder::Note.build(issue_note, user)
 
           expect(Slack::Notifier).to receive(:new).
            with(webhook_url, channel: "random").
@@ -233,8 +236,8 @@ describe SlackService, models: true do
                                 note: 'a comment on a commit')
       end
 
-      it "should call Slack API for commit comment events" do
-        data = Gitlab::NoteDataBuilder.build(commit_note, user)
+      it "calls Slack API for commit comment events" do
+        data = Gitlab::DataBuilder::Note.build(commit_note, user)
         slack.execute(data)
 
         expect(WebMock).to have_requested(:post, webhook_url).once
@@ -247,8 +250,8 @@ describe SlackService, models: true do
                                        note: "merge request note")
       end
 
-      it "should call Slack API for merge request comment events" do
-        data = Gitlab::NoteDataBuilder.build(merge_request_note, user)
+      it "calls Slack API for merge request comment events" do
+        data = Gitlab::DataBuilder::Note.build(merge_request_note, user)
         slack.execute(data)
 
         expect(WebMock).to have_requested(:post, webhook_url).once
@@ -260,8 +263,8 @@ describe SlackService, models: true do
         create(:note_on_issue, project: project, note: "issue note")
       end
 
-      it "should call Slack API for issue comment events" do
-        data = Gitlab::NoteDataBuilder.build(issue_note, user)
+      it "calls Slack API for issue comment events" do
+        data = Gitlab::DataBuilder::Note.build(issue_note, user)
         slack.execute(data)
 
         expect(WebMock).to have_requested(:post, webhook_url).once
@@ -274,8 +277,8 @@ describe SlackService, models: true do
                                          note: "snippet note")
       end
 
-      it "should call Slack API for snippet comment events" do
-        data = Gitlab::NoteDataBuilder.build(snippet_note, user)
+      it "calls Slack API for snippet comment events" do
+        data = Gitlab::DataBuilder::Note.build(snippet_note, user)
         slack.execute(data)
 
         expect(WebMock).to have_requested(:post, webhook_url).once
diff --git a/spec/models/project_spec.rb b/spec/models/project_spec.rb
index e365e4e98b2bdf2ad9551e615671a8e3d383681f..9a3660012f995320f35f3ff3f657d7cbcf20786d 100644
--- a/spec/models/project_spec.rb
+++ b/spec/models/project_spec.rb
@@ -23,6 +23,7 @@ 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_one(:board).dependent(:destroy) }
     it { is_expected.to have_many(:commit_statuses) }
     it { is_expected.to have_many(:pipelines) }
     it { is_expected.to have_many(:builds) }
@@ -69,6 +70,7 @@ describe Project, models: true do
     it { is_expected.to include_module(Gitlab::ConfigHelper) }
     it { is_expected.to include_module(Gitlab::ShellAdapter) }
     it { is_expected.to include_module(Gitlab::VisibilityLevel) }
+    it { is_expected.to include_module(Gitlab::CurrentSettings) }
     it { is_expected.to include_module(Referable) }
     it { is_expected.to include_module(Sortable) }
   end
@@ -88,7 +90,7 @@ describe Project, models: true do
     it { is_expected.to validate_presence_of(:namespace) }
     it { is_expected.to validate_presence_of(:repository_storage) }
 
-    it 'should not allow new projects beyond user limits' do
+    it 'does not allow new projects beyond user limits' do
       project2 = build(:project)
       allow(project2).to receive(:creator).and_return(double(can_create_project?: false, projects_limit: 0).as_null_object)
       expect(project2).not_to be_valid
@@ -97,7 +99,7 @@ describe Project, models: true do
 
     describe 'wiki path conflict' do
       context "when the new path has been used by the wiki of other Project" do
-        it 'should have an error on the name attribute' do
+        it 'has an error on the name attribute' do
           new_project = build_stubbed(:project, namespace_id: project.namespace_id, path: "#{project.path}.wiki")
 
           expect(new_project).not_to be_valid
@@ -106,7 +108,7 @@ describe Project, models: true do
       end
 
       context "when the new wiki path has been used by the path of other Project" do
-        it 'should have an error on the name attribute' do
+        it 'has an error on the name attribute' do
           project_with_wiki_suffix = create(:project, path: 'foo.wiki')
           new_project = build_stubbed(:project, namespace_id: project_with_wiki_suffix.namespace_id, path: 'foo')
 
@@ -124,7 +126,7 @@ describe Project, models: true do
         allow(Gitlab.config.repositories).to receive(:storages).and_return(storages)
       end
 
-      it "should not allow repository storages that don't match a label in the configuration" do
+      it "does not allow repository storages that don't match a label in the configuration" do
         expect(project2).not_to be_valid
         expect(project2.errors[:repository_storage].first).to match(/is not included in the list/)
       end
@@ -171,12 +173,12 @@ describe Project, models: true do
   end
 
   describe 'project token' do
-    it 'should set an random token if none provided' do
+    it 'sets an random token if none provided' do
       project = FactoryGirl.create :empty_project, runners_token: ''
       expect(project.runners_token).not_to eq('')
     end
 
-    it 'should not set an random toke if one provided' do
+    it 'does not set an random toke if one provided' do
       project = FactoryGirl.create :empty_project, runners_token: 'my-token'
       expect(project.runners_token).to eq('my-token')
     end
@@ -224,7 +226,7 @@ describe Project, models: true do
     end
   end
 
-  it 'should return valid url to repo' do
+  it 'returns valid url to repo' do
     project = Project.new(path: 'somewhere')
     expect(project.url_to_repo).to eq(Gitlab.config.gitlab_shell.ssh_path_prefix + 'somewhere.git')
   end
@@ -245,7 +247,7 @@ describe Project, models: true do
     end
   end
 
-  describe "#new_issue_address" do
+  xdescribe "#new_issue_address" do
     let(:project) { create(:empty_project, path: "somewhere") }
     let(:user) { create(:user) }
 
@@ -278,7 +280,7 @@ describe Project, models: true do
     let(:last_event) { double(created_at: Time.now) }
 
     describe 'last_activity' do
-      it 'should alias last_activity to last_event' do
+      it 'alias last_activity to last_event' do
         allow(project).to receive(:last_event).and_return(last_event)
         expect(project.last_activity).to eq(last_event)
       end
@@ -349,13 +351,13 @@ describe Project, models: true do
     let(:prev_commit_id) { merge_request.commits.last.id }
     let(:commit_id) { merge_request.commits.first.id }
 
-    it 'should close merge request if last commit from source branch was pushed to target branch' do
+    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 'should update merge request commits with new one if pushed to source branch' do
+    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)
@@ -432,11 +434,11 @@ describe Project, models: true do
     let(:project) { create(:project) }
     let(:ext_project) { create(:redmine_project) }
 
-    it "should be true if used internal tracker" do
+    it "is true if used internal tracker" do
       expect(project.default_issues_tracker?).to be_truthy
     end
 
-    it "should be false if used other tracker" do
+    it "is false if used other tracker" do
       expect(ext_project.default_issues_tracker?).to be_falsey
     end
   end
@@ -635,12 +637,12 @@ describe Project, models: true do
   describe '#avatar_type' do
     let(:project) { create(:project) }
 
-    it 'should be true if avatar is image' do
+    it 'is true if avatar is image' do
       project.update_attribute(:avatar, 'uploads/avatar.png')
       expect(project.avatar_type).to be_truthy
     end
 
-    it 'should be false if avatar is html page' do
+    it 'is false if avatar is html page' do
       project.update_attribute(:avatar, 'uploads/avatar.html')
       expect(project.avatar_type).to eq(['only images allowed'])
     end
@@ -713,6 +715,20 @@ describe Project, models: true do
     it { expect(project.builds_enabled?).to be_truthy }
   end
 
+  describe '.cached_count', caching: true do
+    let(:group)     { create(:group, :public) }
+    let!(:project1) { create(:empty_project, :public, group: group) }
+    let!(:project2) { create(:empty_project, :public, group: group) }
+
+    it 'returns total project count' do
+      expect(Project).to receive(:count).once.and_call_original
+
+      3.times do
+        expect(Project.cached_count).to eq(2)
+      end
+    end
+  end
+
   describe '.trending' do
     let(:group)    { create(:group, :public) }
     let(:project1) { create(:empty_project, :public, group: group) }
@@ -813,16 +829,16 @@ describe Project, models: true do
     context 'for shared runners disabled' do
       let(:shared_runners_enabled) { false }
 
-      it 'there are no runners available' do
+      it 'has no runners available' do
         expect(project.any_runners?).to be_falsey
       end
 
-      it 'there is a specific runner' do
+      it 'has a specific runner' do
         project.runners << specific_runner
         expect(project.any_runners?).to be_truthy
       end
 
-      it 'there is a shared runner, but they are prohibited to use' do
+      it 'has a shared runner, but they are prohibited to use' do
         shared_runner
         expect(project.any_runners?).to be_falsey
       end
@@ -836,7 +852,7 @@ describe Project, models: true do
     context 'for shared runners enabled' do
       let(:shared_runners_enabled) { true }
 
-      it 'there is a shared runner' do
+      it 'has a shared runner' do
         shared_runner
         expect(project.any_runners?).to be_truthy
       end
@@ -1070,28 +1086,97 @@ describe Project, models: true do
   end
 
   describe '#protected_branch?' do
+    context 'existing project' do
+      let(:project) { create(:project) }
+
+      it 'returns true when the branch matches a protected branch via direct match' do
+        create(:protected_branch, project: project, name: "foo")
+
+        expect(project.protected_branch?('foo')).to eq(true)
+      end
+
+      it 'returns true when the branch matches a protected branch via wildcard match' do
+        create(:protected_branch, project: project, name: "production/*")
+
+        expect(project.protected_branch?('production/some-branch')).to eq(true)
+      end
+
+      it 'returns false when the branch does not match a protected branch via direct match' do
+        expect(project.protected_branch?('foo')).to eq(false)
+      end
+
+      it 'returns false when the branch does not match a protected branch via wildcard match' do
+        create(:protected_branch, project: project, name: "production/*")
+
+        expect(project.protected_branch?('staging/some-branch')).to eq(false)
+      end
+    end
+
+    context "new project" do
+      let(:project) { create(:empty_project) }
+
+      it 'returns false when default_protected_branch is unprotected' do
+        stub_application_setting(default_branch_protection: Gitlab::Access::PROTECTION_NONE)
+
+        expect(project.protected_branch?('master')).to be false
+      end
+
+      it 'returns false when default_protected_branch lets developers push' do
+        stub_application_setting(default_branch_protection: Gitlab::Access::PROTECTION_DEV_CAN_PUSH)
+
+        expect(project.protected_branch?('master')).to be false
+      end
+
+      it 'returns true when default_branch_protection does not let developers push but let developer merge branches' do
+        stub_application_setting(default_branch_protection: Gitlab::Access::PROTECTION_DEV_CAN_MERGE)
+
+        expect(project.protected_branch?('master')).to be true
+      end
+
+      it 'returns true when default_branch_protection is in full protection' do
+        stub_application_setting(default_branch_protection: Gitlab::Access::PROTECTION_FULL)
+
+        expect(project.protected_branch?('master')).to be true
+      end
+    end
+  end
+
+  describe '#user_can_push_to_empty_repo?' do
     let(:project) { create(:empty_project) }
+    let(:user)    { create(:user) }
 
-    it 'returns true when the branch matches a protected branch via direct match' do
-      project.protected_branches.create!(name: 'foo')
+    it 'returns false when default_branch_protection is in full protection and user is developer' do
+      project.team << [user, :developer]
+      stub_application_setting(default_branch_protection: Gitlab::Access::PROTECTION_FULL)
 
-      expect(project.protected_branch?('foo')).to eq(true)
+      expect(project.user_can_push_to_empty_repo?(user)).to be_falsey
     end
 
-    it 'returns true when the branch matches a protected branch via wildcard match' do
-      project.protected_branches.create!(name: 'production/*')
+    it 'returns false when default_branch_protection only lets devs merge and user is dev' do
+      project.team << [user, :developer]
+      stub_application_setting(default_branch_protection: Gitlab::Access::PROTECTION_DEV_CAN_MERGE)
 
-      expect(project.protected_branch?('production/some-branch')).to eq(true)
+      expect(project.user_can_push_to_empty_repo?(user)).to be_falsey
     end
 
-    it 'returns false when the branch does not match a protected branch via direct match' do
-      expect(project.protected_branch?('foo')).to eq(false)
+    it 'returns true when default_branch_protection lets devs push and user is developer' do
+      project.team << [user, :developer]
+      stub_application_setting(default_branch_protection: Gitlab::Access::PROTECTION_DEV_CAN_PUSH)
+
+      expect(project.user_can_push_to_empty_repo?(user)).to be_truthy
+    end
+
+    it 'returns true when default_branch_protection is unprotected and user is developer' do
+      project.team << [user, :developer]
+      stub_application_setting(default_branch_protection: Gitlab::Access::PROTECTION_NONE)
+
+      expect(project.user_can_push_to_empty_repo?(user)).to be_truthy
     end
 
-    it 'returns false when the branch does not match a protected branch via wildcard match' do
-      project.protected_branches.create!(name: 'production/*')
+    it 'returns true when user is master' do
+      project.team << [user, :master]
 
-      expect(project.protected_branch?('staging/some-branch')).to eq(false)
+      expect(project.user_can_push_to_empty_repo?(user)).to be_truthy
     end
   end
 
diff --git a/spec/models/project_team_spec.rb b/spec/models/project_team_spec.rb
index 1f42fbd3385d4ef9b4396fed18a7e874fa180bfa..5eaf0d3b7a6c5f1b823647f744bc6e84fdfb9066 100644
--- a/spec/models/project_team_spec.rb
+++ b/spec/models/project_team_spec.rb
@@ -199,48 +199,69 @@ describe ProjectTeam, models: true do
     end
   end
 
-  describe "#max_member_access_for_users" do
-    it 'returns correct roles for different users' do
-      master = create(:user)
-      reporter = create(:user)
-      promoted_guest = create(:user)
-      guest = create(:user)
-      project = create(:project)
+  shared_examples_for "#max_member_access_for_users" do |enable_request_store|
+    describe "#max_member_access_for_users" do
+      before do
+        RequestStore.begin! if enable_request_store
+      end
 
-      project.team << [master, :master]
-      project.team << [reporter, :reporter]
-      project.team << [promoted_guest, :guest]
-      project.team << [guest, :guest]
+      after do
+        if enable_request_store
+          RequestStore.end!
+          RequestStore.clear!
+        end
+      end
 
-      group = create(:group)
-      group_developer = create(:user)
-      second_developer = create(:user)
-      project.project_group_links.create(
-        group: group,
-        group_access: Gitlab::Access::DEVELOPER)
-
-      group.add_master(promoted_guest)
-      group.add_developer(group_developer)
-      group.add_developer(second_developer)
-
-      second_group = create(:group)
-      project.project_group_links.create(
-        group: second_group,
-        group_access: Gitlab::Access::MASTER)
-      second_group.add_master(second_developer)
-
-      users = [master, reporter, promoted_guest, guest, group_developer, second_developer].map(&:id)
-
-      expected = {
-        master.id => Gitlab::Access::MASTER,
-        reporter.id => Gitlab::Access::REPORTER,
-        promoted_guest.id => Gitlab::Access::DEVELOPER,
-        guest.id => Gitlab::Access::GUEST,
-        group_developer.id => Gitlab::Access::DEVELOPER,
-        second_developer.id => Gitlab::Access::MASTER
-      }
-
-      expect(project.team.max_member_access_for_user_ids(users)).to eq(expected)
+      it 'returns correct roles for different users' do
+        master = create(:user)
+        reporter = create(:user)
+        promoted_guest = create(:user)
+        guest = create(:user)
+        project = create(:project)
+
+        project.team << [master, :master]
+        project.team << [reporter, :reporter]
+        project.team << [promoted_guest, :guest]
+        project.team << [guest, :guest]
+
+        group = create(:group)
+        group_developer = create(:user)
+        second_developer = create(:user)
+        project.project_group_links.create(
+          group: group,
+          group_access: Gitlab::Access::DEVELOPER)
+
+        group.add_master(promoted_guest)
+        group.add_developer(group_developer)
+        group.add_developer(second_developer)
+
+        second_group = create(:group)
+        project.project_group_links.create(
+          group: second_group,
+          group_access: Gitlab::Access::MASTER)
+        second_group.add_master(second_developer)
+
+        users = [master, reporter, promoted_guest, guest, group_developer, second_developer].map(&:id)
+
+        expected = {
+          master.id => Gitlab::Access::MASTER,
+          reporter.id => Gitlab::Access::REPORTER,
+          promoted_guest.id => Gitlab::Access::DEVELOPER,
+          guest.id => Gitlab::Access::GUEST,
+          group_developer.id => Gitlab::Access::DEVELOPER,
+          second_developer.id => Gitlab::Access::MASTER
+        }
+
+        expect(project.team.max_member_access_for_user_ids(users)).to eq(expected)
+      end
     end
   end
+
+  describe '#max_member_access_for_users with RequestStore' do
+    it_behaves_like "#max_member_access_for_users", true
+  end
+
+  describe '#max_member_access_for_users without RequestStore' do
+    it_behaves_like "#max_member_access_for_users", false
+  end
 end
diff --git a/spec/models/repository_spec.rb b/spec/models/repository_spec.rb
index cce15538b93ed9a2092f6f0bab156fcb1cf82962..1fea50ad42c549df96069ce8c2367749dee3c7ff 100644
--- a/spec/models/repository_spec.rb
+++ b/spec/models/repository_spec.rb
@@ -340,14 +340,14 @@ describe Repository, models: true do
 
   describe '#add_branch' do
     context 'when pre hooks were successful' do
-      it 'should run without errors' do
+      it 'runs without errors' do
         hook = double(trigger: [true, nil])
         expect(Gitlab::Git::Hook).to receive(:new).exactly(3).times.and_return(hook)
 
         expect { repository.add_branch(user, 'new_feature', 'master') }.not_to raise_error
       end
 
-      it 'should create the branch' do
+      it 'creates the branch' do
         allow_any_instance_of(Gitlab::Git::Hook).to receive(:trigger).and_return([true, nil])
 
         branch = repository.add_branch(user, 'new_feature', 'master')
@@ -363,7 +363,7 @@ describe Repository, models: true do
     end
 
     context 'when pre hooks failed' do
-      it 'should get an error' do
+      it 'gets an error' do
         allow_any_instance_of(Gitlab::Git::Hook).to receive(:trigger).and_return([false, ''])
 
         expect do
@@ -371,7 +371,7 @@ describe Repository, models: true do
         end.to raise_error(GitHooksService::PreReceiveError)
       end
 
-      it 'should not create the branch' do
+      it 'does not create the branch' do
         allow_any_instance_of(Gitlab::Git::Hook).to receive(:trigger).and_return([false, ''])
 
         expect do
@@ -387,14 +387,14 @@ describe Repository, models: true do
     let(:blank_sha) { '0000000000000000000000000000000000000000' }
 
     context 'when pre hooks were successful' do
-      it 'should run without errors' do
+      it 'runs without errors' do
         expect_any_instance_of(GitHooksService).to receive(:execute).
           with(user, project.repository.path_to_repo, old_rev, blank_sha, 'refs/heads/feature')
 
         expect { repository.rm_branch(user, 'feature') }.not_to raise_error
       end
 
-      it 'should delete the branch' do
+      it 'deletes the branch' do
         allow_any_instance_of(Gitlab::Git::Hook).to receive(:trigger).and_return([true, nil])
 
         expect { repository.rm_branch(user, 'feature') }.not_to raise_error
@@ -404,7 +404,7 @@ describe Repository, models: true do
     end
 
     context 'when pre hooks failed' do
-      it 'should get an error' do
+      it 'gets an error' do
         allow_any_instance_of(Gitlab::Git::Hook).to receive(:trigger).and_return([false, ''])
 
         expect do
@@ -412,7 +412,7 @@ describe Repository, models: true do
         end.to raise_error(GitHooksService::PreReceiveError)
       end
 
-      it 'should not delete the branch' do
+      it 'does not delete the branch' do
         allow_any_instance_of(Gitlab::Git::Hook).to receive(:trigger).and_return([false, ''])
 
         expect do
@@ -433,13 +433,13 @@ describe Repository, models: true do
           and_yield.and_return(true)
       end
 
-      it 'should run without errors' do
+      it 'runs without errors' do
         expect do
           repository.commit_with_hooks(user, 'feature') { sample_commit.id }
         end.not_to raise_error
       end
 
-      it 'should ensure the autocrlf Git option is set to :input' do
+      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 }
@@ -455,7 +455,7 @@ describe Repository, models: true do
     end
 
     context 'when pre hooks failed' do
-      it 'should get an error' do
+      it 'gets an error' do
         allow_any_instance_of(Gitlab::Git::Hook).to receive(:trigger).and_return([false, ''])
 
         expect do
@@ -715,10 +715,18 @@ describe Repository, models: true do
   end
 
   describe '#merge' do
-    it 'should merge the code and return the commit id' do
+    it 'merges the code and return the commit id' 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
@@ -726,13 +734,13 @@ describe Repository, models: true do
     let(:update_image_commit) { repository.commit('2f63565e7aac07bcdadb654e253078b727143ec4') }
 
     context 'when there is a conflict' do
-      it 'should abort the operation' do
+      it 'aborts the operation' do
         expect(repository.revert(user, new_image_commit, 'master')).to eq(false)
       end
     end
 
     context 'when commit was already reverted' do
-      it 'should abort the operation' do
+      it 'aborts the operation' do
         repository.revert(user, update_image_commit, 'master')
 
         expect(repository.revert(user, update_image_commit, 'master')).to eq(false)
@@ -740,13 +748,13 @@ describe Repository, models: true do
     end
 
     context 'when commit can be reverted' do
-      it 'should revert the changes' do
+      it 'reverts the changes' do
         expect(repository.revert(user, update_image_commit, 'master')).to be_truthy
       end
     end
 
     context 'reverting a merge commit' do
-      it 'should revert the changes' do
+      it 'reverts the changes' do
         merge_commit
         expect(repository.blob_at_branch('master', 'files/ruby/feature.rb')).to be_present
 
@@ -762,13 +770,13 @@ describe Repository, models: true do
     let(:pickable_merge) { repository.commit('e56497bb5f03a90a51293fc6d516788730953899') }
 
     context 'when there is a conflict' do
-      it 'should abort the operation' do
+      it 'aborts the operation' do
         expect(repository.cherry_pick(user, conflict_commit, 'master')).to eq(false)
       end
     end
 
     context 'when commit was already cherry-picked' do
-      it 'should abort the operation' do
+      it 'aborts the operation' do
         repository.cherry_pick(user, pickable_commit, 'master')
 
         expect(repository.cherry_pick(user, pickable_commit, 'master')).to eq(false)
@@ -776,13 +784,13 @@ describe Repository, models: true do
     end
 
     context 'when commit can be cherry-picked' do
-      it 'should cherry-pick the changes' do
+      it 'cherry-picks the changes' do
         expect(repository.cherry_pick(user, pickable_commit, 'master')).to be_truthy
       end
     end
 
     context 'cherry-picking a merge commit' do
-      it 'should cherry-pick the changes' do
+      it 'cherry-picks the changes' do
         expect(repository.blob_at_branch('master', 'foo/bar/.gitkeep')).to be_nil
 
         repository.cherry_pick(user, pickable_merge, 'master')
@@ -1154,7 +1162,7 @@ describe Repository, models: true do
       it 'does not flush the cache if the commit does not change any logos' do
         diff = double(:diff, new_path: 'test.txt')
 
-        expect(commit).to receive(:diffs).and_return([diff])
+        expect(commit).to receive(:raw_diffs).and_return([diff])
         expect(cache).not_to receive(:expire)
 
         repository.expire_avatar_cache(repository.root_ref, '123')
@@ -1163,7 +1171,7 @@ describe Repository, models: true do
       it 'flushes the cache if the commit changes any of the logos' do
         diff = double(:diff, new_path: Repository::AVATAR_FILES[0])
 
-        expect(commit).to receive(:diffs).and_return([diff])
+        expect(commit).to receive(:raw_diffs).and_return([diff])
         expect(cache).to receive(:expire).with(:avatar)
 
         repository.expire_avatar_cache(repository.root_ref, '123')
diff --git a/spec/models/service_spec.rb b/spec/models/service_spec.rb
index 67b3783d5145839b1c12694658059b9da99b9622..05056a4bb4759db08fe49ca70605b43ba2c8a1be 100644
--- a/spec/models/service_spec.rb
+++ b/spec/models/service_spec.rb
@@ -65,13 +65,13 @@ describe Service, models: true do
       end
       let(:project) { create(:project) }
 
-      describe 'should be prefilled for projects pushover service' do
+      describe 'is prefilled for projects pushover service' do
         before do
           service_template
           project.build_missing_services
         end
 
-        it "should have all fields prefilled" do
+        it "has all fields prefilled" do
           service = project.pushover_service
           expect(service.template).to eq(false)
           expect(service.device).to eq('MyDevice')
diff --git a/spec/models/user_agent_detail_spec.rb b/spec/models/user_agent_detail_spec.rb
new file mode 100644
index 0000000000000000000000000000000000000000..a8c25766e7368234a77a763f2c5707e929f3413b
--- /dev/null
+++ b/spec/models/user_agent_detail_spec.rb
@@ -0,0 +1,31 @@
+require 'rails_helper'
+
+describe UserAgentDetail, type: :model do
+  describe '.submittable?' do
+    it 'is submittable when not already submitted' do
+      detail = build(:user_agent_detail)
+
+      expect(detail.submittable?).to be_truthy
+    end
+
+    it 'is not submittable when already submitted' do
+      detail = build(:user_agent_detail, submitted: true)
+
+      expect(detail.submittable?).to be_falsey
+    end
+  end
+
+  describe '.valid?' do
+    it 'is valid with a subject' do
+      detail = build(:user_agent_detail)
+
+      expect(detail).to be_valid
+    end
+
+    it 'is invalid without a subject' do
+      detail = build(:user_agent_detail, subject: nil)
+
+      expect(detail).not_to be_valid
+    end
+  end
+end
diff --git a/spec/models/user_spec.rb b/spec/models/user_spec.rb
index 2a5a7fb2fc60eccf2492a38e35a6db8c87b93ae6..51e4780e2b1035b1bd96fee6ddb941a178885bf5 100644
--- a/spec/models/user_spec.rb
+++ b/spec/models/user_spec.rb
@@ -166,7 +166,7 @@ describe User, models: true do
             allow_any_instance_of(ApplicationSetting).to receive(:domain_whitelist).and_return(['*.example.com'])
           end
 
-          it 'should give priority to whitelist and allow info@test.example.com' do
+          it 'gives priority to whitelist and allow info@test.example.com' do
             user = build(:user, email: 'info@test.example.com')
             expect(user).to be_valid
           end
@@ -304,18 +304,18 @@ describe User, models: true do
   end
 
   describe '#generate_password' do
-    it "should execute callback when force_random_password specified" do
+    it "executes callback when force_random_password specified" do
       user = build(:user, force_random_password: true)
       expect(user).to receive(:generate_password)
       user.save
     end
 
-    it "should not generate password by default" do
+    it "does not generate password by default" do
       user = create(:user, password: 'abcdefghe')
       expect(user.password).to eq('abcdefghe')
     end
 
-    it "should generate password when forcing random password" do
+    it "generates password when forcing random password" do
       allow(Devise).to receive(:friendly_token).and_return('123456789')
       user = create(:user, password: 'abcdefg', force_random_password: true)
       expect(user.password).to eq('12345678')
@@ -323,7 +323,7 @@ describe User, models: true do
   end
 
   describe 'authentication token' do
-    it "should have authentication token" do
+    it "has authentication token" do
       user = create(:user)
       expect(user.authentication_token).not_to be_blank
     end
@@ -430,7 +430,7 @@ describe User, models: true do
   describe 'blocking user' do
     let(:user) { create(:user, name: 'John Smith') }
 
-    it "should block user" do
+    it "blocks user" do
       user.block
       expect(user.blocked?).to be_truthy
     end
@@ -501,7 +501,7 @@ describe User, models: true do
     describe 'with defaults' do
       let(:user) { User.new }
 
-      it "should apply defaults to user" do
+      it "applies defaults to user" do
         expect(user.projects_limit).to eq(Gitlab.config.gitlab.default_projects_limit)
         expect(user.can_create_group).to eq(Gitlab.config.gitlab.default_can_create_group)
         expect(user.theme_id).to eq(Gitlab.config.gitlab.default_theme)
@@ -512,7 +512,7 @@ describe User, models: true do
     describe 'with default overrides' do
       let(:user) { User.new(projects_limit: 123, can_create_group: false, can_create_team: true, theme_id: 1) }
 
-      it "should apply defaults to user" do
+      it "applies defaults to user" do
         expect(user.projects_limit).to eq(123)
         expect(user.can_create_group).to be_falsey
         expect(user.theme_id).to eq(1)
@@ -602,7 +602,7 @@ describe User, models: true do
   describe 'by_username_or_id' do
     let(:user1) { create(:user, username: 'foo') }
 
-    it "should get the correct user" do
+    it "gets the correct user" do
       expect(User.by_username_or_id(user1.id)).to eq(user1)
       expect(User.by_username_or_id('foo')).to eq(user1)
       expect(User.by_username_or_id(-1)).to be_nil
@@ -614,7 +614,7 @@ describe User, models: true do
     let(:username) { 'John' }
     let!(:user) { create(:user, username: username) }
 
-    it 'should get the correct user' do
+    it 'gets the correct user' do
       expect(User.by_login(user.email.upcase)).to eq user
       expect(User.by_login(user.email)).to eq user
       expect(User.by_login(username.downcase)).to eq user
@@ -639,23 +639,23 @@ describe User, models: true do
   describe 'all_ssh_keys' do
     it { is_expected.to have_many(:keys).dependent(:destroy) }
 
-    it "should have all ssh keys" do
+    it "has all ssh keys" do
       user = create :user
       key = create :key, key: "ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAABAQD33bWLBxu48Sev9Fert1yzEO4WGcWglWF7K/AwblIUFselOt/QdOL9DSjpQGxLagO1s9wl53STIO8qGS4Ms0EJZyIXOEFMjFJ5xmjSy+S37By4sG7SsltQEHMxtbtFOaW5LV2wCrX+rUsRNqLMamZjgjcPO0/EgGCXIGMAYW4O7cwGZdXWYIhQ1Vwy+CsVMDdPkPgBXqK7nR/ey8KMs8ho5fMNgB5hBw/AL9fNGhRw3QTD6Q12Nkhl4VZES2EsZqlpNnJttnPdp847DUsT6yuLRlfiQfz5Cn9ysHFdXObMN5VYIiPFwHeYCZp1X2S4fDZooRE8uOLTfxWHPXwrhqSH", user_id: user.id
 
-      expect(user.all_ssh_keys).to include(key.key)
+      expect(user.all_ssh_keys).to include(a_string_starting_with(key.key))
     end
   end
 
   describe '#avatar_type' do
     let(:user) { create(:user) }
 
-    it "should be true if avatar is image" do
+    it "is true if avatar is image" do
       user.update_attribute(:avatar, 'uploads/avatar.png')
       expect(user.avatar_type).to be_truthy
     end
 
-    it "should be false if avatar is html page" do
+    it "is false if avatar is html page" do
       user.update_attribute(:avatar, 'uploads/avatar.html')
       expect(user.avatar_type).to eq(["only images allowed"])
     end
@@ -895,7 +895,9 @@ describe User, models: true do
     subject { create(:user) }
     let!(:project1) { create(:project) }
     let!(:project2) { create(:project, forked_from_project: project1) }
-    let!(:push_data) { Gitlab::PushDataBuilder.build_sample(project2, subject) }
+    let!(:push_data) do
+      Gitlab::DataBuilder::Push.build_sample(project2, subject)
+    end
     let!(:push_event) { create(:event, action: Event::PUSHED, project: project2, target: project1, author: subject, data: push_data) }
 
     before do
@@ -955,6 +957,53 @@ describe User, models: true do
     end
   end
 
+  describe '#projects_where_can_admin_issues' do
+    let(:user) { create(:user) }
+
+    it 'includes projects for which the user access level is above or equal to reporter' do
+      create(:project)
+      reporter_project = create(:project)
+      developer_project = create(:project)
+      master_project = create(:project)
+
+      reporter_project.team << [user, :reporter]
+      developer_project.team << [user, :developer]
+      master_project.team << [user, :master]
+
+      expect(user.projects_where_can_admin_issues.to_a).to eq([master_project, developer_project, reporter_project])
+      expect(user.can?(:admin_issue, master_project)).to eq(true)
+      expect(user.can?(:admin_issue, developer_project)).to eq(true)
+      expect(user.can?(:admin_issue, reporter_project)).to eq(true)
+    end
+
+    it 'does not include for which the user access level is below reporter' do
+      project = create(:project)
+      guest_project = create(:project)
+
+      guest_project.team << [user, :guest]
+
+      expect(user.projects_where_can_admin_issues.to_a).to be_empty
+      expect(user.can?(:admin_issue, guest_project)).to eq(false)
+      expect(user.can?(:admin_issue, project)).to eq(false)
+    end
+
+    it 'does not include archived projects' do
+      project = create(:project)
+      project.update_attributes(archived: true)
+
+      expect(user.projects_where_can_admin_issues.to_a).to be_empty
+      expect(user.can?(:admin_issue, project)).to eq(false)
+    end
+
+    it 'does not include projects for which issues are disabled' do
+      project = create(:project)
+      project.update_attributes(issues_enabled: false)
+
+      expect(user.projects_where_can_admin_issues.to_a).to be_empty
+      expect(user.can?(:admin_issue, project)).to eq(false)
+    end
+  end
+
   describe '#ci_authorized_runners' do
     let(:user) { create(:user) }
     let(:runner) { create(:ci_runner) }
diff --git a/spec/models/wiki_page_spec.rb b/spec/models/wiki_page_spec.rb
index ddc49495eda48591d15140256026ca66cdf4c48a..5c34b1b0a30796f4c8042a1ea2dfa482fe949406 100644
--- a/spec/models/wiki_page_spec.rb
+++ b/spec/models/wiki_page_spec.rb
@@ -147,12 +147,12 @@ describe WikiPage, models: true do
       @page = wiki.find_page("Delete Page")
     end
 
-    it "should delete the page" do
+    it "deletes the page" do
       @page.delete
       expect(wiki.pages).to be_empty
     end
 
-    it "should return true" do
+    it "returns true" do
       expect(@page.delete).to eq(true)
     end
   end
@@ -183,7 +183,7 @@ describe WikiPage, models: true do
       destroy_page("Title")
     end
 
-    it "should be replace a hyphen to a space" do
+    it "replaces a hyphen to a space" do
       @page.title = "Import-existing-repositories-into-GitLab"
       expect(@page.title).to eq("Import existing repositories into GitLab")
     end
diff --git a/spec/requests/api/access_requests_spec.rb b/spec/requests/api/access_requests_spec.rb
new file mode 100644
index 0000000000000000000000000000000000000000..d78494b76fac2a281852498c867b575a40a2454e
--- /dev/null
+++ b/spec/requests/api/access_requests_spec.rb
@@ -0,0 +1,246 @@
+require 'spec_helper'
+
+describe API::AccessRequests, api: true  do
+  include ApiHelpers
+
+  let(:master) { create(:user) }
+  let(:developer) { create(:user) }
+  let(:access_requester) { create(:user) }
+  let(:stranger) { create(:user) }
+
+  let(:project) do
+    project = create(:project, :public, creator_id: master.id, namespace: master.namespace)
+    project.team << [developer, :developer]
+    project.team << [master, :master]
+    project.request_access(access_requester)
+    project
+  end
+
+  let(:group) do
+    group = create(:group, :public)
+    group.add_developer(developer)
+    group.add_owner(master)
+    group.request_access(access_requester)
+    group
+  end
+
+  shared_examples 'GET /:sources/:id/access_requests' do |source_type|
+    context "with :sources == #{source_type.pluralize}" do
+      it_behaves_like 'a 404 response when source is private' do
+        let(:route) { get api("/#{source_type.pluralize}/#{source.id}/access_requests", stranger) }
+      end
+
+      context 'when authenticated as a non-master/owner' do
+        %i[developer access_requester stranger].each do |type|
+          context "as a #{type}" do
+            it 'returns 403' do
+              user = public_send(type)
+              get api("/#{source_type.pluralize}/#{source.id}/access_requests", user)
+
+              expect(response).to have_http_status(403)
+            end
+          end
+        end
+      end
+
+      context 'when authenticated as a master/owner' do
+        it 'returns access requesters' do
+          get api("/#{source_type.pluralize}/#{source.id}/access_requests", master)
+
+          expect(response).to have_http_status(200)
+          expect(json_response).to be_an Array
+          expect(json_response.size).to eq(1)
+        end
+      end
+    end
+  end
+
+  shared_examples 'POST /:sources/:id/access_requests' 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}/access_requests", stranger) }
+      end
+
+      context 'when authenticated as a member' do
+        %i[developer master].each do |type|
+          context "as a #{type}" do
+            it 'returns 400' do
+              expect do
+                user = public_send(type)
+                post api("/#{source_type.pluralize}/#{source.id}/access_requests", user)
+
+                expect(response).to have_http_status(400)
+              end.not_to change { source.requesters.count }
+            end
+          end
+        end
+      end
+
+      context 'when authenticated as an access requester' do
+        it 'returns 400' do
+          expect do
+            post api("/#{source_type.pluralize}/#{source.id}/access_requests", access_requester)
+
+            expect(response).to have_http_status(400)
+          end.not_to change { source.requesters.count }
+        end
+      end
+
+      context 'when authenticated as a stranger' do
+        it 'returns 201' do
+          expect do
+            post api("/#{source_type.pluralize}/#{source.id}/access_requests", stranger)
+
+            expect(response).to have_http_status(201)
+          end.to change { source.requesters.count }.by(1)
+
+          # User attributes
+          expect(json_response['id']).to eq(stranger.id)
+          expect(json_response['name']).to eq(stranger.name)
+          expect(json_response['username']).to eq(stranger.username)
+          expect(json_response['state']).to eq(stranger.state)
+          expect(json_response['avatar_url']).to eq(stranger.avatar_url)
+          expect(json_response['web_url']).to eq(Gitlab::Routing.url_helpers.user_url(stranger))
+
+          # Member attributes
+          expect(json_response['requested_at']).to be_present
+        end
+      end
+    end
+  end
+
+  shared_examples 'PUT /:sources/:id/access_requests/:user_id/approve' 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}/access_requests/#{access_requester.id}/approve", stranger) }
+      end
+
+      context 'when authenticated as a non-master/owner' do
+        %i[developer access_requester stranger].each do |type|
+          context "as a #{type}" do
+            it 'returns 403' do
+              user = public_send(type)
+              put api("/#{source_type.pluralize}/#{source.id}/access_requests/#{access_requester.id}/approve", user)
+
+              expect(response).to have_http_status(403)
+            end
+          end
+        end
+      end
+
+      context 'when authenticated as a master/owner' do
+        it 'returns 201' do
+          expect do
+            put api("/#{source_type.pluralize}/#{source.id}/access_requests/#{access_requester.id}/approve", master),
+                access_level: Member::MASTER
+
+            expect(response).to have_http_status(201)
+          end.to change { source.members.count }.by(1)
+          # User attributes
+          expect(json_response['id']).to eq(access_requester.id)
+          expect(json_response['name']).to eq(access_requester.name)
+          expect(json_response['username']).to eq(access_requester.username)
+          expect(json_response['state']).to eq(access_requester.state)
+          expect(json_response['avatar_url']).to eq(access_requester.avatar_url)
+          expect(json_response['web_url']).to eq(Gitlab::Routing.url_helpers.user_url(access_requester))
+
+          # Member attributes
+          expect(json_response['access_level']).to eq(Member::MASTER)
+        end
+
+        context 'user_id does not match an existing access requester' do
+          it 'returns 404' do
+            expect do
+              put api("/#{source_type.pluralize}/#{source.id}/access_requests/#{stranger.id}/approve", master)
+
+              expect(response).to have_http_status(404)
+            end.not_to change { source.members.count }
+          end
+        end
+      end
+    end
+  end
+
+  shared_examples 'DELETE /:sources/:id/access_requests/: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) { delete api("/#{source_type.pluralize}/#{source.id}/access_requests/#{access_requester.id}", stranger) }
+      end
+
+      context 'when authenticated as a non-master/owner' do
+        %i[developer stranger].each do |type|
+          context "as a #{type}" do
+            it 'returns 403' do
+              user = public_send(type)
+              delete api("/#{source_type.pluralize}/#{source.id}/access_requests/#{access_requester.id}", user)
+
+              expect(response).to have_http_status(403)
+            end
+          end
+        end
+      end
+
+      context 'when authenticated as the access requester' do
+        it 'returns 200' do
+          expect do
+            delete api("/#{source_type.pluralize}/#{source.id}/access_requests/#{access_requester.id}", access_requester)
+
+            expect(response).to have_http_status(200)
+          end.to change { source.requesters.count }.by(-1)
+        end
+      end
+
+      context 'when authenticated as a master/owner' do
+        it 'returns 200' do
+          expect do
+            delete api("/#{source_type.pluralize}/#{source.id}/access_requests/#{access_requester.id}", master)
+
+            expect(response).to have_http_status(200)
+          end.to change { source.requesters.count }.by(-1)
+        end
+
+        context 'user_id does not match an existing access requester' do
+          it 'returns 404' do
+            expect do
+              delete api("/#{source_type.pluralize}/#{source.id}/access_requests/#{stranger.id}", master)
+
+              expect(response).to have_http_status(404)
+            end.not_to change { source.requesters.count }
+          end
+        end
+      end
+    end
+  end
+
+  it_behaves_like 'GET /:sources/:id/access_requests', 'project' do
+    let(:source) { project }
+  end
+
+  it_behaves_like 'GET /:sources/:id/access_requests', 'group' do
+    let(:source) { group }
+  end
+
+  it_behaves_like 'POST /:sources/:id/access_requests', 'project' do
+    let(:source) { project }
+  end
+
+  it_behaves_like 'POST /:sources/:id/access_requests', 'group' do
+    let(:source) { group }
+  end
+
+  it_behaves_like 'PUT /:sources/:id/access_requests/:user_id/approve', 'project' do
+    let(:source) { project }
+  end
+
+  it_behaves_like 'PUT /:sources/:id/access_requests/:user_id/approve', 'group' do
+    let(:source) { group }
+  end
+
+  it_behaves_like 'DELETE /:sources/:id/access_requests/:user_id', 'project' do
+    let(:source) { project }
+  end
+
+  it_behaves_like 'DELETE /:sources/:id/access_requests/:user_id', 'group' do
+    let(:source) { group }
+  end
+end
diff --git a/spec/requests/api/api_helpers_spec.rb b/spec/requests/api/api_helpers_spec.rb
index 831889afb6c894fc2b783acb38223e02292aad17..c65510fadecf0dc1aafabb76ac52ce064c58f4f7 100644
--- a/spec/requests/api/api_helpers_spec.rb
+++ b/spec/requests/api/api_helpers_spec.rb
@@ -41,19 +41,19 @@ describe API::Helpers, api: true do
 
   describe ".current_user" do
     describe "when authenticating using a user's private token" do
-      it "should return nil for an invalid token" do
+      it "returns nil for an invalid token" do
         env[API::Helpers::PRIVATE_TOKEN_HEADER] = 'invalid token'
         allow_any_instance_of(self.class).to receive(:doorkeeper_guard){ false }
         expect(current_user).to be_nil
       end
 
-      it "should return nil for a user without access" do
+      it "returns nil for a user without access" do
         env[API::Helpers::PRIVATE_TOKEN_HEADER] = user.private_token
         allow_any_instance_of(Gitlab::UserAccess).to receive(:allowed?).and_return(false)
         expect(current_user).to be_nil
       end
 
-      it "should leave user as is when sudo not specified" do
+      it "leaves user as is when sudo not specified" do
         env[API::Helpers::PRIVATE_TOKEN_HEADER] = user.private_token
         expect(current_user).to eq(user)
         clear_env
@@ -65,19 +65,19 @@ describe API::Helpers, api: true do
     describe "when authenticating using a user's personal access tokens" do
       let(:personal_access_token) { create(:personal_access_token, user: user) }
 
-      it "should return nil for an invalid token" do
+      it "returns nil for an invalid token" do
         env[API::Helpers::PRIVATE_TOKEN_HEADER] = 'invalid token'
         allow_any_instance_of(self.class).to receive(:doorkeeper_guard){ false }
         expect(current_user).to be_nil
       end
 
-      it "should return nil for a user without access" do
+      it "returns nil for a user without access" do
         env[API::Helpers::PRIVATE_TOKEN_HEADER] = personal_access_token.token
         allow_any_instance_of(Gitlab::UserAccess).to receive(:allowed?).and_return(false)
         expect(current_user).to be_nil
       end
 
-      it "should leave user as is when sudo not specified" do
+      it "leaves user as is when sudo not specified" do
         env[API::Helpers::PRIVATE_TOKEN_HEADER] = personal_access_token.token
         expect(current_user).to eq(user)
         clear_env
@@ -100,7 +100,7 @@ describe API::Helpers, api: true do
       end
     end
 
-    it "should change current user to sudo when admin" do
+    it "changes current user to sudo when admin" do
       set_env(admin, user.id)
       expect(current_user).to eq(user)
       set_param(admin, user.id)
@@ -111,7 +111,7 @@ describe API::Helpers, api: true do
       expect(current_user).to eq(user)
     end
 
-    it "should throw an error when the current user is not an admin and attempting to sudo" do
+    it "throws an error when the current user is not an admin and attempting to sudo" do
       set_env(user, admin.id)
       expect { current_user }.to raise_error(Exception)
       set_param(user, admin.id)
@@ -122,7 +122,7 @@ describe API::Helpers, api: true do
       expect { current_user }.to raise_error(Exception)
     end
 
-    it "should throw an error when the user cannot be found for a given id" do
+    it "throws an error when the user cannot be found for a given id" do
       id = user.id + admin.id
       expect(user.id).not_to eq(id)
       expect(admin.id).not_to eq(id)
@@ -133,7 +133,7 @@ describe API::Helpers, api: true do
       expect { current_user }.to raise_error(Exception)
     end
 
-    it "should throw an error when the user cannot be found for a given username" do
+    it "throws an error when the user cannot be found for a given username" do
       username = "#{user.username}#{admin.username}"
       expect(user.username).not_to eq(username)
       expect(admin.username).not_to eq(username)
@@ -144,7 +144,7 @@ describe API::Helpers, api: true do
       expect { current_user }.to raise_error(Exception)
     end
 
-    it "should handle sudo's to oneself" do
+    it "handles sudo's to oneself" do
       set_env(admin, admin.id)
       expect(current_user).to eq(admin)
       set_param(admin, admin.id)
@@ -155,7 +155,7 @@ describe API::Helpers, api: true do
       expect(current_user).to eq(admin)
     end
 
-    it "should handle multiple sudo's to oneself" do
+    it "handles multiple sudo's to oneself" do
       set_env(admin, user.id)
       expect(current_user).to eq(user)
       expect(current_user).to eq(user)
@@ -171,7 +171,7 @@ describe API::Helpers, api: true do
       expect(current_user).to eq(user)
     end
 
-    it "should handle multiple sudo's to oneself using string ids" do
+    it "handles multiple sudo's to oneself using string ids" do
       set_env(admin, user.id.to_s)
       expect(current_user).to eq(user)
       expect(current_user).to eq(user)
@@ -183,7 +183,7 @@ describe API::Helpers, api: true do
   end
 
   describe '.sudo_identifier' do
-    it "should return integers when input is an int" do
+    it "returns integers when input is an int" do
       set_env(admin, '123')
       expect(sudo_identifier).to eq(123)
       set_env(admin, '0001234567890')
@@ -195,7 +195,7 @@ describe API::Helpers, api: true do
       expect(sudo_identifier).to eq(1234567890)
     end
 
-    it "should return string when input is an is not an int" do
+    it "returns string when input is an is not an int" do
       set_env(admin, '12.30')
       expect(sudo_identifier).to eq("12.30")
       set_env(admin, 'hello')
diff --git a/spec/requests/api/award_emoji_spec.rb b/spec/requests/api/award_emoji_spec.rb
index 2b74dd4bbb0c9c8d51cc7e40ba7c637098ce5664..73c268c0d1ef482835ee9f08f1d7cdfb98a58e72 100644
--- a/spec/requests/api/award_emoji_spec.rb
+++ b/spec/requests/api/award_emoji_spec.rb
@@ -22,7 +22,7 @@ describe API::API, api: true  do
         expect(json_response.first['name']).to eq(award_emoji.name)
       end
 
-      it "should return a 404 error when issue id not found" do
+      it "returns a 404 error when issue id not found" do
         get api("/projects/#{project.id}/issues/12345/award_emoji", user)
 
         expect(response).to have_http_status(404)
@@ -124,13 +124,13 @@ describe API::API, api: true  do
         expect(json_response['user']['username']).to eq(user.username)
       end
 
-      it "should return a 400 bad request error if the name is not given" do
+      it "returns a 400 bad request error if the name is not given" do
         post api("/projects/#{project.id}/issues/#{issue.id}/award_emoji", user)
 
         expect(response).to have_http_status(400)
       end
 
-      it "should return a 401 unauthorized error if the user is not authenticated" do
+      it "returns a 401 unauthorized error if the user is not authenticated" do
         post api("/projects/#{project.id}/issues/#{issue.id}/award_emoji"), name: 'thumbsup'
 
         expect(response).to have_http_status(401)
diff --git a/spec/requests/api/branches_spec.rb b/spec/requests/api/branches_spec.rb
index e8fd697965f3c9bdd86be29940ce5c5d44ecca10..3fd989dd7a612ecd2d9773b171f2f240ddf94805 100644
--- a/spec/requests/api/branches_spec.rb
+++ b/spec/requests/api/branches_spec.rb
@@ -13,7 +13,7 @@ describe API::API, api: true  do
   let!(:branch_sha) { '0b4bc9a49b562e85de7cc9e834518ea6828729b9' }
 
   describe "GET /projects/:id/repository/branches" do
-    it "should return an array of project branches" do
+    it "returns an array of project branches" do
       project.repository.expire_cache
 
       get api("/projects/#{project.id}/repository/branches", user)
@@ -25,7 +25,7 @@ describe API::API, api: true  do
   end
 
   describe "GET /projects/:id/repository/branches/:branch" do
-    it "should return the branch information for a single branch" do
+    it "returns the branch information for a single branch" do
       get api("/projects/#{project.id}/repository/branches/#{branch_name}", user)
       expect(response).to have_http_status(200)
 
@@ -36,12 +36,12 @@ describe API::API, api: true  do
       expect(json_response['developers_can_merge']).to eq(false)
     end
 
-    it "should return a 403 error if guest" do
+    it "returns a 403 error if guest" do
       get api("/projects/#{project.id}/repository/branches", user2)
       expect(response).to have_http_status(403)
     end
 
-    it "should return a 404 error if branch is not available" do
+    it "returns a 404 error if branch is not available" do
       get api("/projects/#{project.id}/repository/branches/unknown", user)
       expect(response).to have_http_status(404)
     end
@@ -138,17 +138,17 @@ describe API::API, api: true  do
       end
     end
 
-    it "should return a 404 error if branch not found" do
+    it "returns a 404 error if branch not found" do
       put api("/projects/#{project.id}/repository/branches/unknown/protect", user)
       expect(response).to have_http_status(404)
     end
 
-    it "should return a 403 error if guest" do
+    it "returns a 403 error if guest" do
       put api("/projects/#{project.id}/repository/branches/#{branch_name}/protect", user2)
       expect(response).to have_http_status(403)
     end
 
-    it "should return success when protect branch again" do
+    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)
@@ -156,7 +156,7 @@ describe API::API, api: true  do
   end
 
   describe "PUT /projects/:id/repository/branches/:branch/unprotect" do
-    it "should unprotect a single branch" do
+    it "unprotects a single branch" do
       put api("/projects/#{project.id}/repository/branches/#{branch_name}/unprotect", user)
       expect(response).to have_http_status(200)
 
@@ -165,12 +165,12 @@ describe API::API, api: true  do
       expect(json_response['protected']).to eq(false)
     end
 
-    it "should return success when unprotect branch" do
+    it "returns success when unprotect branch" do
       put api("/projects/#{project.id}/repository/branches/unknown/unprotect", user)
       expect(response).to have_http_status(404)
     end
 
-    it "should return success when unprotect branch again" do
+    it "returns success when unprotect branch again" do
       put api("/projects/#{project.id}/repository/branches/#{branch_name}/unprotect", user)
       put api("/projects/#{project.id}/repository/branches/#{branch_name}/unprotect", user)
       expect(response).to have_http_status(200)
@@ -178,7 +178,7 @@ describe API::API, api: true  do
   end
 
   describe "POST /projects/:id/repository/branches" do
-    it "should create a new branch" do
+    it "creates a new branch" do
       post api("/projects/#{project.id}/repository/branches", user),
            branch_name: 'feature1',
            ref: branch_sha
@@ -189,14 +189,14 @@ describe API::API, api: true  do
       expect(json_response['commit']['id']).to eq(branch_sha)
     end
 
-    it "should deny for user without push access" do
+    it "denies for user without push access" do
       post api("/projects/#{project.id}/repository/branches", user2),
            branch_name: branch_name,
            ref: branch_sha
       expect(response).to have_http_status(403)
     end
 
-    it 'should return 400 if branch name is invalid' do
+    it 'returns 400 if branch name is invalid' do
       post api("/projects/#{project.id}/repository/branches", user),
            branch_name: 'new design',
            ref: branch_sha
@@ -204,7 +204,7 @@ describe API::API, api: true  do
       expect(json_response['message']).to eq('Branch name is invalid')
     end
 
-    it 'should return 400 if branch already exists' do
+    it 'returns 400 if branch already exists' do
       post api("/projects/#{project.id}/repository/branches", user),
            branch_name: 'new_design1',
            ref: branch_sha
@@ -217,7 +217,7 @@ describe API::API, api: true  do
       expect(json_response['message']).to eq('Branch already exists')
     end
 
-    it 'should return 400 if ref name is invalid' do
+    it 'returns 400 if ref name is invalid' do
       post api("/projects/#{project.id}/repository/branches", user),
            branch_name: 'new_design3',
            ref: 'foo'
@@ -231,25 +231,25 @@ describe API::API, api: true  do
       allow_any_instance_of(Repository).to receive(:rm_branch).and_return(true)
     end
 
-    it "should remove branch" do
+    it "removes branch" do
       delete api("/projects/#{project.id}/repository/branches/#{branch_name}", user)
       expect(response).to have_http_status(200)
       expect(json_response['branch_name']).to eq(branch_name)
     end
 
-    it 'should return 404 if branch not exists' do
+    it 'returns 404 if branch not exists' do
       delete api("/projects/#{project.id}/repository/branches/foobar", user)
       expect(response).to have_http_status(404)
     end
 
-    it "should remove protected branch" do
-      project.protected_branches.create(name: branch_name)
+    it "removes protected branch" do
+      create(:protected_branch, project: project, name: branch_name)
       delete api("/projects/#{project.id}/repository/branches/#{branch_name}", user)
       expect(response).to have_http_status(405)
       expect(json_response['message']).to eq('Protected branch cant be removed')
     end
 
-    it "should not remove HEAD branch" do
+    it "does not remove HEAD branch" do
       delete api("/projects/#{project.id}/repository/branches/master", user)
       expect(response).to have_http_status(405)
       expect(json_response['message']).to eq('Cannot remove HEAD branch')
diff --git a/spec/requests/api/builds_spec.rb b/spec/requests/api/builds_spec.rb
index 86a7b242fbe2ec143ca1742a670bf532b88f13b9..9a17a705b1e18af4a91abb3c7c7dc161824949fd 100644
--- a/spec/requests/api/builds_spec.rb
+++ b/spec/requests/api/builds_spec.rb
@@ -9,7 +9,7 @@ describe API::API, api: true do
   let!(:developer) { create(:project_member, :developer, user: user, project: project) }
   let(:reporter) { create(:project_member, :reporter, project: project) }
   let(:guest) { create(:project_member, :guest, project: project) }
-  let!(:pipeline) { create(:ci_pipeline, project: project, sha: project.commit.id, ref: project.default_branch) }
+  let!(:pipeline) { create(:ci_empty_pipeline, project: project, sha: project.commit.id, ref: project.default_branch) }
   let!(:build) { create(:ci_build, pipeline: pipeline) }
 
   describe 'GET /projects/:id/builds ' do
@@ -18,7 +18,7 @@ describe API::API, api: true do
     before { get api("/projects/#{project.id}/builds?#{query}", api_user) }
 
     context 'authorized user' do
-      it 'should return project builds' do
+      it 'returns project builds' do
         expect(response).to have_http_status(200)
         expect(json_response).to be_an Array
       end
@@ -84,7 +84,7 @@ describe API::API, api: true do
             get api("/projects/#{project.id}/repository/commits/#{project.commit.id}/builds", api_user)
           end
 
-          it 'should return project builds for specific commit' do
+          it 'returns project builds for specific commit' do
             expect(response).to have_http_status(200)
             expect(json_response).to be_an Array
             expect(json_response.size).to eq 2
@@ -113,7 +113,7 @@ describe API::API, api: true do
           get api("/projects/#{project.id}/repository/commits/#{project.commit.id}/builds", nil)
         end
 
-        it 'should not return project builds' do
+        it 'does not return project builds' do
           expect(response).to have_http_status(401)
           expect(json_response.except('message')).to be_empty
         end
@@ -125,7 +125,7 @@ describe API::API, api: true do
     before { get api("/projects/#{project.id}/builds/#{build.id}", api_user) }
 
     context 'authorized user' do
-      it 'should return specific build data' do
+      it 'returns specific build data' do
         expect(response).to have_http_status(200)
         expect(json_response['name']).to eq('test')
       end
@@ -134,7 +134,7 @@ describe API::API, api: true do
     context 'unauthorized user' do
       let(:api_user) { nil }
 
-      it 'should not return specific build data' do
+      it 'does not return specific build data' do
         expect(response).to have_http_status(401)
       end
     end
@@ -152,7 +152,7 @@ describe API::API, api: true do
             'Content-Disposition' => 'attachment; filename=ci_build_artifacts.zip' }
         end
 
-        it 'should return specific build artifacts' do
+        it 'returns specific build artifacts' do
           expect(response).to have_http_status(200)
           expect(response.headers).to include(download_headers)
         end
@@ -161,20 +161,24 @@ describe API::API, api: true do
       context 'unauthorized user' do
         let(:api_user) { nil }
 
-        it 'should not return specific build artifacts' do
+        it 'does not return specific build artifacts' do
           expect(response).to have_http_status(401)
         end
       end
     end
 
-    it 'should not return build artifacts if not uploaded' do
+    it 'does not return build artifacts if not uploaded' do
       expect(response).to have_http_status(404)
     end
   end
 
   describe 'GET /projects/:id/artifacts/:ref_name/download?job=name' do
     let(:api_user) { reporter.user }
-    let(:build) { create(:ci_build, :success, :artifacts, pipeline: pipeline) }
+    let(:build) { create(:ci_build, :artifacts, pipeline: pipeline) }
+
+    before do
+      build.success
+    end
 
     def path_for_ref(ref = pipeline.ref, job = build.name)
       api("/projects/#{project.id}/builds/artifacts/#{ref}/download?job=#{job}", api_user)
@@ -272,7 +276,7 @@ describe API::API, api: true do
     end
 
     context 'authorized user' do
-      it 'should return specific build trace' do
+      it 'returns specific build trace' do
         expect(response).to have_http_status(200)
         expect(response.body).to eq(build.trace)
       end
@@ -281,7 +285,7 @@ describe API::API, api: true do
     context 'unauthorized user' do
       let(:api_user) { nil }
 
-      it 'should not return specific build trace' do
+      it 'does not return specific build trace' do
         expect(response).to have_http_status(401)
       end
     end
@@ -292,7 +296,7 @@ describe API::API, api: true do
 
     context 'authorized user' do
       context 'user with :update_build persmission' do
-        it 'should cancel running or pending build' do
+        it 'cancels running or pending build' do
           expect(response).to have_http_status(201)
           expect(project.builds.first.status).to eq('canceled')
         end
@@ -301,7 +305,7 @@ describe API::API, api: true do
       context 'user without :update_build permission' do
         let(:api_user) { reporter.user }
 
-        it 'should not cancel build' do
+        it 'does not cancel build' do
           expect(response).to have_http_status(403)
         end
       end
@@ -310,7 +314,7 @@ describe API::API, api: true do
     context 'unauthorized user' do
       let(:api_user) { nil }
 
-      it 'should not cancel build' do
+      it 'does not cancel build' do
         expect(response).to have_http_status(401)
       end
     end
@@ -323,7 +327,7 @@ describe API::API, api: true do
 
     context 'authorized user' do
       context 'user with :update_build permission' do
-        it 'should retry non-running build' do
+        it 'retries non-running build' do
           expect(response).to have_http_status(201)
           expect(project.builds.first.status).to eq('canceled')
           expect(json_response['status']).to eq('pending')
@@ -333,7 +337,7 @@ describe API::API, api: true do
       context 'user without :update_build permission' do
         let(:api_user) { reporter.user }
 
-        it 'should not retry build' do
+        it 'does not retry build' do
           expect(response).to have_http_status(403)
         end
       end
@@ -342,7 +346,7 @@ describe API::API, api: true do
     context 'unauthorized user' do
       let(:api_user) { nil }
 
-      it 'should not retry build' do
+      it 'does not retry build' do
         expect(response).to have_http_status(401)
       end
     end
@@ -356,14 +360,14 @@ describe API::API, api: true do
     context 'build is erasable' do
       let(:build) { create(:ci_build, :trace, :artifacts, :success, project: project, pipeline: pipeline) }
 
-      it 'should erase build content' do
+      it 'erases build content' do
         expect(response.status).to eq 201
         expect(build.trace).to be_empty
         expect(build.artifacts_file.exists?).to be_falsy
         expect(build.artifacts_metadata.exists?).to be_falsy
       end
 
-      it 'should update build' do
+      it 'updates build' do
         expect(build.reload.erased_at).to be_truthy
         expect(build.reload.erased_by).to eq user
       end
@@ -372,7 +376,7 @@ describe API::API, api: true do
     context 'build is not erasable' do
       let(:build) { create(:ci_build, :trace, project: project, pipeline: pipeline) }
 
-      it 'should respond with forbidden' do
+      it 'responds with forbidden' do
         expect(response.status).to eq 403
       end
     end
@@ -403,4 +407,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 2da01da7fa1201f36bdf66d5f8a5ebbe221c8796..2d6093fec7a467eb9b7545729ef348cefae68e25 100644
--- a/spec/requests/api/commit_statuses_spec.rb
+++ b/spec/requests/api/commit_statuses_spec.rb
@@ -99,7 +99,7 @@ describe API::CommitStatuses, api: true do
     context "guest user" do
       before { get api(get_url, guest) }
 
-      it "should not return project commits" do
+      it "does not return project commits" do
         expect(response).to have_http_status(403)
       end
     end
@@ -107,7 +107,7 @@ describe API::CommitStatuses, api: true do
     context "unauthorized user" do
       before { get api(get_url) }
 
-      it "should not return project commits" do
+      it "does not return project commits" do
         expect(response).to have_http_status(401)
       end
     end
@@ -179,7 +179,7 @@ describe API::CommitStatuses, api: true do
     context 'reporter user' do
       before { post api(post_url, reporter) }
 
-      it 'should not create commit status' do
+      it 'does not create commit status' do
         expect(response).to have_http_status(403)
       end
     end
@@ -187,7 +187,7 @@ describe API::CommitStatuses, api: true do
     context 'guest user' do
       before { post api(post_url, guest) }
 
-      it 'should not create commit status' do
+      it 'does not create commit status' do
         expect(response).to have_http_status(403)
       end
     end
@@ -195,7 +195,7 @@ describe API::CommitStatuses, api: true do
     context 'unauthorized user' do
       before { post api(post_url) }
 
-      it 'should not create commit status' do
+      it 'does not create commit status' do
         expect(response).to have_http_status(401)
       end
     end
diff --git a/spec/requests/api/commits_spec.rb b/spec/requests/api/commits_spec.rb
index e4ea850659899364107030693a126a1321bfb96f..7ca75d776733343d0988c00cb64f5c190c3042af 100644
--- a/spec/requests/api/commits_spec.rb
+++ b/spec/requests/api/commits_spec.rb
@@ -17,7 +17,7 @@ describe API::API, api: true  do
     context "authorized user" do
       before { project.team << [user2, :reporter] }
 
-      it "should return project commits" do
+      it "returns project commits" do
         get api("/projects/#{project.id}/repository/commits", user)
         expect(response).to have_http_status(200)
 
@@ -27,14 +27,14 @@ describe API::API, api: true  do
     end
 
     context "unauthorized user" do
-      it "should not return project commits" do
+      it "does not return project commits" do
         get api("/projects/#{project.id}/repository/commits")
         expect(response).to have_http_status(401)
       end
     end
 
     context "since optional parameter" do
-      it "should return project commits since provided parameter" do
+      it "returns project commits since provided parameter" do
         commits = project.repository.commits("master")
         since = commits.second.created_at
 
@@ -47,7 +47,7 @@ describe API::API, api: true  do
     end
 
     context "until optional parameter" do
-      it "should return project commits until provided parameter" do
+      it "returns project commits until provided parameter" do
         commits = project.repository.commits("master")
         before = commits.second.created_at
 
@@ -60,7 +60,7 @@ describe API::API, api: true  do
     end
 
     context "invalid xmlschema date parameters" do
-      it "should return an invalid parameter error message" do
+      it "returns an invalid parameter error message" do
         get api("/projects/#{project.id}/repository/commits?since=invalid-date", user)
 
         expect(response).to have_http_status(400)
@@ -71,7 +71,7 @@ describe API::API, api: true  do
 
   describe "GET /projects:id/repository/commits/:sha" do
     context "authorized user" do
-      it "should return a commit by sha" do
+      it "returns a commit by sha" do
         get api("/projects/#{project.id}/repository/commits/#{project.repository.commit.id}", user)
 
         expect(response).to have_http_status(200)
@@ -82,27 +82,40 @@ describe API::API, api: true  do
         expect(json_response['stats']['total']).to eq(project.repository.commit.stats.total)
       end
 
-      it "should return a 404 error if not found" do
+      it "returns a 404 error if not found" do
         get api("/projects/#{project.id}/repository/commits/invalid_sha", user)
         expect(response).to have_http_status(404)
       end
 
-      it "should return nil for commit without CI" do
+      it "returns nil for commit without CI" do
         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
       end
 
-      it "should return status for CI" do
+      it "returns status for CI" do
         pipeline = project.ensure_pipeline(project.repository.commit.sha, 'master')
+        pipeline.update(status: 'success')
+
         get api("/projects/#{project.id}/repository/commits/#{project.repository.commit.id}", user)
+
         expect(response).to have_http_status(200)
         expect(json_response['status']).to eq(pipeline.status)
       end
+
+      it "returns status for CI when pipeline is created" do
+        project.ensure_pipeline(project.repository.commit.sha, 'master')
+
+        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
+      end
     end
 
     context "unauthorized user" do
-      it "should not return the selected commit" do
+      it "does not return the selected commit" do
         get api("/projects/#{project.id}/repository/commits/#{project.repository.commit.id}")
         expect(response).to have_http_status(401)
       end
@@ -113,7 +126,7 @@ describe API::API, api: true  do
     context "authorized user" do
       before { project.team << [user2, :reporter] }
 
-      it "should return the diff of the selected commit" do
+      it "returns the diff of the selected commit" do
         get api("/projects/#{project.id}/repository/commits/#{project.repository.commit.id}/diff", user)
         expect(response).to have_http_status(200)
 
@@ -122,14 +135,14 @@ describe API::API, api: true  do
         expect(json_response.first.keys).to include "diff"
       end
 
-      it "should return a 404 error if invalid commit" do
+      it "returns a 404 error if invalid commit" do
         get api("/projects/#{project.id}/repository/commits/invalid_sha/diff", user)
         expect(response).to have_http_status(404)
       end
     end
 
     context "unauthorized user" do
-      it "should not return the diff of the selected commit" do
+      it "does not return the diff of the selected commit" do
         get api("/projects/#{project.id}/repository/commits/#{project.repository.commit.id}/diff")
         expect(response).to have_http_status(401)
       end
@@ -138,7 +151,7 @@ describe API::API, api: true  do
 
   describe 'GET /projects:id/repository/commits/:sha/comments' do
     context 'authorized user' do
-      it 'should return merge_request comments' do
+      it 'returns merge_request comments' do
         get api("/projects/#{project.id}/repository/commits/#{project.repository.commit.id}/comments", user)
         expect(response).to have_http_status(200)
         expect(json_response).to be_an Array
@@ -147,14 +160,14 @@ describe API::API, api: true  do
         expect(json_response.first['author']['id']).to eq(user.id)
       end
 
-      it 'should return a 404 error if merge_request_id not found' do
+      it 'returns a 404 error if merge_request_id not found' do
         get api("/projects/#{project.id}/repository/commits/1234ab/comments", user)
         expect(response).to have_http_status(404)
       end
     end
 
     context 'unauthorized user' do
-      it 'should not return the diff of the selected commit' do
+      it 'does not return the diff of the selected commit' do
         get api("/projects/#{project.id}/repository/commits/1234ab/comments")
         expect(response).to have_http_status(401)
       end
@@ -163,7 +176,7 @@ describe API::API, api: true  do
 
   describe 'POST /projects:id/repository/commits/:sha/comments' do
     context 'authorized user' do
-      it 'should return comment' do
+      it 'returns comment' do
         post api("/projects/#{project.id}/repository/commits/#{project.repository.commit.id}/comments", user), note: 'My comment'
         expect(response).to have_http_status(201)
         expect(json_response['note']).to eq('My comment')
@@ -172,28 +185,28 @@ describe API::API, api: true  do
         expect(json_response['line_type']).to be_nil
       end
 
-      it 'should return the inline comment' do
-        post api("/projects/#{project.id}/repository/commits/#{project.repository.commit.id}/comments", user), note: 'My comment', path: project.repository.commit.diffs.first.new_path, line: 7, line_type: 'new'
+      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'
         expect(response).to have_http_status(201)
         expect(json_response['note']).to eq('My comment')
-        expect(json_response['path']).to eq(project.repository.commit.diffs.first.new_path)
+        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_type']).to eq('new')
       end
 
-      it 'should return 400 if note is missing' do
+      it 'returns 400 if note is missing' do
         post api("/projects/#{project.id}/repository/commits/#{project.repository.commit.id}/comments", user)
         expect(response).to have_http_status(400)
       end
 
-      it 'should return 404 if note is attached to non existent commit' do
+      it 'returns 404 if note is attached to non existent commit' do
         post api("/projects/#{project.id}/repository/commits/1234ab/comments", user), note: 'My comment'
         expect(response).to have_http_status(404)
       end
     end
 
     context 'unauthorized user' do
-      it 'should not return the diff of the selected commit' do
+      it 'does not return the diff of the selected commit' do
         post api("/projects/#{project.id}/repository/commits/#{project.repository.commit.id}/comments")
         expect(response).to have_http_status(401)
       end
diff --git a/spec/requests/api/deploy_keys.rb b/spec/requests/api/deploy_keys.rb
deleted file mode 100644
index ac42288bc3493d28e3ea21c3ac4dcfcdc9f7d37c..0000000000000000000000000000000000000000
--- a/spec/requests/api/deploy_keys.rb
+++ /dev/null
@@ -1,38 +0,0 @@
-require 'spec_helper'
-
-describe API::API, api: true  do
-  include ApiHelpers
-
-  let(:user)  { create(:user) }
-  let(:project) { create(:project, creator_id: user.id) }
-  let!(:deploy_keys_project) { create(:deploy_keys_project, project: project) }
-  let(:admin) { create(:admin) }
-
-  describe 'GET /deploy_keys' do
-    before { admin }
-
-    context 'when unauthenticated' do
-      it 'should return authentication error' do
-        get api('/deploy_keys')
-        expect(response.status).to eq(401)
-      end
-    end
-
-    context 'when authenticated as non-admin user' do
-      it 'should return a 403 error' do
-        get api('/deploy_keys', user)
-        expect(response.status).to eq(403)
-      end
-    end
-
-    context 'when authenticated as admin' do
-      it 'should return all deploy keys' do
-        get api('/deploy_keys', admin)
-        expect(response.status).to eq(200)
-
-        expect(json_response).to be_an Array
-        expect(json_response.first['id']).to eq(deploy_keys_project.deploy_key.id)
-      end
-    end
-  end
-end
diff --git a/spec/requests/api/deploy_keys_spec.rb b/spec/requests/api/deploy_keys_spec.rb
new file mode 100644
index 0000000000000000000000000000000000000000..7d8cc45327c73f339fb10eae5686f5f9dec652dd
--- /dev/null
+++ b/spec/requests/api/deploy_keys_spec.rb
@@ -0,0 +1,160 @@
+require 'spec_helper'
+
+describe API::API, api: true  do
+  include ApiHelpers
+
+  let(:user)        { create(:user) }
+  let(:admin)       { create(:admin) }
+  let(:project)     { create(:project, creator_id: user.id) }
+  let(:deploy_key)  { create(:deploy_key, public: true) }
+
+  let!(:deploy_keys_project) do
+    create(:deploy_keys_project, project: project, deploy_key: deploy_key)
+  end
+
+  describe 'GET /deploy_keys' do
+    context 'when unauthenticated' do
+      it 'should return authentication error' do
+        get api('/deploy_keys')
+
+        expect(response.status).to eq(401)
+      end
+    end
+
+    context 'when authenticated as non-admin user' do
+      it 'should return a 403 error' do
+        get api('/deploy_keys', user)
+
+        expect(response.status).to eq(403)
+      end
+    end
+
+    context 'when authenticated as admin' do
+      it 'should return all deploy keys' do
+        get api('/deploy_keys', admin)
+
+        expect(response.status).to eq(200)
+        expect(json_response).to be_an Array
+        expect(json_response.first['id']).to eq(deploy_keys_project.deploy_key.id)
+      end
+    end
+  end
+
+  describe 'GET /projects/:id/deploy_keys' do
+    before { deploy_key }
+
+    it 'should return array of ssh keys' do
+      get api("/projects/#{project.id}/deploy_keys", admin)
+
+      expect(response).to have_http_status(200)
+      expect(json_response).to be_an Array
+      expect(json_response.first['title']).to eq(deploy_key.title)
+    end
+  end
+
+  describe 'GET /projects/:id/deploy_keys/:key_id' do
+    it 'should return a single key' do
+      get api("/projects/#{project.id}/deploy_keys/#{deploy_key.id}", admin)
+
+      expect(response).to have_http_status(200)
+      expect(json_response['title']).to eq(deploy_key.title)
+    end
+
+    it 'should return 404 Not Found with invalid ID' do
+      get api("/projects/#{project.id}/deploy_keys/404", admin)
+
+      expect(response).to have_http_status(404)
+    end
+  end
+
+  describe 'POST /projects/:id/deploy_keys' do
+    it 'should not create an invalid ssh key' do
+      post api("/projects/#{project.id}/deploy_keys", admin), { title: 'invalid key' }
+
+      expect(response).to have_http_status(400)
+      expect(json_response['message']['key']).to eq([
+        'can\'t be blank',
+        'is too short (minimum is 0 characters)',
+        'is invalid'
+      ])
+    end
+
+    it 'should not create a key without title' do
+      post api("/projects/#{project.id}/deploy_keys", admin), key: 'some key'
+
+      expect(response).to have_http_status(400)
+      expect(json_response['message']['title']).to eq([
+        'can\'t be blank',
+        'is too short (minimum is 0 characters)'
+      ])
+    end
+
+    it 'should create new ssh key' do
+      key_attrs = attributes_for :another_key
+
+      expect do
+        post api("/projects/#{project.id}/deploy_keys", admin), key_attrs
+      end.to change{ project.deploy_keys.count }.by(1)
+    end
+  end
+
+  describe 'DELETE /projects/:id/deploy_keys/:key_id' do
+    before { deploy_key }
+
+    it 'should delete existing key' do
+      expect do
+        delete api("/projects/#{project.id}/deploy_keys/#{deploy_key.id}", admin)
+      end.to change{ project.deploy_keys.count }.by(-1)
+    end
+
+    it 'should return 404 Not Found with invalid ID' do
+      delete api("/projects/#{project.id}/deploy_keys/404", admin)
+
+      expect(response).to have_http_status(404)
+    end
+  end
+
+  describe 'POST /projects/:id/deploy_keys/:key_id/enable' do
+    let(:project2) { create(:empty_project) }
+
+    context 'when the user can admin the project' do
+      it 'enables the key' do
+        expect do
+          post api("/projects/#{project2.id}/deploy_keys/#{deploy_key.id}/enable", admin)
+        end.to change { project2.deploy_keys.count }.from(0).to(1)
+
+        expect(response).to have_http_status(201)
+        expect(json_response['id']).to eq(deploy_key.id)
+      end
+    end
+
+    context 'when authenticated as non-admin user' do
+      it 'should return a 404 error' do
+        post api("/projects/#{project2.id}/deploy_keys/#{deploy_key.id}/enable", user)
+
+        expect(response).to have_http_status(404)
+      end
+    end
+  end
+
+  describe 'DELETE /projects/:id/deploy_keys/:key_id/disable' do
+    context 'when the user can admin the project' do
+      it 'disables the key' do
+        expect do
+          delete api("/projects/#{project.id}/deploy_keys/#{deploy_key.id}/disable", admin)
+        end.to change { project.deploy_keys.count }.from(1).to(0)
+
+        expect(response).to have_http_status(200)
+        expect(json_response['id']).to eq(deploy_key.id)
+      end
+    end
+
+    context 'when authenticated as non-admin user' do
+      it 'should return a 404 error' do
+        delete api("/projects/#{project.id}/deploy_keys/#{deploy_key.id}/disable", user)
+
+        expect(response).to have_http_status(404)
+      end
+    end
+  end
+end
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 2e5448143d5ad16722b39907f64cb555229eab0f..2d1213df8a7b42b9cc6ab62cc4afd630c237fcbe 100644
--- a/spec/requests/api/files_spec.rb
+++ b/spec/requests/api/files_spec.rb
@@ -9,7 +9,7 @@ describe API::API, api: true  do
   before { project.team << [user, :developer] }
 
   describe "GET /projects/:id/repository/files" do
-    it "should return file info" do
+    it "returns file info" do
       params = {
         file_path: file_path,
         ref: 'master',
@@ -23,12 +23,12 @@ describe API::API, api: true  do
       expect(Base64.decode64(json_response['content']).lines.first).to eq("require 'fileutils'\n")
     end
 
-    it "should return a 400 bad request if no params given" 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
 
-    it "should return a 404 if such file does not exist" do
+    it "returns a 404 if such file does not exist" do
       params = {
         file_path: 'app/models/application.rb',
         ref: 'master',
@@ -49,18 +49,18 @@ describe API::API, api: true  do
       }
     end
 
-    it "should create a new file in project repo" 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')
     end
 
-    it "should return a 400 bad request if no params given" do
+    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
 
-    it "should return a 400 if editor fails to create file" do
+    it "returns a 400 if editor fails to create file" do
       allow_any_instance_of(Repository).to receive(:commit_file).
         and_return(false)
 
@@ -79,13 +79,13 @@ describe API::API, api: true  do
       }
     end
 
-    it "should update existing file in project repo" 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)
     end
 
-    it "should return a 400 bad request if no params given" do
+    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
@@ -100,18 +100,18 @@ describe API::API, api: true  do
       }
     end
 
-    it "should delete existing file in project repo" 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)
     end
 
-    it "should return a 400 bad request if no params given" do
+    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
 
-    it "should return a 400 if fails to create file" do
+    it "returns a 400 if fails to create file" do
       allow_any_instance_of(Repository).to receive(:remove_file).and_return(false)
 
       delete api("/projects/#{project.id}/repository/files", user), valid_params
diff --git a/spec/requests/api/fork_spec.rb b/spec/requests/api/fork_spec.rb
index a9f5aa924b71a7495f22009fade6fd23941d0299..f802fcd2d2e590607baabd4a40d7b143e9780e9f 100644
--- a/spec/requests/api/fork_spec.rb
+++ b/spec/requests/api/fork_spec.rb
@@ -20,7 +20,7 @@ describe API::API, api: true  do
     before { user3 }
 
     context 'when authenticated' do
-      it 'should fork if user has sufficient access to project' 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)
@@ -30,7 +30,7 @@ describe API::API, api: true  do
         expect(json_response['forked_from_project']['id']).to eq(project.id)
       end
 
-      it 'should fork if user is admin' 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)
@@ -40,20 +40,20 @@ describe API::API, api: true  do
         expect(json_response['forked_from_project']['id']).to eq(project.id)
       end
 
-      it 'should fail on missing project access for the project to fork' 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 'should fail if forked project exists in the user namespace' do
+      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'])
       end
 
-      it 'should fail if project to fork from does not exist' 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')
@@ -61,7 +61,7 @@ describe API::API, api: true  do
     end
 
     context 'when unauthenticated' do
-      it 'should return authentication error' 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')
diff --git a/spec/requests/api/group_members_spec.rb b/spec/requests/api/group_members_spec.rb
deleted file mode 100644
index 52f9e7d4681cc71b796c6b33db658a31c5b5693c..0000000000000000000000000000000000000000
--- a/spec/requests/api/group_members_spec.rb
+++ /dev/null
@@ -1,199 +0,0 @@
-require 'spec_helper'
-
-describe API::API, api: true  do
-  include ApiHelpers
-
-  let(:owner) { create(:user) }
-  let(:reporter) { create(:user) }
-  let(:developer) { create(:user) }
-  let(:master) { create(:user) }
-  let(:guest) { create(:user) }
-  let(:stranger) { create(:user) }
-
-  let!(:group_with_members) do
-    group = create(:group, :private)
-    group.add_users([reporter.id], GroupMember::REPORTER)
-    group.add_users([developer.id], GroupMember::DEVELOPER)
-    group.add_users([master.id], GroupMember::MASTER)
-    group.add_users([guest.id], GroupMember::GUEST)
-    group
-  end
-
-  let!(:group_no_members) { create(:group) }
-
-  before do
-    group_with_members.add_owner owner
-    group_no_members.add_owner owner
-  end
-
-  describe "GET /groups/:id/members" do
-    context "when authenticated as user that is part or the group" do
-      it "each user: should return an array of members groups of group3" do
-        [owner, master, developer, reporter, guest].each do |user|
-          get api("/groups/#{group_with_members.id}/members", user)
-          expect(response).to have_http_status(200)
-          expect(json_response).to be_an Array
-          expect(json_response.size).to eq(5)
-          expect(json_response.find { |e| e['id'] == owner.id }['access_level']).to eq(GroupMember::OWNER)
-          expect(json_response.find { |e| e['id'] == reporter.id }['access_level']).to eq(GroupMember::REPORTER)
-          expect(json_response.find { |e| e['id'] == developer.id }['access_level']).to eq(GroupMember::DEVELOPER)
-          expect(json_response.find { |e| e['id'] == master.id }['access_level']).to eq(GroupMember::MASTER)
-          expect(json_response.find { |e| e['id'] == guest.id }['access_level']).to eq(GroupMember::GUEST)
-        end
-      end
-
-      it 'users not part of the group should get access error' do
-        get api("/groups/#{group_with_members.id}/members", stranger)
-
-        expect(response).to have_http_status(404)
-      end
-    end
-  end
-
-  describe "POST /groups/:id/members" do
-    context "when not a member of the group" do
-      it "should not add guest as member of group_no_members when adding being done by person outside the group" do
-        post api("/groups/#{group_no_members.id}/members", reporter), user_id: guest.id, access_level: GroupMember::MASTER
-        expect(response).to have_http_status(403)
-      end
-    end
-
-    context "when a member of the group" do
-      it "should return ok and add new member" do
-        new_user = create(:user)
-
-        expect do
-          post api("/groups/#{group_no_members.id}/members", owner), user_id: new_user.id, access_level: GroupMember::MASTER
-        end.to change { group_no_members.members.count }.by(1)
-
-        expect(response).to have_http_status(201)
-        expect(json_response['name']).to eq(new_user.name)
-        expect(json_response['access_level']).to eq(GroupMember::MASTER)
-      end
-
-      it "should not allow guest to modify group members" do
-        new_user = create(:user)
-
-        expect do
-          post api("/groups/#{group_with_members.id}/members", guest), user_id: new_user.id, access_level: GroupMember::MASTER
-        end.not_to change { group_with_members.members.count }
-
-        expect(response).to have_http_status(403)
-      end
-
-      it "should return error if member already exists" do
-        post api("/groups/#{group_with_members.id}/members", owner), user_id: master.id, access_level: GroupMember::MASTER
-        expect(response).to have_http_status(409)
-      end
-
-      it "should return a 400 error when user id is not given" do
-        post api("/groups/#{group_no_members.id}/members", owner), access_level: GroupMember::MASTER
-        expect(response).to have_http_status(400)
-      end
-
-      it "should return a 400 error when access level is not given" do
-        post api("/groups/#{group_no_members.id}/members", owner), user_id: master.id
-        expect(response).to have_http_status(400)
-      end
-
-      it "should return a 422 error when access level is not known" do
-        post api("/groups/#{group_no_members.id}/members", owner), user_id: master.id, access_level: 1234
-        expect(response).to have_http_status(422)
-      end
-    end
-  end
-
-  describe 'PUT /groups/:id/members/:user_id' do
-    context 'when not a member of the group' do
-      it 'should return a 409 error if the user is not a group member' do
-        put(
-          api("/groups/#{group_no_members.id}/members/#{developer.id}",
-              owner), access_level: GroupMember::MASTER
-        )
-        expect(response).to have_http_status(404)
-      end
-    end
-
-    context 'when a member of the group' do
-      it 'should return ok and update member access level' do
-        put(
-          api("/groups/#{group_with_members.id}/members/#{reporter.id}",
-              owner),
-          access_level: GroupMember::MASTER
-        )
-
-        expect(response).to have_http_status(200)
-
-        get api("/groups/#{group_with_members.id}/members", owner)
-        json_reporter = json_response.find do |e|
-          e['id'] == reporter.id
-        end
-
-        expect(json_reporter['access_level']).to eq(GroupMember::MASTER)
-      end
-
-      it 'should not allow guest to modify group members' do
-        put(
-          api("/groups/#{group_with_members.id}/members/#{developer.id}",
-              guest),
-          access_level: GroupMember::MASTER
-        )
-
-        expect(response).to have_http_status(403)
-
-        get api("/groups/#{group_with_members.id}/members", owner)
-        json_developer = json_response.find do |e|
-          e['id'] == developer.id
-        end
-
-        expect(json_developer['access_level']).to eq(GroupMember::DEVELOPER)
-      end
-
-      it 'should return a 400 error when access level is not given' do
-        put(
-          api("/groups/#{group_with_members.id}/members/#{master.id}", owner)
-        )
-        expect(response).to have_http_status(400)
-      end
-
-      it 'should return a 422 error when access level is not known' do
-        put(
-          api("/groups/#{group_with_members.id}/members/#{master.id}", owner),
-          access_level: 1234
-        )
-        expect(response).to have_http_status(422)
-      end
-    end
-  end
-
-  describe 'DELETE /groups/:id/members/:user_id' do
-    context 'when not a member of the group' do
-      it "should not delete guest's membership of group_with_members" do
-        random_user = create(:user)
-        delete api("/groups/#{group_with_members.id}/members/#{owner.id}", random_user)
-
-        expect(response).to have_http_status(404)
-      end
-    end
-
-    context "when a member of the group" do
-      it "should delete guest's membership of group" do
-        expect do
-          delete api("/groups/#{group_with_members.id}/members/#{guest.id}", owner)
-        end.to change { group_with_members.members.count }.by(-1)
-
-        expect(response).to have_http_status(200)
-      end
-
-      it "should return a 404 error when user id is not known" do
-        delete api("/groups/#{group_with_members.id}/members/1328", owner)
-        expect(response).to have_http_status(404)
-      end
-
-      it "should not allow guest to modify group members" do
-        delete api("/groups/#{group_with_members.id}/members/#{master.id}", guest)
-        expect(response).to have_http_status(403)
-      end
-    end
-  end
-end
diff --git a/spec/requests/api/groups_spec.rb b/spec/requests/api/groups_spec.rb
index c2c94040eceb558d12eca31d3243ebed4c452308..4860b23c2ed7bfa2f68648b18c6ebac1bba93c64 100644
--- a/spec/requests/api/groups_spec.rb
+++ b/spec/requests/api/groups_spec.rb
@@ -21,14 +21,14 @@ describe API::API, api: true  do
 
   describe "GET /groups" do
     context "when unauthenticated" do
-      it "should return authentication error" do
+      it "returns authentication error" do
         get api("/groups")
         expect(response).to have_http_status(401)
       end
     end
 
     context "when authenticated as user" do
-      it "normal user: should return an array of groups of user1" do
+      it "normal user: returns an array of groups of user1" do
         get api("/groups", user1)
         expect(response).to have_http_status(200)
         expect(json_response).to be_an Array
@@ -38,7 +38,7 @@ describe API::API, api: true  do
     end
 
     context "when authenticated as  admin" do
-      it "admin: should return an array of all groups" do
+      it "admin: returns an array of all groups" do
         get api("/groups", admin)
         expect(response).to have_http_status(200)
         expect(json_response).to be_an Array
@@ -70,12 +70,12 @@ describe API::API, api: true  do
         expect(json_response['shared_projects'][0]['id']).to eq(project.id)
       end
 
-      it "should not return a non existing group" do
+      it "does not return a non existing group" do
         get api("/groups/1328", user1)
         expect(response).to have_http_status(404)
       end
 
-      it "should not return a group not attached to user1" do
+      it "does not return a group not attached to user1" do
         get api("/groups/#{group2.id}", user1)
 
         expect(response).to have_http_status(404)
@@ -83,31 +83,31 @@ describe API::API, api: true  do
     end
 
     context "when authenticated as admin" do
-      it "should return any existing group" do
+      it "returns any existing group" do
         get api("/groups/#{group2.id}", admin)
         expect(response).to have_http_status(200)
         expect(json_response['name']).to eq(group2.name)
       end
 
-      it "should not return a non existing group" do
+      it "does not return a non existing group" do
         get api("/groups/1328", admin)
         expect(response).to have_http_status(404)
       end
     end
 
     context 'when using group path in URL' do
-      it 'should return any existing group' do
+      it 'returns any existing group' do
         get api("/groups/#{group1.path}", admin)
         expect(response).to have_http_status(200)
         expect(json_response['name']).to eq(group1.name)
       end
 
-      it 'should not return a non existing group' do
+      it 'does not return a non existing group' do
         get api('/groups/unknown', admin)
         expect(response).to have_http_status(404)
       end
 
-      it 'should not return a group not attached to user1' do
+      it 'does not return a group not attached to user1' do
         get api("/groups/#{group2.path}", user1)
 
         expect(response).to have_http_status(404)
@@ -161,7 +161,7 @@ describe API::API, api: true  do
 
   describe "GET /groups/:id/projects" do
     context "when authenticated as user" do
-      it "should return the group's projects" do
+      it "returns the group's projects" do
         get api("/groups/#{group1.id}/projects", user1)
 
         expect(response).to have_http_status(200)
@@ -170,12 +170,12 @@ describe API::API, api: true  do
         expect(project_names).to match_array([project1.name, project3.name])
       end
 
-      it "should not return a non existing group" do
+      it "does not return a non existing group" do
         get api("/groups/1328/projects", user1)
         expect(response).to have_http_status(404)
       end
 
-      it "should not return a group not attached to user1" do
+      it "does not return a group not attached to user1" do
         get api("/groups/#{group2.id}/projects", user1)
 
         expect(response).to have_http_status(404)
@@ -215,12 +215,12 @@ describe API::API, api: true  do
         expect(project_names).to match_array([project1.name, project3.name])
       end
 
-      it 'should not return a non existing group' do
+      it 'does not return a non existing group' do
         get api('/groups/unknown/projects', admin)
         expect(response).to have_http_status(404)
       end
 
-      it 'should not return a group not attached to user1' do
+      it 'does not return a group not attached to user1' do
         get api("/groups/#{group2.path}/projects", user1)
 
         expect(response).to have_http_status(404)
@@ -230,30 +230,30 @@ describe API::API, api: true  do
 
   describe "POST /groups" do
     context "when authenticated as user without group permissions" do
-      it "should not create group" do
+      it "does not create group" do
         post api("/groups", user1), attributes_for(:group)
         expect(response).to have_http_status(403)
       end
     end
 
     context "when authenticated as user with group permissions" do
-      it "should create group" do
+      it "creates group" do
         post api("/groups", user3), attributes_for(:group)
         expect(response).to have_http_status(201)
       end
 
-      it "should not create group, duplicate" do
+      it "does not create group, duplicate" do
         post api("/groups", user3), { name: 'Duplicate Test', path: group2.path }
         expect(response).to have_http_status(400)
         expect(response.message).to eq("Bad Request")
       end
 
-      it "should return 400 bad request error if name not given" do
+      it "returns 400 bad request error if name not given" do
         post api("/groups", user3), { path: group2.path }
         expect(response).to have_http_status(400)
       end
 
-      it "should return 400 bad request error if path not given" do
+      it "returns 400 bad request error if path not given" do
         post api("/groups", user3), { name: 'test' }
         expect(response).to have_http_status(400)
       end
@@ -262,24 +262,24 @@ describe API::API, api: true  do
 
   describe "DELETE /groups/:id" do
     context "when authenticated as user" do
-      it "should remove group" do
+      it "removes group" do
         delete api("/groups/#{group1.id}", user1)
         expect(response).to have_http_status(200)
       end
 
-      it "should not remove a group if not an owner" do
+      it "does not remove a group if not an owner" do
         user4 = create(:user)
         group1.add_master(user4)
         delete api("/groups/#{group1.id}", user3)
         expect(response).to have_http_status(403)
       end
 
-      it "should not remove a non existing group" do
+      it "does not remove a non existing group" do
         delete api("/groups/1328", user1)
         expect(response).to have_http_status(404)
       end
 
-      it "should not remove a group not attached to user1" do
+      it "does not remove a group not attached to user1" do
         delete api("/groups/#{group2.id}", user1)
 
         expect(response).to have_http_status(404)
@@ -287,12 +287,12 @@ describe API::API, api: true  do
     end
 
     context "when authenticated as admin" do
-      it "should remove any existing group" do
+      it "removes any existing group" do
         delete api("/groups/#{group2.id}", admin)
         expect(response).to have_http_status(200)
       end
 
-      it "should not remove a non existing group" do
+      it "does not remove a non existing group" do
         delete api("/groups/1328", admin)
         expect(response).to have_http_status(404)
       end
@@ -308,14 +308,14 @@ describe API::API, api: true  do
     end
 
     context "when authenticated as user" do
-      it "should not transfer project to group" do
+      it "does not transfer project to group" do
         post api("/groups/#{group1.id}/projects/#{project.id}", user2)
         expect(response).to have_http_status(403)
       end
     end
 
     context "when authenticated as admin" do
-      it "should transfer project to group" do
+      it "transfers project to group" do
         post api("/groups/#{group1.id}/projects/#{project.id}", admin)
         expect(response).to have_http_status(201)
       end
diff --git a/spec/requests/api/internal_spec.rb b/spec/requests/api/internal_spec.rb
index f6f85d6e95ede66a0d4badb985944e5560c23ba4..be52f88831f7078b468495bba8038632e4602349 100644
--- a/spec/requests/api/internal_spec.rb
+++ b/spec/requests/api/internal_spec.rb
@@ -275,6 +275,24 @@ describe API::API, api: true  do
     end
   end
 
+  describe 'GET /internal/merge_request_urls' do
+    let(:repo_name) { "#{project.namespace.name}/#{project.path}" }
+    let(:changes) { URI.escape("#{Gitlab::Git::BLANK_SHA} 570e7b2abdd848b95f2f578043fc23bd6f6fd24d refs/heads/new_branch") }
+
+    before do
+      project.team << [user, :developer]
+      get api("/internal/merge_request_urls?project=#{repo_name}&changes=#{changes}"), secret_token: secret_token
+    end
+
+    it 'returns link to create new merge request' do
+      expect(json_response).to match [{
+        "branch_name" => "new_branch",
+        "url" => "http://localhost/#{project.namespace.name}/#{project.path}/merge_requests/new?merge_request%5Bsource_branch%5D=new_branch",
+        "new_merge_request" => true
+      }]
+    end
+  end
+
   def pull(key, project, protocol = 'ssh')
     post(
       api("/internal/allowed"),
diff --git a/spec/requests/api/issues_spec.rb b/spec/requests/api/issues_spec.rb
index 9d3d28e0b9151a940c05ed9076946b4d3ea6af67..a40e1a93b715abf29ec2d04c85dbee6af0b25ae0 100644
--- a/spec/requests/api/issues_spec.rb
+++ b/spec/requests/api/issues_spec.rb
@@ -49,28 +49,28 @@ describe API::API, api: true  do
 
   describe "GET /issues" do
     context "when unauthenticated" do
-      it "should return authentication error" do
+      it "returns authentication error" do
         get api("/issues")
         expect(response).to have_http_status(401)
       end
     end
 
     context "when authenticated" do
-      it "should return an array of issues" do
+      it "returns an array of issues" do
         get api("/issues", user)
         expect(response).to have_http_status(200)
         expect(json_response).to be_an Array
         expect(json_response.first['title']).to eq(issue.title)
       end
 
-      it "should add pagination headers and keep query params" do
+      it "adds pagination headers and keep query params" do
         get api("/issues?state=closed&per_page=3", user)
         expect(response.headers['Link']).to eq(
           '<http://www.example.com/api/v3/issues?page=1&per_page=3&private_token=%s&state=closed>; rel="first", <http://www.example.com/api/v3/issues?page=1&per_page=3&private_token=%s&state=closed>; rel="last"' % [user.private_token, user.private_token]
         )
       end
 
-      it 'should return an array of closed issues' do
+      it 'returns an array of closed issues' do
         get api('/issues?state=closed', user)
         expect(response).to have_http_status(200)
         expect(json_response).to be_an Array
@@ -78,7 +78,7 @@ describe API::API, api: true  do
         expect(json_response.first['id']).to eq(closed_issue.id)
       end
 
-      it 'should return an array of opened issues' do
+      it 'returns an array of opened issues' do
         get api('/issues?state=opened', user)
         expect(response).to have_http_status(200)
         expect(json_response).to be_an Array
@@ -86,7 +86,7 @@ describe API::API, api: true  do
         expect(json_response.first['id']).to eq(issue.id)
       end
 
-      it 'should return an array of all issues' do
+      it 'returns an array of all issues' do
         get api('/issues?state=all', user)
         expect(response).to have_http_status(200)
         expect(json_response).to be_an Array
@@ -95,7 +95,7 @@ describe API::API, api: true  do
         expect(json_response.second['id']).to eq(closed_issue.id)
       end
 
-      it 'should return an array of labeled issues' do
+      it 'returns an array of labeled issues' do
         get api("/issues?labels=#{label.title}", user)
         expect(response).to have_http_status(200)
         expect(json_response).to be_an Array
@@ -103,7 +103,7 @@ describe API::API, api: true  do
         expect(json_response.first['labels']).to eq([label.title])
       end
 
-      it 'should return an array of labeled issues when at least one label matches' do
+      it 'returns an array of labeled issues when at least one label matches' do
         get api("/issues?labels=#{label.title},foo,bar", user)
         expect(response).to have_http_status(200)
         expect(json_response).to be_an Array
@@ -111,14 +111,14 @@ describe API::API, api: true  do
         expect(json_response.first['labels']).to eq([label.title])
       end
 
-      it 'should return an empty array if no issue matches labels' do
+      it 'returns an empty array if no issue matches labels' do
         get api('/issues?labels=foo,bar', user)
         expect(response).to have_http_status(200)
         expect(json_response).to be_an Array
         expect(json_response.length).to eq(0)
       end
 
-      it 'should return an array of labeled issues matching given state' do
+      it 'returns an array of labeled issues matching given state' do
         get api("/issues?labels=#{label.title}&state=opened", user)
         expect(response).to have_http_status(200)
         expect(json_response).to be_an Array
@@ -127,7 +127,7 @@ describe API::API, api: true  do
         expect(json_response.first['state']).to eq('opened')
       end
 
-      it 'should return an empty array if no issue matches labels and state filters' do
+      it 'returns an empty array if no issue matches labels and state filters' do
         get api("/issues?labels=#{label.title}&state=closed", user)
         expect(response).to have_http_status(200)
         expect(json_response).to be_an Array
@@ -282,7 +282,7 @@ describe API::API, api: true  do
     let(:base_url) { "/projects/#{project.id}" }
     let(:title) { milestone.title }
 
-    it 'should return project issues without confidential issues for non project members' do
+    it 'returns project issues without confidential issues for non project members' do
       get api("#{base_url}/issues", non_member)
       expect(response).to have_http_status(200)
       expect(json_response).to be_an Array
@@ -290,7 +290,7 @@ describe API::API, api: true  do
       expect(json_response.first['title']).to eq(issue.title)
     end
 
-    it 'should return project issues without confidential issues for project members with guest role' do
+    it 'returns project issues without confidential issues for project members with guest role' do
       get api("#{base_url}/issues", guest)
       expect(response).to have_http_status(200)
       expect(json_response).to be_an Array
@@ -298,7 +298,7 @@ describe API::API, api: true  do
       expect(json_response.first['title']).to eq(issue.title)
     end
 
-    it 'should return project confidential issues for author' do
+    it 'returns project confidential issues for author' do
       get api("#{base_url}/issues", author)
       expect(response).to have_http_status(200)
       expect(json_response).to be_an Array
@@ -306,7 +306,7 @@ describe API::API, api: true  do
       expect(json_response.first['title']).to eq(issue.title)
     end
 
-    it 'should return project confidential issues for assignee' do
+    it 'returns project confidential issues for assignee' do
       get api("#{base_url}/issues", assignee)
       expect(response).to have_http_status(200)
       expect(json_response).to be_an Array
@@ -314,7 +314,7 @@ describe API::API, api: true  do
       expect(json_response.first['title']).to eq(issue.title)
     end
 
-    it 'should return project issues with confidential issues for project members' do
+    it 'returns project issues with confidential issues for project members' do
       get api("#{base_url}/issues", user)
       expect(response).to have_http_status(200)
       expect(json_response).to be_an Array
@@ -322,7 +322,7 @@ describe API::API, api: true  do
       expect(json_response.first['title']).to eq(issue.title)
     end
 
-    it 'should return project confidential issues for admin' do
+    it 'returns project confidential issues for admin' do
       get api("#{base_url}/issues", admin)
       expect(response).to have_http_status(200)
       expect(json_response).to be_an Array
@@ -330,7 +330,7 @@ describe API::API, api: true  do
       expect(json_response.first['title']).to eq(issue.title)
     end
 
-    it 'should return an array of labeled project issues' do
+    it 'returns an array of labeled project issues' do
       get api("#{base_url}/issues?labels=#{label.title}", user)
       expect(response).to have_http_status(200)
       expect(json_response).to be_an Array
@@ -338,7 +338,7 @@ describe API::API, api: true  do
       expect(json_response.first['labels']).to eq([label.title])
     end
 
-    it 'should return an array of labeled project issues when at least one label matches' do
+    it 'returns an array of labeled project issues when at least one label matches' do
       get api("#{base_url}/issues?labels=#{label.title},foo,bar", user)
       expect(response).to have_http_status(200)
       expect(json_response).to be_an Array
@@ -346,28 +346,28 @@ describe API::API, api: true  do
       expect(json_response.first['labels']).to eq([label.title])
     end
 
-    it 'should return an empty array if no project issue matches labels' do
+    it 'returns an empty array if no project issue matches labels' do
       get api("#{base_url}/issues?labels=foo,bar", user)
       expect(response).to have_http_status(200)
       expect(json_response).to be_an Array
       expect(json_response.length).to eq(0)
     end
 
-    it 'should return an empty array if no issue matches milestone' do
+    it 'returns an empty array if no issue matches milestone' do
       get api("#{base_url}/issues?milestone=#{empty_milestone.title}", user)
       expect(response).to have_http_status(200)
       expect(json_response).to be_an Array
       expect(json_response.length).to eq(0)
     end
 
-    it 'should return an empty array if milestone does not exist' do
+    it 'returns an empty array if milestone does not exist' do
       get api("#{base_url}/issues?milestone=foo", user)
       expect(response).to have_http_status(200)
       expect(json_response).to be_an Array
       expect(json_response.length).to eq(0)
     end
 
-    it 'should return an array of issues in given milestone' do
+    it 'returns an array of issues in given milestone' do
       get api("#{base_url}/issues?milestone=#{title}", user)
       expect(response).to have_http_status(200)
       expect(json_response).to be_an Array
@@ -376,7 +376,7 @@ describe API::API, api: true  do
       expect(json_response.second['id']).to eq(closed_issue.id)
     end
 
-    it 'should return an array of issues matching state in milestone' do
+    it 'returns an array of issues matching state in milestone' do
       get api("#{base_url}/issues?milestone=#{milestone.title}"\
               '&state=closed', user)
       expect(response).to have_http_status(200)
@@ -405,7 +405,7 @@ describe API::API, api: true  do
       expect(json_response['author']).to be_a Hash
     end
 
-    it "should return a project issue by id" do
+    it "returns a project issue by id" do
       get api("/projects/#{project.id}/issues/#{issue.id}", user)
 
       expect(response).to have_http_status(200)
@@ -413,7 +413,7 @@ describe API::API, api: true  do
       expect(json_response['iid']).to eq(issue.iid)
     end
 
-    it 'should return a project issue by iid' do
+    it 'returns a project issue by iid' do
       get api("/projects/#{project.id}/issues?iid=#{issue.iid}", user)
       expect(response.status).to eq 200
       expect(json_response.first['title']).to eq issue.title
@@ -421,44 +421,44 @@ describe API::API, api: true  do
       expect(json_response.first['iid']).to eq issue.iid
     end
 
-    it "should return 404 if issue id not found" do
+    it "returns 404 if issue id not found" do
       get api("/projects/#{project.id}/issues/54321", user)
       expect(response).to have_http_status(404)
     end
 
     context 'confidential issues' do
-      it "should return 404 for non project members" do
+      it "returns 404 for non project members" do
         get api("/projects/#{project.id}/issues/#{confidential_issue.id}", non_member)
         expect(response).to have_http_status(404)
       end
 
-      it "should return 404 for project members with guest role" do
+      it "returns 404 for project members with guest role" do
         get api("/projects/#{project.id}/issues/#{confidential_issue.id}", guest)
         expect(response).to have_http_status(404)
       end
 
-      it "should return confidential issue for project members" do
+      it "returns confidential issue for project members" do
         get api("/projects/#{project.id}/issues/#{confidential_issue.id}", user)
         expect(response).to have_http_status(200)
         expect(json_response['title']).to eq(confidential_issue.title)
         expect(json_response['iid']).to eq(confidential_issue.iid)
       end
 
-      it "should return confidential issue for author" do
+      it "returns confidential issue for author" do
         get api("/projects/#{project.id}/issues/#{confidential_issue.id}", author)
         expect(response).to have_http_status(200)
         expect(json_response['title']).to eq(confidential_issue.title)
         expect(json_response['iid']).to eq(confidential_issue.iid)
       end
 
-      it "should return confidential issue for assignee" do
+      it "returns confidential issue for assignee" do
         get api("/projects/#{project.id}/issues/#{confidential_issue.id}", assignee)
         expect(response).to have_http_status(200)
         expect(json_response['title']).to eq(confidential_issue.title)
         expect(json_response['iid']).to eq(confidential_issue.iid)
       end
 
-      it "should return confidential issue for admin" do
+      it "returns confidential issue for admin" do
         get api("/projects/#{project.id}/issues/#{confidential_issue.id}", admin)
         expect(response).to have_http_status(200)
         expect(json_response['title']).to eq(confidential_issue.title)
@@ -468,7 +468,7 @@ describe API::API, api: true  do
   end
 
   describe "POST /projects/:id/issues" do
-    it "should create 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)
@@ -477,12 +477,12 @@ describe API::API, api: true  do
       expect(json_response['labels']).to eq(['label', 'label2'])
     end
 
-    it "should return a 400 bad request if title not given" do
+    it "returns a 400 bad request if title not given" do
       post api("/projects/#{project.id}/issues", user), labels: 'label, label2'
       expect(response).to have_http_status(400)
     end
 
-    it 'should allow special label names' do
+    it 'allows special label names' do
       post api("/projects/#{project.id}/issues", user),
            title: 'new issue',
            labels: 'label, label?, label&foo, ?, &'
@@ -494,7 +494,7 @@ describe API::API, api: true  do
       expect(json_response['labels']).to include '&'
     end
 
-    it 'should return 400 if title is too long' do
+    it 'returns 400 if title is too long' do
       post api("/projects/#{project.id}/issues", user),
            title: 'g' * 256
       expect(response).to have_http_status(400)
@@ -531,8 +531,8 @@ describe API::API, api: true  do
 
   describe 'POST /projects/:id/issues with spam filtering' do
     before do
-      allow_any_instance_of(Gitlab::AkismetHelper).to receive(:check_for_spam?).and_return(true)
-      allow_any_instance_of(Gitlab::AkismetHelper).to receive(:is_spam?).and_return(true)
+      allow_any_instance_of(SpamService).to receive(:check_for_spam?).and_return(true)
+      allow_any_instance_of(AkismetService).to receive_messages(is_spam?: true)
     end
 
     let(:params) do
@@ -543,7 +543,7 @@ describe API::API, api: true  do
       }
     end
 
-    it "should not create a new project issue" do
+    it "does not create a new project issue" do
       expect { post api("/projects/#{project.id}/issues", user), params }.not_to change(Issue, :count)
       expect(response).to have_http_status(400)
       expect(json_response['message']).to eq({ "error" => "Spam detected" })
@@ -554,12 +554,11 @@ describe API::API, api: true  do
       expect(spam_logs[0].description).to eq('content here')
       expect(spam_logs[0].user).to eq(user)
       expect(spam_logs[0].noteable_type).to eq('Issue')
-      expect(spam_logs[0].project_id).to eq(project.id)
     end
   end
 
   describe "PUT /projects/:id/issues/:issue_id to update only title" do
-    it "should update a project issue" do
+    it "updates a project issue" do
       put api("/projects/#{project.id}/issues/#{issue.id}", user),
         title: 'updated title'
       expect(response).to have_http_status(200)
@@ -567,13 +566,13 @@ describe API::API, api: true  do
       expect(json_response['title']).to eq('updated title')
     end
 
-    it "should return 404 error if issue id not found" do
+    it "returns 404 error if issue id not found" do
       put api("/projects/#{project.id}/issues/44444", user),
         title: 'updated title'
       expect(response).to have_http_status(404)
     end
 
-    it 'should allow special label names' do
+    it 'allows special label names' do
       put api("/projects/#{project.id}/issues/#{issue.id}", user),
           title: 'updated title',
           labels: 'label, label?, label&foo, ?, &'
@@ -587,33 +586,33 @@ describe API::API, api: true  do
     end
 
     context 'confidential issues' do
-      it "should return 403 for non project members" do
+      it "returns 403 for non project members" do
         put api("/projects/#{project.id}/issues/#{confidential_issue.id}", non_member),
           title: 'updated title'
         expect(response).to have_http_status(403)
       end
 
-      it "should return 403 for project members with guest role" do
+      it "returns 403 for project members with guest role" do
         put api("/projects/#{project.id}/issues/#{confidential_issue.id}", guest),
           title: 'updated title'
         expect(response).to have_http_status(403)
       end
 
-      it "should update a confidential issue for project members" do
+      it "updates a confidential issue for project members" do
         put api("/projects/#{project.id}/issues/#{confidential_issue.id}", user),
           title: 'updated title'
         expect(response).to have_http_status(200)
         expect(json_response['title']).to eq('updated title')
       end
 
-      it "should update a confidential issue for author" do
+      it "updates a confidential issue for author" do
         put api("/projects/#{project.id}/issues/#{confidential_issue.id}", author),
           title: 'updated title'
         expect(response).to have_http_status(200)
         expect(json_response['title']).to eq('updated title')
       end
 
-      it "should update a confidential issue for admin" do
+      it "updates a confidential issue for admin" do
         put api("/projects/#{project.id}/issues/#{confidential_issue.id}", admin),
           title: 'updated title'
         expect(response).to have_http_status(200)
@@ -626,21 +625,21 @@ describe API::API, api: true  do
     let!(:label) { create(:label, title: 'dummy', project: project) }
     let!(:label_link) { create(:label_link, label: label, target: issue) }
 
-    it 'should not update labels if not present' do
+    it 'does not update labels if not present' do
       put api("/projects/#{project.id}/issues/#{issue.id}", user),
           title: 'updated title'
       expect(response).to have_http_status(200)
       expect(json_response['labels']).to eq([label.title])
     end
 
-    it 'should remove all labels' do
+    it 'removes all labels' do
       put api("/projects/#{project.id}/issues/#{issue.id}", user),
           labels: ''
       expect(response).to have_http_status(200)
       expect(json_response['labels']).to eq([])
     end
 
-    it 'should update labels' do
+    it 'updates labels' do
       put api("/projects/#{project.id}/issues/#{issue.id}", user),
           labels: 'foo,bar'
       expect(response).to have_http_status(200)
@@ -648,7 +647,7 @@ describe API::API, api: true  do
       expect(json_response['labels']).to include 'bar'
     end
 
-    it 'should allow special label names' do
+    it 'allows special label names' do
       put api("/projects/#{project.id}/issues/#{issue.id}", user),
           labels: 'label:foo, label-bar,label_bar,label/bar,label?bar,label&bar,?,&'
       expect(response.status).to eq(200)
@@ -662,7 +661,7 @@ describe API::API, api: true  do
       expect(json_response['labels']).to include '&'
     end
 
-    it 'should return 400 if title is too long' do
+    it 'returns 400 if title is too long' do
       put api("/projects/#{project.id}/issues/#{issue.id}", user),
           title: 'g' * 256
       expect(response).to have_http_status(400)
@@ -673,7 +672,7 @@ describe API::API, api: true  do
   end
 
   describe "PUT /projects/:id/issues/:issue_id to update state and label" do
-    it "should update a project issue" do
+    it "updates a project issue" do
       put api("/projects/#{project.id}/issues/#{issue.id}", user),
         labels: 'label2', state_event: "close"
       expect(response).to have_http_status(200)
diff --git a/spec/requests/api/keys_spec.rb b/spec/requests/api/keys_spec.rb
index 1861882d59ecb8ec3be42d817aa0a036061e2f83..893ed5c2b10d979b8584e3e6ca2859a19e6adce5 100644
--- a/spec/requests/api/keys_spec.rb
+++ b/spec/requests/api/keys_spec.rb
@@ -12,20 +12,20 @@ describe API::API, api: true  do
     before { admin }
 
     context 'when unauthenticated' do
-      it 'should return authentication error' do
+      it 'returns authentication error' do
         get api("/keys/#{key.id}")
         expect(response).to have_http_status(401)
       end
     end
 
     context 'when authenticated' do
-      it 'should return 404 for non-existing key' do
+      it 'returns 404 for non-existing key' do
         get api('/keys/999999', admin)
         expect(response).to have_http_status(404)
         expect(json_response['message']).to eq('404 Not found')
       end
 
-      it 'should return single ssh key with user information' do
+      it 'returns single ssh key with user information' do
         user.keys << key
         user.save
         get api("/keys/#{key.id}", admin)
diff --git a/spec/requests/api/labels_spec.rb b/spec/requests/api/labels_spec.rb
index 63636b4a1b61a8cb0f1b270499ca34cc35ba7371..83789223019c76ffdbb904c22f6e918c0367f0ea 100644
--- a/spec/requests/api/labels_spec.rb
+++ b/spec/requests/api/labels_spec.rb
@@ -12,7 +12,7 @@ describe API::API, api: true  do
   end
 
   describe 'GET /projects/:id/labels' do
-    it 'should return project labels' do
+    it 'returns project labels' do
       get api("/projects/#{project.id}/labels", user)
       expect(response).to have_http_status(200)
       expect(json_response).to be_an Array
@@ -22,7 +22,7 @@ describe API::API, api: true  do
   end
 
   describe 'POST /projects/:id/labels' do
-    it 'should return created label when all params' do
+    it 'returns created label when all params' do
       post api("/projects/#{project.id}/labels", user),
            name: 'Foo',
            color: '#FFAABB',
@@ -33,7 +33,7 @@ describe API::API, api: true  do
       expect(json_response['description']).to eq('test')
     end
 
-    it 'should return created label when only required params' do
+    it 'returns created label when only required params' do
       post api("/projects/#{project.id}/labels", user),
            name: 'Foo & Bar',
            color: '#FFAABB'
@@ -43,17 +43,17 @@ describe API::API, api: true  do
       expect(json_response['description']).to be_nil
     end
 
-    it 'should return a 400 bad request if name not given' do
+    it 'returns a 400 bad request if name not given' do
       post api("/projects/#{project.id}/labels", user), color: '#FFAABB'
       expect(response).to have_http_status(400)
     end
 
-    it 'should return a 400 bad request if color not given' do
+    it 'returns a 400 bad request if color not given' do
       post api("/projects/#{project.id}/labels", user), name: 'Foobar'
       expect(response).to have_http_status(400)
     end
 
-    it 'should return 400 for invalid color' do
+    it 'returns 400 for invalid color' do
       post api("/projects/#{project.id}/labels", user),
            name: 'Foo',
            color: '#FFAA'
@@ -61,7 +61,7 @@ describe API::API, api: true  do
       expect(json_response['message']['color']).to eq(['must be a valid color code'])
     end
 
-    it 'should return 400 for too long color code' do
+    it 'returns 400 for too long color code' do
       post api("/projects/#{project.id}/labels", user),
            name: 'Foo',
            color: '#FFAAFFFF'
@@ -69,7 +69,7 @@ describe API::API, api: true  do
       expect(json_response['message']['color']).to eq(['must be a valid color code'])
     end
 
-    it 'should return 400 for invalid name' do
+    it 'returns 400 for invalid name' do
       post api("/projects/#{project.id}/labels", user),
            name: ',',
            color: '#FFAABB'
@@ -77,7 +77,7 @@ describe API::API, api: true  do
       expect(json_response['message']['title']).to eq(['is invalid'])
     end
 
-    it 'should return 409 if label already exists' do
+    it 'returns 409 if label already exists' do
       post api("/projects/#{project.id}/labels", user),
            name: 'label1',
            color: '#FFAABB'
@@ -87,25 +87,25 @@ describe API::API, api: true  do
   end
 
   describe 'DELETE /projects/:id/labels' do
-    it 'should return 200 for existing label' do
+    it 'returns 200 for existing label' do
       delete api("/projects/#{project.id}/labels", user), name: 'label1'
       expect(response).to have_http_status(200)
     end
 
-    it 'should return 404 for non existing label' do
+    it 'returns 404 for non existing label' do
       delete api("/projects/#{project.id}/labels", user), name: 'label2'
       expect(response).to have_http_status(404)
       expect(json_response['message']).to eq('404 Label Not Found')
     end
 
-    it 'should return 400 for wrong parameters' do
+    it 'returns 400 for wrong parameters' do
       delete api("/projects/#{project.id}/labels", user)
       expect(response).to have_http_status(400)
     end
   end
 
   describe 'PUT /projects/:id/labels' do
-    it 'should return 200 if name and colors and description are changed' do
+    it 'returns 200 if name and colors and description are changed' do
       put api("/projects/#{project.id}/labels", user),
           name: 'label1',
           new_name: 'New Label',
@@ -117,7 +117,7 @@ describe API::API, api: true  do
       expect(json_response['description']).to eq('test')
     end
 
-    it 'should return 200 if name is changed' do
+    it 'returns 200 if name is changed' do
       put api("/projects/#{project.id}/labels", user),
           name: 'label1',
           new_name: 'New Label'
@@ -126,7 +126,7 @@ describe API::API, api: true  do
       expect(json_response['color']).to eq(label1.color)
     end
 
-    it 'should return 200 if colors is changed' do
+    it 'returns 200 if colors is changed' do
       put api("/projects/#{project.id}/labels", user),
           name: 'label1',
           color: '#FFFFFF'
@@ -135,7 +135,7 @@ describe API::API, api: true  do
       expect(json_response['color']).to eq('#FFFFFF')
     end
 
-    it 'should return 200 if description is changed' do
+    it 'returns 200 if description is changed' do
       put api("/projects/#{project.id}/labels", user),
           name: 'label1',
           description: 'test'
@@ -144,27 +144,27 @@ describe API::API, api: true  do
       expect(json_response['description']).to eq('test')
     end
 
-    it 'should return 404 if label does not exist' do
+    it 'returns 404 if label does not exist' do
       put api("/projects/#{project.id}/labels", user),
           name: 'label2',
           new_name: 'label3'
       expect(response).to have_http_status(404)
     end
 
-    it 'should return 400 if no label name given' 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')
     end
 
-    it 'should return 400 if no new parameters given' do
+    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')
     end
 
-    it 'should return 400 for invalid name' do
+    it 'returns 400 for invalid name' do
       put api("/projects/#{project.id}/labels", user),
           name: 'label1',
           new_name: ',',
@@ -173,7 +173,7 @@ describe API::API, api: true  do
       expect(json_response['message']['title']).to eq(['is invalid'])
     end
 
-    it 'should return 400 when color code is too short' do
+    it 'returns 400 when color code is too short' do
       put api("/projects/#{project.id}/labels", user),
           name: 'label1',
           color: '#FF'
@@ -181,7 +181,7 @@ describe API::API, api: true  do
       expect(json_response['message']['color']).to eq(['must be a valid color code'])
     end
 
-    it 'should return 400 for too long color code' do
+    it 'returns 400 for too long color code' do
       post api("/projects/#{project.id}/labels", user),
            name: 'Foo',
            color: '#FFAAFFFF'
@@ -192,7 +192,7 @@ describe API::API, api: true  do
 
   describe "POST /projects/:id/labels/:label_id/subscription" do
     context "when label_id is a label title" do
-      it "should subscribe to the label" do
+      it "subscribes to the label" do
         post api("/projects/#{project.id}/labels/#{label1.title}/subscription", user)
 
         expect(response).to have_http_status(201)
@@ -202,7 +202,7 @@ describe API::API, api: true  do
     end
 
     context "when label_id is a label ID" do
-      it "should subscribe to the label" do
+      it "subscribes to the label" do
         post api("/projects/#{project.id}/labels/#{label1.id}/subscription", user)
 
         expect(response).to have_http_status(201)
@@ -214,7 +214,7 @@ describe API::API, api: true  do
     context "when user is already subscribed to label" do
       before { label1.subscribe(user) }
 
-      it "should return 304" do
+      it "returns 304" do
         post api("/projects/#{project.id}/labels/#{label1.id}/subscription", user)
 
         expect(response).to have_http_status(304)
@@ -222,7 +222,7 @@ describe API::API, api: true  do
     end
 
     context "when label ID is not found" do
-      it "should a return 404 error" do
+      it "returns 404 error" do
         post api("/projects/#{project.id}/labels/1234/subscription", user)
 
         expect(response).to have_http_status(404)
@@ -234,7 +234,7 @@ describe API::API, api: true  do
     before { label1.subscribe(user) }
 
     context "when label_id is a label title" do
-      it "should unsubscribe from the label" do
+      it "unsubscribes from the label" do
         delete api("/projects/#{project.id}/labels/#{label1.title}/subscription", user)
 
         expect(response).to have_http_status(200)
@@ -244,7 +244,7 @@ describe API::API, api: true  do
     end
 
     context "when label_id is a label ID" do
-      it "should unsubscribe from the label" do
+      it "unsubscribes from the label" do
         delete api("/projects/#{project.id}/labels/#{label1.id}/subscription", user)
 
         expect(response).to have_http_status(200)
@@ -256,7 +256,7 @@ describe API::API, api: true  do
     context "when user is already unsubscribed from label" do
       before { label1.unsubscribe(user) }
 
-      it "should return 304" do
+      it "returns 304" do
         delete api("/projects/#{project.id}/labels/#{label1.id}/subscription", user)
 
         expect(response).to have_http_status(304)
@@ -264,7 +264,7 @@ describe API::API, api: true  do
     end
 
     context "when label ID is not found" do
-      it "should a return 404 error" do
+      it "returns 404 error" do
         delete api("/projects/#{project.id}/labels/1234/subscription", user)
 
         expect(response).to have_http_status(404)
diff --git a/spec/requests/api/members_spec.rb b/spec/requests/api/members_spec.rb
new file mode 100644
index 0000000000000000000000000000000000000000..1e365bf353a9e133de342456237ef9a65e379021
--- /dev/null
+++ b/spec/requests/api/members_spec.rb
@@ -0,0 +1,314 @@
+require 'spec_helper'
+
+describe API::Members, api: true  do
+  include ApiHelpers
+
+  let(:master) { create(:user) }
+  let(:developer) { create(:user) }
+  let(:access_requester) { create(:user) }
+  let(:stranger) { create(:user) }
+
+  let(:project) do
+    project = create(:project, :public, creator_id: master.id, namespace: master.namespace)
+    project.team << [developer, :developer]
+    project.team << [master, :master]
+    project.request_access(access_requester)
+    project
+  end
+
+  let!(:group) do
+    group = create(:group, :public)
+    group.add_developer(developer)
+    group.add_owner(master)
+    group.request_access(access_requester)
+    group
+  end
+
+  shared_examples 'GET /: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) { 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)
+
+              expect(response).to have_http_status(200)
+              expect(json_response.size).to eq(2)
+            end
+          end
+        end
+      end
+
+      it 'finds members with query string' do
+        get api("/#{source_type.pluralize}/#{source.id}/members", developer), query: master.username
+
+        expect(response).to have_http_status(200)
+        expect(json_response.count).to eq(1)
+        expect(json_response.first['username']).to eq(master.username)
+      end
+    end
+  end
+
+  shared_examples 'GET /: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) { get api("/#{source_type.pluralize}/#{source.id}/members/#{developer.id}", 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/#{developer.id}", user)
+
+              expect(response).to have_http_status(200)
+              # User attributes
+              expect(json_response['id']).to eq(developer.id)
+              expect(json_response['name']).to eq(developer.name)
+              expect(json_response['username']).to eq(developer.username)
+              expect(json_response['state']).to eq(developer.state)
+              expect(json_response['avatar_url']).to eq(developer.avatar_url)
+              expect(json_response['web_url']).to eq(Gitlab::Routing.url_helpers.user_url(developer))
+
+              # Member attributes
+              expect(json_response['access_level']).to eq(Member::DEVELOPER)
+            end
+          end
+        end
+      end
+    end
+  end
+
+  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) }
+      end
+
+      context 'when authenticated as a non-member or member with insufficient rights' do
+        %i[access_requester stranger developer].each do |type|
+          context "as a #{type}" do
+            it 'returns 403' do
+              user = public_send(type)
+              post api("/#{source_type.pluralize}/#{source.id}/members", user)
+
+              expect(response).to have_http_status(403)
+            end
+          end
+        end
+      end
+
+      context 'when authenticated as a master/owner' do
+        context 'and new member is already a requester' do
+          it 'transforms the requester into a proper member' do
+            expect do
+              post api("/#{source_type.pluralize}/#{source.id}/members", master),
+                   user_id: access_requester.id, access_level: Member::MASTER
+
+              expect(response).to have_http_status(201)
+            end.to change { source.members.count }.by(1)
+            expect(source.requesters.count).to eq(0)
+            expect(json_response['id']).to eq(access_requester.id)
+            expect(json_response['access_level']).to eq(Member::MASTER)
+          end
+        end
+
+        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, 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
+
+      it "returns #{source_type == 'project' ? 201 : 409} if member already exists" do
+        post api("/#{source_type.pluralize}/#{source.id}/members", master),
+             user_id: master.id, access_level: Member::MASTER
+
+        expect(response).to have_http_status(source_type == 'project' ? 201 : 409)
+      end
+
+      it 'returns 400 when user_id is not given' do
+        post api("/#{source_type.pluralize}/#{source.id}/members", master),
+             access_level: Member::MASTER
+
+        expect(response).to have_http_status(400)
+      end
+
+      it 'returns 400 when access_level is not given' do
+        post api("/#{source_type.pluralize}/#{source.id}/members", master),
+             user_id: stranger.id
+
+        expect(response).to have_http_status(400)
+      end
+
+      it 'returns 422 when access_level is not valid' do
+        post api("/#{source_type.pluralize}/#{source.id}/members", master),
+             user_id: stranger.id, access_level: 1234
+
+        expect(response).to have_http_status(422)
+      end
+    end
+  end
+
+  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) }
+      end
+
+      context 'when authenticated as a non-member or member with insufficient rights' do
+        %i[access_requester stranger developer].each do |type|
+          context "as a #{type}" do
+            it 'returns 403' do
+              user = public_send(type)
+              put api("/#{source_type.pluralize}/#{source.id}/members/#{developer.id}", user)
+
+              expect(response).to have_http_status(403)
+            end
+          end
+        end
+      end
+
+      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, 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
+
+      it 'returns 409 if member does not exist' do
+        put api("/#{source_type.pluralize}/#{source.id}/members/123", master),
+            access_level: Member::MASTER
+
+        expect(response).to have_http_status(404)
+      end
+
+      it 'returns 400 when access_level is not given' do
+        put api("/#{source_type.pluralize}/#{source.id}/members/#{developer.id}", master)
+
+        expect(response).to have_http_status(400)
+      end
+
+      it 'returns 422 when access level is not valid' do
+        put api("/#{source_type.pluralize}/#{source.id}/members/#{developer.id}", master),
+            access_level: 1234
+
+        expect(response).to have_http_status(422)
+      end
+    end
+  end
+
+  shared_examples 'DELETE /: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) { delete api("/#{source_type.pluralize}/#{source.id}/members/#{developer.id}", stranger) }
+      end
+
+      context 'when authenticated as a non-member or member with insufficient rights' do
+        %i[access_requester stranger].each do |type|
+          context "as a #{type}" do
+            it 'returns 403' do
+              user = public_send(type)
+              delete api("/#{source_type.pluralize}/#{source.id}/members/#{developer.id}", user)
+
+              expect(response).to have_http_status(403)
+            end
+          end
+        end
+      end
+
+      context 'when authenticated as a member and deleting themself' do
+        it 'deletes the member' do
+          expect do
+            delete api("/#{source_type.pluralize}/#{source.id}/members/#{developer.id}", developer)
+
+            expect(response).to have_http_status(200)
+          end.to change { source.members.count }.by(-1)
+        end
+      end
+
+      context 'when authenticated as a master/owner' do
+        context 'and member is a requester' do
+          it "returns #{source_type == 'project' ? 200 : 404}" do
+            expect do
+              delete api("/#{source_type.pluralize}/#{source.id}/members/#{access_requester.id}", master)
+
+              expect(response).to have_http_status(source_type == 'project' ? 200 : 404)
+            end.not_to change { source.requesters.count }
+          end
+        end
+
+        it 'deletes the member' do
+          expect do
+            delete api("/#{source_type.pluralize}/#{source.id}/members/#{developer.id}", master)
+
+            expect(response).to have_http_status(200)
+          end.to change { source.members.count }.by(-1)
+        end
+      end
+
+      it "returns #{source_type == 'project' ? 200 : 404} if member does not exist" do
+        delete api("/#{source_type.pluralize}/#{source.id}/members/123", master)
+
+        expect(response).to have_http_status(source_type == 'project' ? 200 : 404)
+      end
+    end
+  end
+
+  it_behaves_like 'GET /:sources/:id/members', 'project' do
+    let(:source) { project }
+  end
+
+  it_behaves_like 'GET /:sources/:id/members', 'group' do
+    let(:source) { group }
+  end
+
+  it_behaves_like 'GET /:sources/:id/members/:user_id', 'project' do
+    let(:source) { project }
+  end
+
+  it_behaves_like 'GET /:sources/:id/members/:user_id', 'group' do
+    let(:source) { group }
+  end
+
+  it_behaves_like 'POST /:sources/:id/members', 'project' do
+    let(:source) { project }
+  end
+
+  it_behaves_like 'POST /:sources/:id/members', 'group' do
+    let(:source) { group }
+  end
+
+  it_behaves_like 'PUT /:sources/:id/members/:user_id', 'project' do
+    let(:source) { project }
+  end
+
+  it_behaves_like 'PUT /:sources/:id/members/:user_id', 'group' do
+    let(:source) { group }
+  end
+
+  it_behaves_like 'DELETE /:sources/:id/members/:user_id', 'project' do
+    let(:source) { project }
+  end
+
+  it_behaves_like 'DELETE /:sources/:id/members/:user_id', 'group' do
+    let(:source) { group }
+  end
+end
diff --git a/spec/requests/api/merge_requests_spec.rb b/spec/requests/api/merge_requests_spec.rb
index 651b91e9f68a4d1e032dfdfb6e1d6f4346b16fa5..617600d617344f2922e54c2fd393ec43010bef75 100644
--- a/spec/requests/api/merge_requests_spec.rb
+++ b/spec/requests/api/merge_requests_spec.rb
@@ -20,14 +20,14 @@ describe API::API, api: true  do
 
   describe "GET /projects/:id/merge_requests" do
     context "when unauthenticated" do
-      it "should return authentication error" do
+      it "returns authentication error" do
         get api("/projects/#{project.id}/merge_requests")
         expect(response).to have_http_status(401)
       end
     end
 
     context "when authenticated" do
-      it "should return an array of all merge_requests" do
+      it "returns an array of all merge_requests" do
         get api("/projects/#{project.id}/merge_requests", user)
         expect(response).to have_http_status(200)
         expect(json_response).to be_an Array
@@ -35,7 +35,7 @@ describe API::API, api: true  do
         expect(json_response.last['title']).to eq(merge_request.title)
       end
 
-      it "should return an array of all merge_requests" do
+      it "returns an array of all merge_requests" do
         get api("/projects/#{project.id}/merge_requests?state", user)
         expect(response).to have_http_status(200)
         expect(json_response).to be_an Array
@@ -43,7 +43,7 @@ describe API::API, api: true  do
         expect(json_response.last['title']).to eq(merge_request.title)
       end
 
-      it "should return an array of open merge_requests" do
+      it "returns an array of open merge_requests" do
         get api("/projects/#{project.id}/merge_requests?state=opened", user)
         expect(response).to have_http_status(200)
         expect(json_response).to be_an Array
@@ -51,7 +51,7 @@ describe API::API, api: true  do
         expect(json_response.last['title']).to eq(merge_request.title)
       end
 
-      it "should return an array of closed merge_requests" do
+      it "returns an array of closed merge_requests" do
         get api("/projects/#{project.id}/merge_requests?state=closed", user)
         expect(response).to have_http_status(200)
         expect(json_response).to be_an Array
@@ -59,7 +59,7 @@ describe API::API, api: true  do
         expect(json_response.first['title']).to eq(merge_request_closed.title)
       end
 
-      it "should return an array of merged merge_requests" do
+      it "returns an array of merged merge_requests" do
         get api("/projects/#{project.id}/merge_requests?state=merged", user)
         expect(response).to have_http_status(200)
         expect(json_response).to be_an Array
@@ -73,7 +73,7 @@ describe API::API, api: true  do
           @mr_earlier = mr_with_earlier_created_and_updated_at_time
         end
 
-        it "should return an array of merge_requests in ascending order" do
+        it "returns an array of merge_requests in ascending order" do
           get api("/projects/#{project.id}/merge_requests?sort=asc", user)
           expect(response).to have_http_status(200)
           expect(json_response).to be_an Array
@@ -82,7 +82,7 @@ describe API::API, api: true  do
           expect(response_dates).to eq(response_dates.sort)
         end
 
-        it "should return an array of merge_requests in descending order" do
+        it "returns an array of merge_requests in descending order" do
           get api("/projects/#{project.id}/merge_requests?sort=desc", user)
           expect(response).to have_http_status(200)
           expect(json_response).to be_an Array
@@ -91,7 +91,7 @@ describe API::API, api: true  do
           expect(response_dates).to eq(response_dates.sort.reverse)
         end
 
-        it "should return an array of merge_requests ordered by updated_at" do
+        it "returns an array of merge_requests ordered by updated_at" do
           get api("/projects/#{project.id}/merge_requests?order_by=updated_at", user)
           expect(response).to have_http_status(200)
           expect(json_response).to be_an Array
@@ -100,7 +100,7 @@ describe API::API, api: true  do
           expect(response_dates).to eq(response_dates.sort.reverse)
         end
 
-        it "should return an array of merge_requests ordered by created_at" do
+        it "returns an array of merge_requests ordered by created_at" do
           get api("/projects/#{project.id}/merge_requests?order_by=created_at&sort=asc", user)
           expect(response).to have_http_status(200)
           expect(json_response).to be_an Array
@@ -142,7 +142,7 @@ describe API::API, api: true  do
       expect(json_response['force_close_merge_request']).to be_falsy
     end
 
-    it "should return merge_request" do
+    it "returns merge_request" do
       get api("/projects/#{project.id}/merge_requests/#{merge_request.id}", user)
       expect(response).to have_http_status(200)
       expect(json_response['title']).to eq(merge_request.title)
@@ -153,7 +153,7 @@ describe API::API, api: true  do
       expect(json_response['force_close_merge_request']).to be_falsy
     end
 
-    it 'should return merge_request by iid' do
+    it 'returns merge_request by iid' do
       url = "/projects/#{project.id}/merge_requests?iid=#{merge_request.iid}"
       get api(url, user)
       expect(response.status).to eq 200
@@ -161,7 +161,7 @@ describe API::API, api: true  do
       expect(json_response.first['id']).to eq merge_request.id
     end
 
-    it "should return a 404 error if merge_request_id not found" do
+    it "returns a 404 error if merge_request_id not found" do
       get api("/projects/#{project.id}/merge_requests/999", user)
       expect(response).to have_http_status(404)
     end
@@ -169,7 +169,7 @@ describe API::API, api: true  do
     context 'Work in Progress' do
       let!(:merge_request_wip) { create(:merge_request, author: user, assignee: user, source_project: project, target_project: project, title: "WIP: Test", created_at: base_time + 1.second) }
 
-      it "should return merge_request" do
+      it "returns merge_request" do
         get api("/projects/#{project.id}/merge_requests/#{merge_request_wip.id}", user)
         expect(response).to have_http_status(200)
         expect(json_response['work_in_progress']).to eq(true)
@@ -195,7 +195,7 @@ describe API::API, api: true  do
   end
 
   describe 'GET /projects/:id/merge_requests/:merge_request_id/changes' do
-    it 'should return the change information of the merge_request' do
+    it 'returns the change information of the merge_request' do
       get api("/projects/#{project.id}/merge_requests/#{merge_request.id}/changes", user)
       expect(response.status).to eq 200
       expect(json_response['changes'].size).to eq(merge_request.diffs.size)
@@ -209,7 +209,7 @@ describe API::API, api: true  do
 
   describe "POST /projects/:id/merge_requests" do
     context 'between branches projects' do
-      it "should return merge_request" do
+      it "returns merge_request" do
         post api("/projects/#{project.id}/merge_requests", user),
              title: 'Test merge_request',
              source_branch: 'feature_conflict',
@@ -223,31 +223,31 @@ describe API::API, api: true  do
         expect(json_response['milestone']['id']).to eq(milestone.id)
       end
 
-      it "should return 422 when source_branch equals target_branch" do
+      it "returns 422 when source_branch equals target_branch" do
         post api("/projects/#{project.id}/merge_requests", user),
         title: "Test merge_request", source_branch: "master", target_branch: "master", author: user
         expect(response).to have_http_status(422)
       end
 
-      it "should return 400 when source_branch is missing" do
+      it "returns 400 when source_branch is missing" do
         post api("/projects/#{project.id}/merge_requests", user),
         title: "Test merge_request", target_branch: "master", author: user
         expect(response).to have_http_status(400)
       end
 
-      it "should return 400 when target_branch is missing" do
+      it "returns 400 when target_branch is missing" do
         post api("/projects/#{project.id}/merge_requests", user),
         title: "Test merge_request", source_branch: "markdown", author: user
         expect(response).to have_http_status(400)
       end
 
-      it "should return 400 when title is missing" do
+      it "returns 400 when title is missing" do
         post api("/projects/#{project.id}/merge_requests", user),
         target_branch: 'master', source_branch: 'markdown'
         expect(response).to have_http_status(400)
       end
 
-      it 'should allow special label names' do
+      it 'allows special label names' do
         post api("/projects/#{project.id}/merge_requests", user),
              title: 'Test merge_request',
              source_branch: 'markdown',
@@ -272,7 +272,7 @@ describe API::API, api: true  do
           @mr = MergeRequest.all.last
         end
 
-        it 'should return 409 when MR already exists for source/target' do
+        it 'returns 409 when MR already exists for source/target' do
           expect do
             post api("/projects/#{project.id}/merge_requests", user),
                  title: 'New test merge_request',
@@ -294,7 +294,7 @@ describe API::API, api: true  do
         fork_project.team << [user2, :reporters]
       end
 
-      it "should return merge_request" do
+      it "returns merge_request" do
         post api("/projects/#{fork_project.id}/merge_requests", user2),
           title: 'Test merge_request', source_branch: "feature_conflict", target_branch: "master",
           author: user2, target_project_id: project.id, description: 'Test description for Test merge_request'
@@ -303,7 +303,7 @@ describe API::API, api: true  do
         expect(json_response['description']).to eq('Test description for Test merge_request')
       end
 
-      it "should not return 422 when source_branch equals target_branch" do
+      it "does not return 422 when source_branch equals target_branch" do
         expect(project.id).not_to eq(fork_project.id)
         expect(fork_project.forked?).to be_truthy
         expect(fork_project.forked_from_project).to eq(project)
@@ -313,26 +313,26 @@ describe API::API, api: true  do
         expect(json_response['title']).to eq('Test merge_request')
       end
 
-      it "should return 400 when source_branch is missing" do
+      it "returns 400 when source_branch is missing" do
         post api("/projects/#{fork_project.id}/merge_requests", user2),
         title: 'Test merge_request', target_branch: "master", author: user2, target_project_id: project.id
         expect(response).to have_http_status(400)
       end
 
-      it "should return 400 when target_branch is missing" do
+      it "returns 400 when target_branch is missing" do
         post api("/projects/#{fork_project.id}/merge_requests", user2),
         title: 'Test merge_request', target_branch: "master", author: user2, target_project_id: project.id
         expect(response).to have_http_status(400)
       end
 
-      it "should return 400 when title is missing" do
+      it "returns 400 when title is missing" do
         post api("/projects/#{fork_project.id}/merge_requests", user2),
         target_branch: 'master', source_branch: 'markdown', author: user2, target_project_id: project.id
         expect(response).to have_http_status(400)
       end
 
       context 'when target_branch is specified' do
-        it 'should return 422 if not a forked project' do
+        it 'returns 422 if not a forked project' do
           post api("/projects/#{project.id}/merge_requests", user),
                title: 'Test merge_request',
                target_branch: 'master',
@@ -342,7 +342,7 @@ describe API::API, api: true  do
           expect(response).to have_http_status(422)
         end
 
-        it 'should return 422 if targeting a different fork' do
+        it 'returns 422 if targeting a different fork' do
           post api("/projects/#{fork_project.id}/merge_requests", user2),
                title: 'Test merge_request',
                target_branch: 'master',
@@ -353,7 +353,7 @@ describe API::API, api: true  do
         end
       end
 
-      it "should return 201 when target_branch is specified and for the same project" do
+      it "returns 201 when target_branch is specified and for the same project" do
         post api("/projects/#{fork_project.id}/merge_requests", user2),
         title: 'Test merge_request', target_branch: 'master', source_branch: 'markdown', author: user2, target_project_id: fork_project.id
         expect(response).to have_http_status(201)
@@ -385,7 +385,7 @@ describe API::API, api: true  do
   end
 
   describe "PUT /projects/:id/merge_requests/:merge_request_id to close MR" do
-    it "should return merge_request" do
+    it "returns merge_request" do
       put api("/projects/#{project.id}/merge_requests/#{merge_request.id}", user), state_event: "close"
       expect(response).to have_http_status(200)
       expect(json_response['state']).to eq('closed')
@@ -395,13 +395,13 @@ describe API::API, api: true  do
   describe "PUT /projects/:id/merge_requests/:merge_request_id/merge" do
     let(:pipeline) { create(:ci_pipeline_without_jobs) }
 
-    it "should return merge_request in case of success" do
+    it "returns merge_request in case of success" do
       put api("/projects/#{project.id}/merge_requests/#{merge_request.id}/merge", user)
 
       expect(response).to have_http_status(200)
     end
 
-    it "should return 406 if branch can't be merged" do
+    it "returns 406 if branch can't be merged" do
       allow_any_instance_of(MergeRequest).
         to receive(:can_be_merged?).and_return(false)
 
@@ -411,14 +411,14 @@ describe API::API, api: true  do
       expect(json_response['message']).to eq('Branch cannot be merged')
     end
 
-    it "should return 405 if merge_request is not open" do
+    it "returns 405 if merge_request is not open" do
       merge_request.close
       put api("/projects/#{project.id}/merge_requests/#{merge_request.id}/merge", user)
       expect(response).to have_http_status(405)
       expect(json_response['message']).to eq('405 Method Not Allowed')
     end
 
-    it "should return 405 if merge_request is a work in progress" do
+    it "returns 405 if merge_request is a work in progress" do
       merge_request.update_attribute(:title, "WIP: #{merge_request.title}")
       put api("/projects/#{project.id}/merge_requests/#{merge_request.id}/merge", user)
       expect(response).to have_http_status(405)
@@ -434,7 +434,7 @@ describe API::API, api: true  do
       expect(json_response['message']).to eq('405 Method Not Allowed')
     end
 
-    it "should return 401 if user has no permissions to merge" do
+    it "returns 401 if user has no permissions to merge" do
       user2 = create(:user)
       project.team << [user2, :reporter]
       put api("/projects/#{project.id}/merge_requests/#{merge_request.id}/merge", user2)
@@ -486,19 +486,19 @@ describe API::API, api: true  do
       expect(json_response['milestone']['id']).to eq(milestone.id)
     end
 
-    it "should return 400 when source_branch is specified" do
+    it "returns 400 when source_branch is specified" do
       put api("/projects/#{project.id}/merge_requests/#{merge_request.id}", user),
       source_branch: "master", target_branch: "master"
       expect(response).to have_http_status(400)
     end
 
-    it "should return merge_request with renamed target_branch" do
+    it "returns merge_request with renamed target_branch" do
       put api("/projects/#{project.id}/merge_requests/#{merge_request.id}", user), target_branch: "wiki"
       expect(response).to have_http_status(200)
       expect(json_response['target_branch']).to eq('wiki')
     end
 
-    it 'should allow special label names' do
+    it 'allows special label names' do
       put api("/projects/#{project.id}/merge_requests/#{merge_request.id}",
               user),
           title: 'new issue',
@@ -513,7 +513,7 @@ describe API::API, api: true  do
   end
 
   describe "POST /projects/:id/merge_requests/:merge_request_id/comments" do
-    it "should return comment" do
+    it "returns comment" do
       original_count = merge_request.notes.size
 
       post api("/projects/#{project.id}/merge_requests/#{merge_request.id}/comments", user), note: "My comment"
@@ -524,12 +524,12 @@ describe API::API, api: true  do
       expect(merge_request.notes.size).to eq(original_count + 1)
     end
 
-    it "should return 400 if note is missing" do
+    it "returns 400 if note is missing" do
       post api("/projects/#{project.id}/merge_requests/#{merge_request.id}/comments", user)
       expect(response).to have_http_status(400)
     end
 
-    it "should return 404 if note is attached to non existent merge request" do
+    it "returns 404 if note is attached to non existent merge request" do
       post api("/projects/#{project.id}/merge_requests/404/comments", user),
            note: 'My comment'
       expect(response).to have_http_status(404)
@@ -537,7 +537,7 @@ describe API::API, api: true  do
   end
 
   describe "GET :id/merge_requests/:merge_request_id/comments" do
-    it "should return merge_request comments ordered by created_at" do
+    it "returns merge_request comments ordered by created_at" do
       get api("/projects/#{project.id}/merge_requests/#{merge_request.id}/comments", user)
       expect(response).to have_http_status(200)
       expect(json_response).to be_an Array
@@ -547,7 +547,7 @@ describe API::API, api: true  do
       expect(json_response.last['note']).to eq("another comment on a MR")
     end
 
-    it "should return a 404 error if merge_request_id not found" do
+    it "returns a 404 error if merge_request_id not found" do
       get api("/projects/#{project.id}/merge_requests/999/comments", user)
       expect(response).to have_http_status(404)
     end
diff --git a/spec/requests/api/milestones_spec.rb b/spec/requests/api/milestones_spec.rb
index 0f4e38b24750dd32c3034cbba7f5e316b6d711a5..d6a0c656e7495dccdf43e21145a41d169962487e 100644
--- a/spec/requests/api/milestones_spec.rb
+++ b/spec/requests/api/milestones_spec.rb
@@ -10,14 +10,14 @@ describe API::API, api: true  do
   before { project.team << [user, :developer] }
 
   describe 'GET /projects/:id/milestones' do
-    it 'should return project 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)
     end
 
-    it 'should return a 401 error if user not authenticated' 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
@@ -42,14 +42,14 @@ describe API::API, api: true  do
   end
 
   describe 'GET /projects/:id/milestones/:milestone_id' do
-    it 'should return a project milestone by id' do
+    it 'returns a project milestone by id' do
       get api("/projects/#{project.id}/milestones/#{milestone.id}", user)
       expect(response).to have_http_status(200)
       expect(json_response['title']).to eq(milestone.title)
       expect(json_response['iid']).to eq(milestone.iid)
     end
 
-    it 'should return a project milestone by iid' do
+    it 'returns a project milestone by iid' do
       get api("/projects/#{project.id}/milestones?iid=#{closed_milestone.iid}", user)
 
       expect(response.status).to eq 200
@@ -58,26 +58,26 @@ describe API::API, api: true  do
       expect(json_response.first['id']).to eq closed_milestone.id
     end
 
-    it 'should return 401 error if user not authenticated' 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 'should return a 404 error if milestone id not found' do
+    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
 
   describe 'POST /projects/:id/milestones' do
-    it 'should create a new project milestone' 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
     end
 
-    it 'should create a new project milestone with description and due date' 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)
@@ -85,21 +85,21 @@ describe API::API, api: true  do
       expect(json_response['due_date']).to eq('2013-03-02')
     end
 
-    it 'should return a 400 error if title is missing' 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
   end
 
   describe 'PUT /projects/:id/milestones/:milestone_id' do
-    it 'should update a project milestone' 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 'should return a 404 error if milestone id not found' do
+    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)
@@ -107,7 +107,7 @@ describe API::API, api: true  do
   end
 
   describe 'PUT /projects/:id/milestones/:milestone_id to close milestone' do
-    it 'should update a project milestone' do
+    it 'updates a project milestone' do
       put api("/projects/#{project.id}/milestones/#{milestone.id}", user),
         state_event: 'close'
       expect(response).to have_http_status(200)
@@ -117,7 +117,7 @@ describe API::API, api: true  do
   end
 
   describe 'PUT /projects/:id/milestones/:milestone_id to test observer on close' do
-    it 'should create an activity event when an milestone is closed' do
+    it 'creates an activity event when an milestone is closed' do
       expect(Event).to receive(:create)
 
       put api("/projects/#{project.id}/milestones/#{milestone.id}", user),
@@ -129,14 +129,14 @@ describe API::API, api: true  do
     before do
       milestone.issues << create(:issue, project: project)
     end
-    it 'should return project issues for a particular milestone' do
+    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)
     end
 
-    it 'should return a 401 error if user not authenticated' 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
diff --git a/spec/requests/api/namespaces_spec.rb b/spec/requests/api/namespaces_spec.rb
index 237b4b17eb5c636dab922e9bf792a9b94888349e..5347cf4f7bcce9af6fc4b1c5c9a36ea00f7187b8 100644
--- a/spec/requests/api/namespaces_spec.rb
+++ b/spec/requests/api/namespaces_spec.rb
@@ -9,14 +9,14 @@ describe API::API, api: true  do
 
   describe "GET /namespaces" do
     context "when unauthenticated" do
-      it "should return authentication error" do
+      it "returns authentication error" do
         get api("/namespaces")
         expect(response).to have_http_status(401)
       end
     end
 
     context "when authenticated as admin" do
-      it "admin: should return an array of all namespaces" do
+      it "admin: returns an array of all namespaces" do
         get api("/namespaces", admin)
         expect(response).to have_http_status(200)
         expect(json_response).to be_an Array
@@ -24,7 +24,7 @@ describe API::API, api: true  do
         expect(json_response.length).to eq(Namespace.count)
       end
 
-      it "admin: should return an array of matched namespaces" do
+      it "admin: returns an array of matched namespaces" do
         get api("/namespaces?search=#{group1.name}", admin)
         expect(response).to have_http_status(200)
         expect(json_response).to be_an Array
@@ -34,7 +34,7 @@ describe API::API, api: true  do
     end
 
     context "when authenticated as a regular user" do
-      it "user: should return an array of namespaces" do
+      it "user: returns an array of namespaces" do
         get api("/namespaces", user)
         expect(response).to have_http_status(200)
         expect(json_response).to be_an Array
@@ -42,7 +42,7 @@ describe API::API, api: true  do
         expect(json_response.length).to eq(1)
       end
 
-      it "admin: should return an array of matched namespaces" do
+      it "admin: returns an array of matched namespaces" do
         get api("/namespaces?search=#{user.username}", user)
         expect(response).to have_http_status(200)
         expect(json_response).to be_an Array
diff --git a/spec/requests/api/notes_spec.rb b/spec/requests/api/notes_spec.rb
index 65c53211dd312d0a7b0dca6f8d465abce6fbcb33..737fa14cbb0c366a1ee1ad8b6fb5bc04f33fc2c1 100644
--- a/spec/requests/api/notes_spec.rb
+++ b/spec/requests/api/notes_spec.rb
@@ -37,7 +37,7 @@ describe API::API, api: true  do
     end
 
     context "when noteable is an Issue" do
-      it "should return an array of issue notes" do
+      it "returns an array of issue notes" do
         get api("/projects/#{project.id}/issues/#{issue.id}/notes", user)
 
         expect(response).to have_http_status(200)
@@ -45,14 +45,14 @@ describe API::API, api: true  do
         expect(json_response.first['body']).to eq(issue_note.note)
       end
 
-      it "should return a 404 error when issue id not found" do
+      it "returns a 404 error when issue id not found" do
         get api("/projects/#{project.id}/issues/12345/notes", user)
 
         expect(response).to have_http_status(404)
       end
 
       context "and current user cannot view the notes" do
-        it "should return an empty array" do
+        it "returns an empty array" do
           get api("/projects/#{ext_proj.id}/issues/#{ext_issue.id}/notes", user)
 
           expect(response).to have_http_status(200)
@@ -71,7 +71,7 @@ describe API::API, api: true  do
         end
 
         context "and current user can view the note" do
-          it "should return an empty array" do
+          it "returns an empty array" do
             get api("/projects/#{ext_proj.id}/issues/#{ext_issue.id}/notes", private_user)
 
             expect(response).to have_http_status(200)
@@ -83,7 +83,7 @@ describe API::API, api: true  do
     end
 
     context "when noteable is a Snippet" do
-      it "should return an array of snippet notes" do
+      it "returns an array of snippet notes" do
         get api("/projects/#{project.id}/snippets/#{snippet.id}/notes", user)
 
         expect(response).to have_http_status(200)
@@ -91,7 +91,7 @@ describe API::API, api: true  do
         expect(json_response.first['body']).to eq(snippet_note.note)
       end
 
-      it "should return a 404 error when snippet id not found" do
+      it "returns a 404 error when snippet id not found" do
         get api("/projects/#{project.id}/snippets/42/notes", user)
 
         expect(response).to have_http_status(404)
@@ -105,7 +105,7 @@ describe API::API, api: true  do
     end
 
     context "when noteable is a Merge Request" do
-      it "should return an array of merge_requests notes" do
+      it "returns an array of merge_requests notes" do
         get api("/projects/#{project.id}/merge_requests/#{merge_request.id}/notes", user)
 
         expect(response).to have_http_status(200)
@@ -113,7 +113,7 @@ describe API::API, api: true  do
         expect(json_response.first['body']).to eq(merge_request_note.note)
       end
 
-      it "should return a 404 error if merge request id not found" do
+      it "returns a 404 error if merge request id not found" do
         get api("/projects/#{project.id}/merge_requests/4444/notes", user)
 
         expect(response).to have_http_status(404)
@@ -129,21 +129,21 @@ describe API::API, api: true  do
 
   describe "GET /projects/:id/noteable/:noteable_id/notes/:note_id" do
     context "when noteable is an Issue" do
-      it "should return an issue note by id" do
+      it "returns an issue note by id" do
         get api("/projects/#{project.id}/issues/#{issue.id}/notes/#{issue_note.id}", user)
 
         expect(response).to have_http_status(200)
         expect(json_response['body']).to eq(issue_note.note)
       end
 
-      it "should return a 404 error if issue note not found" do
+      it "returns a 404 error if issue note not found" do
         get api("/projects/#{project.id}/issues/#{issue.id}/notes/12345", user)
 
         expect(response).to have_http_status(404)
       end
 
       context "and current user cannot view the note" do
-        it "should return a 404 error" do
+        it "returns a 404 error" do
           get api("/projects/#{ext_proj.id}/issues/#{ext_issue.id}/notes/#{cross_reference_note.id}", user)
 
           expect(response).to have_http_status(404)
@@ -160,7 +160,7 @@ describe API::API, api: true  do
         end
 
         context "and current user can view the note" do
-          it "should return an issue note by id" do
+          it "returns an issue note by id" do
             get api("/projects/#{ext_proj.id}/issues/#{ext_issue.id}/notes/#{cross_reference_note.id}", private_user)
 
             expect(response).to have_http_status(200)
@@ -171,14 +171,14 @@ describe API::API, api: true  do
     end
 
     context "when noteable is a Snippet" do
-      it "should return a snippet note by id" do
+      it "returns a snippet note by id" do
         get api("/projects/#{project.id}/snippets/#{snippet.id}/notes/#{snippet_note.id}", user)
 
         expect(response).to have_http_status(200)
         expect(json_response['body']).to eq(snippet_note.note)
       end
 
-      it "should return a 404 error if snippet note not found" do
+      it "returns a 404 error if snippet note not found" do
         get api("/projects/#{project.id}/snippets/#{snippet.id}/notes/12345", user)
 
         expect(response).to have_http_status(404)
@@ -188,7 +188,7 @@ describe API::API, api: true  do
 
   describe "POST /projects/:id/noteable/:noteable_id/notes" do
     context "when noteable is an Issue" do
-      it "should create a new issue note" do
+      it "creates a new issue note" do
         post api("/projects/#{project.id}/issues/#{issue.id}/notes", user), body: 'hi!'
 
         expect(response).to have_http_status(201)
@@ -196,13 +196,13 @@ describe API::API, api: true  do
         expect(json_response['author']['username']).to eq(user.username)
       end
 
-      it "should return a 400 bad request error if body not given" do
+      it "returns a 400 bad request error if body not given" do
         post api("/projects/#{project.id}/issues/#{issue.id}/notes", user)
 
         expect(response).to have_http_status(400)
       end
 
-      it "should return a 401 unauthorized error if user not authenticated" do
+      it "returns a 401 unauthorized error if user not authenticated" do
         post api("/projects/#{project.id}/issues/#{issue.id}/notes"), body: 'hi!'
 
         expect(response).to have_http_status(401)
@@ -223,7 +223,7 @@ describe API::API, api: true  do
     end
 
     context "when noteable is a Snippet" do
-      it "should create a new snippet note" do
+      it "creates a new snippet note" do
         post api("/projects/#{project.id}/snippets/#{snippet.id}/notes", user), body: 'hi!'
 
         expect(response).to have_http_status(201)
@@ -231,13 +231,13 @@ describe API::API, api: true  do
         expect(json_response['author']['username']).to eq(user.username)
       end
 
-      it "should return a 400 bad request error if body not given" do
+      it "returns a 400 bad request error if body not given" do
         post api("/projects/#{project.id}/snippets/#{snippet.id}/notes", user)
 
         expect(response).to have_http_status(400)
       end
 
-      it "should return a 401 unauthorized error if user not authenticated" do
+      it "returns a 401 unauthorized error if user not authenticated" do
         post api("/projects/#{project.id}/snippets/#{snippet.id}/notes"), body: 'hi!'
 
         expect(response).to have_http_status(401)
@@ -267,7 +267,7 @@ describe API::API, api: true  do
   end
 
   describe "POST /projects/:id/noteable/:noteable_id/notes to test observer on create" do
-    it "should create an activity event when an issue note is created" do
+    it "creates an activity event when an issue note is created" do
       expect(Event).to receive(:create)
 
       post api("/projects/#{project.id}/issues/#{issue.id}/notes", user), body: 'hi!'
@@ -276,7 +276,7 @@ describe API::API, api: true  do
 
   describe 'PUT /projects/:id/noteable/:noteable_id/notes/:note_id' do
     context 'when noteable is an Issue' do
-      it 'should return modified note' do
+      it 'returns modified note' do
         put api("/projects/#{project.id}/issues/#{issue.id}/"\
                   "notes/#{issue_note.id}", user), body: 'Hello!'
 
@@ -284,14 +284,14 @@ describe API::API, api: true  do
         expect(json_response['body']).to eq('Hello!')
       end
 
-      it 'should return a 404 error when note id not found' do
+      it 'returns a 404 error when note id not found' do
         put api("/projects/#{project.id}/issues/#{issue.id}/notes/12345", user),
                 body: 'Hello!'
 
         expect(response).to have_http_status(404)
       end
 
-      it 'should return a 400 bad request error if body not given' do
+      it 'returns a 400 bad request error if body not given' do
         put api("/projects/#{project.id}/issues/#{issue.id}/"\
                   "notes/#{issue_note.id}", user)
 
@@ -300,7 +300,7 @@ describe API::API, api: true  do
     end
 
     context 'when noteable is a Snippet' do
-      it 'should return modified note' do
+      it 'returns modified note' do
         put api("/projects/#{project.id}/snippets/#{snippet.id}/"\
                   "notes/#{snippet_note.id}", user), body: 'Hello!'
 
@@ -308,7 +308,7 @@ describe API::API, api: true  do
         expect(json_response['body']).to eq('Hello!')
       end
 
-      it 'should return a 404 error when note id not found' do
+      it 'returns a 404 error when note id not found' do
         put api("/projects/#{project.id}/snippets/#{snippet.id}/"\
                   "notes/12345", user), body: "Hello!"
 
@@ -317,7 +317,7 @@ describe API::API, api: true  do
     end
 
     context 'when noteable is a Merge Request' do
-      it 'should return modified note' do
+      it 'returns modified note' do
         put api("/projects/#{project.id}/merge_requests/#{merge_request.id}/"\
                   "notes/#{merge_request_note.id}", user), body: 'Hello!'
 
@@ -325,7 +325,7 @@ describe API::API, api: true  do
         expect(json_response['body']).to eq('Hello!')
       end
 
-      it 'should return a 404 error when note id not found' do
+      it 'returns a 404 error when note id not found' do
         put api("/projects/#{project.id}/merge_requests/#{merge_request.id}/"\
                   "notes/12345", user), body: "Hello!"
 
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 504deed81f981b4ed9764f962130f8474e36726c..914e88c94878d0cfa3b55b52f1496a51f2501a4b 100644
--- a/spec/requests/api/project_hooks_spec.rb
+++ b/spec/requests/api/project_hooks_spec.rb
@@ -7,10 +7,9 @@ describe API::API, 'ProjectHooks', api: true do
   let!(:project) { create(:project, creator_id: user.id, namespace: user.namespace) }
   let!(:hook) do
     create(:project_hook,
-           project: project, url: "http://example.com",
-           push_events: true, merge_requests_events: true,
-           tag_push_events: true, issues_events: true, note_events: true,
-           build_events: true, pipeline_events: true,
+           :all_events_enabled,
+           project: project,
+           url: 'http://example.com',
            enable_ssl_verification: true)
   end
 
@@ -21,7 +20,7 @@ describe API::API, 'ProjectHooks', api: true do
 
   describe "GET /projects/:id/hooks" do
     context "authorized user" do
-      it "should return project hooks" do
+      it "returns project hooks" do
         get api("/projects/#{project.id}/hooks", user)
         expect(response).to have_http_status(200)
 
@@ -40,7 +39,7 @@ describe API::API, 'ProjectHooks', api: true do
     end
 
     context "unauthorized user" do
-      it "should not access project hooks" do
+      it "does not access project hooks" do
         get api("/projects/#{project.id}/hooks", user3)
         expect(response).to have_http_status(403)
       end
@@ -49,7 +48,7 @@ describe API::API, 'ProjectHooks', api: true do
 
   describe "GET /projects/:id/hooks/:hook_id" do
     context "authorized user" do
-      it "should return a project hook" do
+      it "returns a project hook" do
         get api("/projects/#{project.id}/hooks/#{hook.id}", user)
         expect(response).to have_http_status(200)
         expect(json_response['url']).to eq(hook.url)
@@ -61,27 +60,27 @@ describe API::API, 'ProjectHooks', api: true do
         expect(json_response['enable_ssl_verification']).to eq(hook.enable_ssl_verification)
       end
 
-      it "should return a 404 error if hook id is not available" do
+      it "returns a 404 error if hook id is not available" do
         get api("/projects/#{project.id}/hooks/1234", user)
         expect(response).to have_http_status(404)
       end
     end
 
     context "unauthorized user" do
-      it "should not access an existing hook" do
+      it "does not access an existing hook" do
         get api("/projects/#{project.id}/hooks/#{hook.id}", user3)
         expect(response).to have_http_status(403)
       end
     end
 
-    it "should return a 404 error if hook id is not available" do
+    it "returns a 404 error if hook id is not available" do
       get api("/projects/#{project.id}/hooks/1234", user)
       expect(response).to have_http_status(404)
     end
   end
 
   describe "POST /projects/:id/hooks" do
-    it "should add hook to project" do
+    it "adds hook to project" 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)
@@ -97,19 +96,19 @@ describe API::API, 'ProjectHooks', api: true do
       expect(json_response['enable_ssl_verification']).to eq(true)
     end
 
-    it "should return a 400 error if url not given" do
+    it "returns a 400 error if url not given" do
       post api("/projects/#{project.id}/hooks", user)
       expect(response).to have_http_status(400)
     end
 
-    it "should return a 422 error if url not valid" do
+    it "returns a 422 error if url not valid" do
       post api("/projects/#{project.id}/hooks", user), "url" => "ftp://example.com"
       expect(response).to have_http_status(422)
     end
   end
 
   describe "PUT /projects/:id/hooks/:hook_id" do
-    it "should update an existing project hook" do
+    it "updates an existing project hook" do
       put api("/projects/#{project.id}/hooks/#{hook.id}", user),
         url: 'http://example.org', push_events: false
       expect(response).to have_http_status(200)
@@ -122,46 +121,46 @@ describe API::API, 'ProjectHooks', api: true do
       expect(json_response['enable_ssl_verification']).to eq(hook.enable_ssl_verification)
     end
 
-    it "should return 404 error if hook id not found" do
+    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)
     end
 
-    it "should return 400 error if url is not given" do
+    it "returns 400 error if url is not given" do
       put api("/projects/#{project.id}/hooks/#{hook.id}", user)
       expect(response).to have_http_status(400)
     end
 
-    it "should return a 422 error if url is not valid" do
+    it "returns a 422 error if url is not valid" do
       put api("/projects/#{project.id}/hooks/#{hook.id}", user), url: 'ftp://example.com'
       expect(response).to have_http_status(422)
     end
   end
 
   describe "DELETE /projects/:id/hooks/:hook_id" do
-    it "should delete hook from project" do
+    it "deletes hook from project" do
       expect do
         delete api("/projects/#{project.id}/hooks/#{hook.id}", user)
       end.to change {project.hooks.count}.by(-1)
       expect(response).to have_http_status(200)
     end
 
-    it "should return success when deleting hook" do
+    it "returns success when deleting hook" do
       delete api("/projects/#{project.id}/hooks/#{hook.id}", user)
       expect(response).to have_http_status(200)
     end
 
-    it "should return a 404 error when deleting non existent hook" do
+    it "returns a 404 error when deleting non existent hook" do
       delete api("/projects/#{project.id}/hooks/42", user)
       expect(response).to have_http_status(404)
     end
 
-    it "should return a 405 error if hook id not given" do
+    it "returns a 405 error if hook id not given" do
       delete api("/projects/#{project.id}/hooks", user)
       expect(response).to have_http_status(405)
     end
 
-    it "shold return a 404 if a user attempts to delete project hooks he/she does not own" do
+    it "returns a 404 if a user attempts to delete project hooks he/she does not own" do
       test_user = create(:user)
       other_project = create(:project)
       other_project.team << [test_user, :master]
diff --git a/spec/requests/api/project_members_spec.rb b/spec/requests/api/project_members_spec.rb
deleted file mode 100644
index 9a7c1da44018b4da1d293ce783efa32be27c4f45..0000000000000000000000000000000000000000
--- a/spec/requests/api/project_members_spec.rb
+++ /dev/null
@@ -1,166 +0,0 @@
-require 'spec_helper'
-
-describe API::API, api: true  do
-  include ApiHelpers
-  let(:user) { create(:user) }
-  let(:user2) { create(:user) }
-  let(:user3) { create(:user) }
-  let(:project) { create(:project, creator_id: user.id, namespace: user.namespace) }
-  let(:project_member) { create(:project_member, :master, user: user, project: project) }
-  let(:project_member2) { create(:project_member, :developer, user: user3, project: project) }
-
-  describe "GET /projects/:id/members" do
-    before { project_member }
-    before { project_member2 }
-
-    it "should return project team members" do
-      get api("/projects/#{project.id}/members", user)
-      expect(response).to have_http_status(200)
-      expect(json_response).to be_an Array
-      expect(json_response.count).to eq(2)
-      expect(json_response.map { |u| u['username'] }).to include user.username
-    end
-
-    it "finds team members with query string" do
-      get api("/projects/#{project.id}/members", user), query: user.username
-      expect(response).to have_http_status(200)
-      expect(json_response).to be_an Array
-      expect(json_response.count).to eq(1)
-      expect(json_response.first['username']).to eq(user.username)
-    end
-
-    it "should return a 404 error if id not found" do
-      get api("/projects/9999/members", user)
-      expect(response).to have_http_status(404)
-    end
-  end
-
-  describe "GET /projects/:id/members/:user_id" do
-    before { project_member }
-
-    it "should return project team member" do
-      get api("/projects/#{project.id}/members/#{user.id}", user)
-      expect(response).to have_http_status(200)
-      expect(json_response['username']).to eq(user.username)
-      expect(json_response['access_level']).to eq(ProjectMember::MASTER)
-    end
-
-    it "should return a 404 error if user id not found" do
-      get api("/projects/#{project.id}/members/1234", user)
-      expect(response).to have_http_status(404)
-    end
-  end
-
-  describe "POST /projects/:id/members" do
-    it "should add user to project team" do
-      expect do
-        post api("/projects/#{project.id}/members", user), user_id: user2.id, access_level: ProjectMember::DEVELOPER
-      end.to change { ProjectMember.count }.by(1)
-
-      expect(response).to have_http_status(201)
-      expect(json_response['username']).to eq(user2.username)
-      expect(json_response['access_level']).to eq(ProjectMember::DEVELOPER)
-    end
-
-    it "should return a 201 status if user is already project member" do
-      post api("/projects/#{project.id}/members", user),
-           user_id: user2.id,
-           access_level: ProjectMember::DEVELOPER
-      expect do
-        post api("/projects/#{project.id}/members", user), user_id: user2.id, access_level: ProjectMember::DEVELOPER
-      end.not_to change { ProjectMember.count }
-
-      expect(response).to have_http_status(201)
-      expect(json_response['username']).to eq(user2.username)
-      expect(json_response['access_level']).to eq(ProjectMember::DEVELOPER)
-    end
-
-    it "should return a 400 error when user id is not given" do
-      post api("/projects/#{project.id}/members", user), access_level: ProjectMember::MASTER
-      expect(response).to have_http_status(400)
-    end
-
-    it "should return a 400 error when access level is not given" do
-      post api("/projects/#{project.id}/members", user), user_id: user2.id
-      expect(response).to have_http_status(400)
-    end
-
-    it "should return a 422 error when access level is not known" do
-      post api("/projects/#{project.id}/members", user), user_id: user2.id, access_level: 1234
-      expect(response).to have_http_status(422)
-    end
-  end
-
-  describe "PUT /projects/:id/members/:user_id" do
-    before { project_member2 }
-
-    it "should update project team member" do
-      put api("/projects/#{project.id}/members/#{user3.id}", user), access_level: ProjectMember::MASTER
-      expect(response).to have_http_status(200)
-      expect(json_response['username']).to eq(user3.username)
-      expect(json_response['access_level']).to eq(ProjectMember::MASTER)
-    end
-
-    it "should return a 404 error if user_id is not found" do
-      put api("/projects/#{project.id}/members/1234", user), access_level: ProjectMember::MASTER
-      expect(response).to have_http_status(404)
-    end
-
-    it "should return a 400 error when access level is not given" do
-      put api("/projects/#{project.id}/members/#{user3.id}", user)
-      expect(response).to have_http_status(400)
-    end
-
-    it "should return a 422 error when access level is not known" do
-      put api("/projects/#{project.id}/members/#{user3.id}", user), access_level: 123
-      expect(response).to have_http_status(422)
-    end
-  end
-
-  describe "DELETE /projects/:id/members/:user_id" do
-    before do
-      project_member
-      project_member2
-    end
-
-    it "should remove user from project team" do
-      expect do
-        delete api("/projects/#{project.id}/members/#{user3.id}", user)
-      end.to change { ProjectMember.count }.by(-1)
-    end
-
-    it "should return 200 if team member is not part of a project" do
-      delete api("/projects/#{project.id}/members/#{user3.id}", user)
-      expect do
-        delete api("/projects/#{project.id}/members/#{user3.id}", user)
-      end.not_to change { ProjectMember.count }
-      expect(response).to have_http_status(200)
-    end
-
-    it "should return 200 if team member already removed" do
-      delete api("/projects/#{project.id}/members/#{user3.id}", user)
-      delete api("/projects/#{project.id}/members/#{user3.id}", user)
-      expect(response).to have_http_status(200)
-    end
-
-    it "should return 200 OK when the user was not member" do
-      expect do
-        delete api("/projects/#{project.id}/members/1000000", user)
-      end.to change { ProjectMember.count }.by(0)
-      expect(response).to have_http_status(200)
-      expect(json_response['id']).to eq(1000000)
-      expect(json_response['message']).to eq('Access revoked')
-    end
-
-    context 'when the user is not an admin or owner' do
-      it 'can leave the project' do
-        expect do
-          delete api("/projects/#{project.id}/members/#{user3.id}", user3)
-        end.to change { ProjectMember.count }.by(-1)
-
-        expect(response).to have_http_status(200)
-        expect(json_response['id']).to eq(project_member2.id)
-      end
-    end
-  end
-end
diff --git a/spec/requests/api/project_snippets_spec.rb b/spec/requests/api/project_snippets_spec.rb
index 4ebde20194115c3b0d6590f1d835174dfa0cac50..42757ff21b081d984dc0486fff3c32cb8dad32b0 100644
--- a/spec/requests/api/project_snippets_spec.rb
+++ b/spec/requests/api/project_snippets_spec.rb
@@ -17,7 +17,7 @@ describe API::API, api: true do
   end
 
   describe 'GET /projects/:project_id/snippets/' do
-    it 'all snippets available to team member' do
+    it 'returns all snippets available to team member' do
       project = create(:project, :public)
       user = create(:user)
       project.team << [user, :developer]
diff --git a/spec/requests/api/projects_spec.rb b/spec/requests/api/projects_spec.rb
index 8c6a7e6529d0f555b10d646c7bf05396035ec221..4742b3d0e374e69e609318fbc0f7a80783073b88 100644
--- a/spec/requests/api/projects_spec.rb
+++ b/spec/requests/api/projects_spec.rb
@@ -43,14 +43,14 @@ describe API::API, api: true  do
     before { project }
 
     context 'when unauthenticated' do
-      it 'should return authentication error' do
+      it 'returns authentication error' do
         get api('/projects')
         expect(response).to have_http_status(401)
       end
     end
 
     context 'when authenticated' do
-      it 'should return an array of projects' do
+      it 'returns an array of projects' do
         get api('/projects', user)
         expect(response).to have_http_status(200)
         expect(json_response).to be_an Array
@@ -58,21 +58,21 @@ describe API::API, api: true  do
         expect(json_response.first['owner']['username']).to eq(user.username)
       end
 
-      it 'should include the project labels as the tag_list' do
+      it 'includes the project labels as the tag_list' do
         get api('/projects', user)
         expect(response.status).to eq 200
         expect(json_response).to be_an Array
         expect(json_response.first.keys).to include('tag_list')
       end
 
-      it 'should include open_issues_count' do
+      it 'includes open_issues_count' do
         get api('/projects', user)
         expect(response.status).to eq 200
         expect(json_response).to be_an Array
         expect(json_response.first.keys).to include('open_issues_count')
       end
 
-      it 'should not include open_issues_count' do
+      it 'does not include open_issues_count' do
         project.update_attributes( { issues_enabled: false } )
 
         get api('/projects', user)
@@ -94,7 +94,7 @@ describe API::API, api: true  do
       end
 
       context 'and using search' do
-        it 'should return searched project' do
+        it 'returns searched project' do
           get api('/projects', user), { search: project.name }
           expect(response).to have_http_status(200)
           expect(json_response).to be_an Array
@@ -103,21 +103,21 @@ describe API::API, api: true  do
       end
 
       context 'and using the visibility filter' do
-        it 'should filter based on private visibility param' do
+        it 'filters based on private visibility param' do
           get api('/projects', user), { visibility: 'private' }
           expect(response).to have_http_status(200)
           expect(json_response).to be_an Array
           expect(json_response.length).to eq(user.namespace.projects.where(visibility_level: Gitlab::VisibilityLevel::PRIVATE).count)
         end
 
-        it 'should filter based on internal visibility param' do
+        it 'filters based on internal visibility param' do
           get api('/projects', user), { visibility: 'internal' }
           expect(response).to have_http_status(200)
           expect(json_response).to be_an Array
           expect(json_response.length).to eq(user.namespace.projects.where(visibility_level: Gitlab::VisibilityLevel::INTERNAL).count)
         end
 
-        it 'should filter based on public visibility param' do
+        it 'filters based on public visibility param' do
           get api('/projects', user), { visibility: 'public' }
           expect(response).to have_http_status(200)
           expect(json_response).to be_an Array
@@ -131,7 +131,7 @@ describe API::API, api: true  do
           project3
         end
 
-        it 'should return the correct order when sorted by id' do
+        it 'returns the correct order when sorted by id' do
           get api('/projects', user), { order_by: 'id', sort: 'desc' }
           expect(response).to have_http_status(200)
           expect(json_response).to be_an Array
@@ -145,21 +145,21 @@ describe API::API, api: true  do
     before { project }
 
     context 'when unauthenticated' do
-      it 'should return authentication error' do
+      it 'returns authentication error' do
         get api('/projects/all')
         expect(response).to have_http_status(401)
       end
     end
 
     context 'when authenticated as regular user' do
-      it 'should return authentication error' do
+      it 'returns authentication error' do
         get api('/projects/all', user)
         expect(response).to have_http_status(403)
       end
     end
 
     context 'when authenticated as admin' do
-      it 'should return an array of all projects' do
+      it 'returns an array of all projects' do
         get api('/projects/all', admin)
         expect(response).to have_http_status(200)
         expect(json_response).to be_an Array
@@ -183,7 +183,7 @@ describe API::API, api: true  do
       user3.update_attributes(starred_projects: [project, project2, project3, public_project])
     end
 
-    it 'should return the starred projects viewable by the user' do
+    it 'returns the starred projects viewable by the user' do
       get api('/projects/starred', user3)
       expect(response).to have_http_status(200)
       expect(json_response).to be_an Array
@@ -193,7 +193,7 @@ describe API::API, api: true  do
 
   describe 'POST /projects' do
     context 'maximum number of projects reached' do
-      it 'should not create new project and respond with 403' do
+      it 'does not create new project and respond with 403' do
         allow_any_instance_of(User).to receive(:projects_limit_left).and_return(0)
         expect { post api('/projects', user2), name: 'foo' }.
           to change {Project.count}.by(0)
@@ -201,24 +201,24 @@ describe API::API, api: true  do
       end
     end
 
-    it 'should create new project without path and return 201' do
+    it 'creates new project without path and return 201' do
       expect { post api('/projects', user), name: 'foo' }.
         to change { Project.count }.by(1)
       expect(response).to have_http_status(201)
     end
 
-    it 'should create last project before reaching project limit' do
+    it 'creates last project before reaching project limit' do
       allow_any_instance_of(User).to receive(:projects_limit_left).and_return(1)
       post api('/projects', user2), name: 'foo'
       expect(response).to have_http_status(201)
     end
 
-    it 'should not create new project without name and return 400' do
+    it 'does not create new project without name and return 400' do
       expect { post api('/projects', user) }.not_to change { Project.count }
       expect(response).to have_http_status(400)
     end
 
-    it "should assign attributes to project" do
+    it "assigns attributes to project" do
       project = attributes_for(:project, {
         path: 'camelCasePath',
         description: FFaker::Lorem.sentence,
@@ -234,42 +234,42 @@ describe API::API, api: true  do
       end
     end
 
-    it 'should set a project as public' do
+    it 'sets a project as public' do
       project = attributes_for(:project, :public)
       post api('/projects', user), project
       expect(json_response['public']).to be_truthy
       expect(json_response['visibility_level']).to eq(Gitlab::VisibilityLevel::PUBLIC)
     end
 
-    it 'should set a project as public using :public' do
+    it 'sets a project as public using :public' do
       project = attributes_for(:project, { public: true })
       post api('/projects', user), project
       expect(json_response['public']).to be_truthy
       expect(json_response['visibility_level']).to eq(Gitlab::VisibilityLevel::PUBLIC)
     end
 
-    it 'should set a project as internal' do
+    it 'sets a project as internal' do
       project = attributes_for(:project, :internal)
       post api('/projects', user), project
       expect(json_response['public']).to be_falsey
       expect(json_response['visibility_level']).to eq(Gitlab::VisibilityLevel::INTERNAL)
     end
 
-    it 'should set a project as internal overriding :public' do
+    it 'sets a project as internal overriding :public' do
       project = attributes_for(:project, :internal, { public: true })
       post api('/projects', user), project
       expect(json_response['public']).to be_falsey
       expect(json_response['visibility_level']).to eq(Gitlab::VisibilityLevel::INTERNAL)
     end
 
-    it 'should set a project as private' do
+    it 'sets a project as private' do
       project = attributes_for(:project, :private)
       post api('/projects', user), project
       expect(json_response['public']).to be_falsey
       expect(json_response['visibility_level']).to eq(Gitlab::VisibilityLevel::PRIVATE)
     end
 
-    it 'should set a project as private using :public' do
+    it 'sets a project as private using :public' do
       project = attributes_for(:project, { public: false })
       post api('/projects', user), project
       expect(json_response['public']).to be_falsey
@@ -282,7 +282,7 @@ describe API::API, api: true  do
         stub_application_setting(restricted_visibility_levels: [Gitlab::VisibilityLevel::PUBLIC])
       end
 
-      it 'should not allow a non-admin to use a restricted visibility level' do
+      it 'does not allow a non-admin to use a restricted visibility level' do
         post api('/projects', user), @project
 
         expect(response).to have_http_status(400)
@@ -291,7 +291,7 @@ describe API::API, api: true  do
         )
       end
 
-      it 'should allow an admin to override restricted visibility settings' do
+      it 'allows an admin to override restricted visibility settings' do
         post api('/projects', admin), @project
         expect(json_response['public']).to be_truthy
         expect(json_response['visibility_level']).to(
@@ -310,7 +310,7 @@ describe API::API, api: true  do
       expect(response).to have_http_status(201)
     end
 
-    it 'should respond with 400 on failure and not project' do
+    it 'responds with 400 on failure and not project' do
       expect { post api("/projects/user/#{user.id}", admin) }.
         not_to change { Project.count }
 
@@ -327,7 +327,7 @@ describe API::API, api: true  do
       ])
     end
 
-    it 'should assign attributes to project' do
+    it 'assigns attributes to project' do
       project = attributes_for(:project, {
         description: FFaker::Lorem.sentence,
         issues_enabled: false,
@@ -343,42 +343,42 @@ describe API::API, api: true  do
       end
     end
 
-    it 'should set a project as public' do
+    it 'sets a project as public' do
       project = attributes_for(:project, :public)
       post api("/projects/user/#{user.id}", admin), project
       expect(json_response['public']).to be_truthy
       expect(json_response['visibility_level']).to eq(Gitlab::VisibilityLevel::PUBLIC)
     end
 
-    it 'should set a project as public using :public' do
+    it 'sets a project as public using :public' do
       project = attributes_for(:project, { public: true })
       post api("/projects/user/#{user.id}", admin), project
       expect(json_response['public']).to be_truthy
       expect(json_response['visibility_level']).to eq(Gitlab::VisibilityLevel::PUBLIC)
     end
 
-    it 'should set a project as internal' do
+    it 'sets a project as internal' do
       project = attributes_for(:project, :internal)
       post api("/projects/user/#{user.id}", admin), project
       expect(json_response['public']).to be_falsey
       expect(json_response['visibility_level']).to eq(Gitlab::VisibilityLevel::INTERNAL)
     end
 
-    it 'should set a project as internal overriding :public' do
+    it 'sets a project as internal overriding :public' do
       project = attributes_for(:project, :internal, { public: true })
       post api("/projects/user/#{user.id}", admin), project
       expect(json_response['public']).to be_falsey
       expect(json_response['visibility_level']).to eq(Gitlab::VisibilityLevel::INTERNAL)
     end
 
-    it 'should set a project as private' do
+    it 'sets a project as private' do
       project = attributes_for(:project, :private)
       post api("/projects/user/#{user.id}", admin), project
       expect(json_response['public']).to be_falsey
       expect(json_response['visibility_level']).to eq(Gitlab::VisibilityLevel::PRIVATE)
     end
 
-    it 'should set a project as private using :public' do
+    it 'sets a project as private using :public' do
       project = attributes_for(:project, { public: false })
       post api("/projects/user/#{user.id}", admin), project
       expect(json_response['public']).to be_falsey
@@ -446,25 +446,25 @@ describe API::API, api: true  do
       expect(json_response['shared_with_groups'][0]['group_access_level']).to eq(link.group_access)
     end
 
-    it 'should return a project by path name' do
+    it 'returns a project by path name' do
       get api("/projects/#{project.id}", user)
       expect(response).to have_http_status(200)
       expect(json_response['name']).to eq(project.name)
     end
 
-    it 'should return a 404 error if not found' do
+    it 'returns a 404 error if not found' do
       get api('/projects/42', user)
       expect(response).to have_http_status(404)
       expect(json_response['message']).to eq('404 Project Not Found')
     end
 
-    it 'should return a 404 error if user is not a member' do
+    it 'returns a 404 error if user is not a member' do
       other_user = create(:user)
       get api("/projects/#{project.id}", other_user)
       expect(response).to have_http_status(404)
     end
 
-    it 'should handle users with dots' do
+    it 'handles users with dots' do
       dot_user = create(:user, username: 'dot.user')
       project = create(:project, creator_id: dot_user.id, namespace: dot_user.namespace)
 
@@ -504,7 +504,7 @@ describe API::API, api: true  do
 
         before { project2.group.add_owner(user) }
 
-        it 'should set the owner and return 200' do
+        it 'sets the owner and return 200' do
           get api("/projects/#{project2.id}", user)
 
           expect(response).to have_http_status(200)
@@ -545,13 +545,13 @@ describe API::API, api: true  do
       end
     end
 
-    it 'should return a 404 error if not found' do
+    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 'should return a 404 error if user is not a member' do
+    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)
@@ -561,7 +561,7 @@ describe API::API, api: true  do
   describe 'GET /projects/:id/snippets' do
     before { snippet }
 
-    it 'should return an array of project snippets' do
+    it 'returns an array of project snippets' do
       get api("/projects/#{project.id}/snippets", user)
       expect(response).to have_http_status(200)
       expect(json_response).to be_an Array
@@ -570,20 +570,20 @@ describe API::API, api: true  do
   end
 
   describe 'GET /projects/:id/snippets/:snippet_id' do
-    it 'should return a project snippet' do
+    it 'returns a project snippet' do
       get api("/projects/#{project.id}/snippets/#{snippet.id}", user)
       expect(response).to have_http_status(200)
       expect(json_response['title']).to eq(snippet.title)
     end
 
-    it 'should return a 404 error if snippet id not found' do
+    it 'returns a 404 error if snippet id not found' do
       get api("/projects/#{project.id}/snippets/1234", user)
       expect(response).to have_http_status(404)
     end
   end
 
   describe 'POST /projects/:id/snippets' do
-    it 'should create a new project snippet' do
+    it 'creates a new project snippet' do
       post api("/projects/#{project.id}/snippets", user),
         title: 'api test', file_name: 'sample.rb', code: 'test',
         visibility_level: '0'
@@ -591,14 +591,14 @@ describe API::API, api: true  do
       expect(json_response['title']).to eq('api test')
     end
 
-    it 'should return a 400 error if invalid snippet is given' do
+    it 'returns a 400 error if invalid snippet is given' do
       post api("/projects/#{project.id}/snippets", user)
       expect(status).to eq(400)
     end
   end
 
   describe 'PUT /projects/:id/snippets/:snippet_id' do
-    it 'should update an existing project snippet' do
+    it 'updates an existing project snippet' do
       put api("/projects/#{project.id}/snippets/#{snippet.id}", user),
         code: 'updated code'
       expect(response).to have_http_status(200)
@@ -606,7 +606,7 @@ describe API::API, api: true  do
       expect(snippet.reload.content).to eq('updated code')
     end
 
-    it 'should update an existing project snippet with new title' do
+    it 'updates an existing project snippet with new title' do
       put api("/projects/#{project.id}/snippets/#{snippet.id}", user),
         title: 'other api test'
       expect(response).to have_http_status(200)
@@ -617,103 +617,31 @@ describe API::API, api: true  do
   describe 'DELETE /projects/:id/snippets/:snippet_id' do
     before { snippet }
 
-    it 'should delete existing project snippet' do
+    it 'deletes existing project snippet' do
       expect do
         delete api("/projects/#{project.id}/snippets/#{snippet.id}", user)
       end.to change { Snippet.count }.by(-1)
       expect(response).to have_http_status(200)
     end
 
-    it 'should return 404 when deleting unknown snippet id' do
+    it 'returns 404 when deleting unknown snippet id' do
       delete api("/projects/#{project.id}/snippets/1234", user)
       expect(response).to have_http_status(404)
     end
   end
 
   describe 'GET /projects/:id/snippets/:snippet_id/raw' do
-    it 'should get a raw project snippet' do
+    it 'gets a raw project snippet' do
       get api("/projects/#{project.id}/snippets/#{snippet.id}/raw", user)
       expect(response).to have_http_status(200)
     end
 
-    it 'should return a 404 error if raw project snippet not found' do
+    it 'returns a 404 error if raw project snippet not found' do
       get api("/projects/#{project.id}/snippets/5555/raw", user)
       expect(response).to have_http_status(404)
     end
   end
 
-  describe :deploy_keys do
-    let(:deploy_keys_project) { create(:deploy_keys_project, project: project) }
-    let(:deploy_key) { deploy_keys_project.deploy_key }
-
-    describe 'GET /projects/:id/deploy_keys' do
-      before { deploy_key }
-
-      it 'should return array of ssh keys' do
-        get api("/projects/#{project.id}/deploy_keys", user)
-        expect(response).to have_http_status(200)
-        expect(json_response).to be_an Array
-        expect(json_response.first['title']).to eq(deploy_key.title)
-      end
-    end
-
-    describe 'GET /projects/:id/deploy_keys/:key_id' do
-      it 'should return a single key' do
-        get api("/projects/#{project.id}/deploy_keys/#{deploy_key.id}", user)
-        expect(response).to have_http_status(200)
-        expect(json_response['title']).to eq(deploy_key.title)
-      end
-
-      it 'should return 404 Not Found with invalid ID' do
-        get api("/projects/#{project.id}/deploy_keys/404", user)
-        expect(response).to have_http_status(404)
-      end
-    end
-
-    describe 'POST /projects/:id/deploy_keys' do
-      it 'should not create an invalid ssh key' do
-        post api("/projects/#{project.id}/deploy_keys", user), { title: 'invalid key' }
-        expect(response).to have_http_status(400)
-        expect(json_response['message']['key']).to eq([
-          'can\'t be blank',
-          'is too short (minimum is 0 characters)',
-          'is invalid'
-        ])
-      end
-
-      it 'should not create a key without title' do
-        post api("/projects/#{project.id}/deploy_keys", user), key: 'some key'
-        expect(response).to have_http_status(400)
-        expect(json_response['message']['title']).to eq([
-          'can\'t be blank',
-          'is too short (minimum is 0 characters)'
-        ])
-      end
-
-      it 'should create new ssh key' do
-        key_attrs = attributes_for :key
-        expect do
-          post api("/projects/#{project.id}/deploy_keys", user), key_attrs
-        end.to change{ project.deploy_keys.count }.by(1)
-      end
-    end
-
-    describe 'DELETE /projects/:id/deploy_keys/:key_id' do
-      before { deploy_key }
-
-      it 'should delete existing key' do
-        expect do
-          delete api("/projects/#{project.id}/deploy_keys/#{deploy_key.id}", user)
-        end.to change{ project.deploy_keys.count }.by(-1)
-      end
-
-      it 'should return 404 Not Found with invalid ID' do
-        delete api("/projects/#{project.id}/deploy_keys/404", user)
-        expect(response).to have_http_status(404)
-      end
-    end
-  end
-
   describe :fork_admin do
     let(:project_fork_target) { create(:project) }
     let(:project_fork_source) { create(:project, :public) }
@@ -721,12 +649,12 @@ describe API::API, api: true  do
     describe 'POST /projects/:id/fork/:forked_from_id' do
       let(:new_project_fork_source) { create(:project, :public) }
 
-      it "shouldn't available for non admin users" do
+      it "is not available for non admin users" do
         post api("/projects/#{project_fork_target.id}/fork/#{project_fork_source.id}", user)
         expect(response).to have_http_status(403)
       end
 
-      it 'should allow project to be forked from an existing project' do
+      it 'allows project to be forked from an existing project' do
         expect(project_fork_target.forked?).not_to be_truthy
         post api("/projects/#{project_fork_target.id}/fork/#{project_fork_source.id}", admin)
         expect(response).to have_http_status(201)
@@ -736,12 +664,12 @@ describe API::API, api: true  do
         expect(project_fork_target.forked?).to be_truthy
       end
 
-      it 'should fail if forked_from project which does not exist' do
+      it 'fails if forked_from project which does not exist' do
         post api("/projects/#{project_fork_target.id}/fork/9999", admin)
         expect(response).to have_http_status(404)
       end
 
-      it 'should fail with 409 if already forked' do
+      it 'fails with 409 if already forked' do
         post api("/projects/#{project_fork_target.id}/fork/#{project_fork_source.id}", admin)
         project_fork_target.reload
         expect(project_fork_target.forked_from_project.id).to eq(project_fork_source.id)
@@ -754,7 +682,7 @@ describe API::API, api: true  do
     end
 
     describe 'DELETE /projects/:id/fork' do
-      it "shouldn't be visible to users outside group" do
+      it "is not visible to users outside group" do
         delete api("/projects/#{project_fork_target.id}/fork", user)
         expect(response).to have_http_status(404)
       end
@@ -767,12 +695,12 @@ describe API::API, api: true  do
           project_fork_target.group.add_developer user2
         end
 
-        it 'should be forbidden to non-owner users' do
+        it 'is forbidden to non-owner users' do
           delete api("/projects/#{project_fork_target.id}/fork", user2)
           expect(response).to have_http_status(403)
         end
 
-        it 'should make forked project unforked' do
+        it 'makes forked project unforked' do
           post api("/projects/#{project_fork_target.id}/fork/#{project_fork_source.id}", admin)
           project_fork_target.reload
           expect(project_fork_target.forked_from_project).not_to be_nil
@@ -784,7 +712,7 @@ describe API::API, api: true  do
           expect(project_fork_target.forked?).not_to be_truthy
         end
 
-        it 'should be idempotent if not forked' do
+        it 'is idempotent if not forked' do
           expect(project_fork_target.forked_from_project).to be_nil
           delete api("/projects/#{project_fork_target.id}/fork", admin)
           expect(response).to have_http_status(200)
@@ -797,7 +725,7 @@ describe API::API, api: true  do
   describe "POST /projects/:id/share" do
     let(:group) { create(:group) }
 
-    it "should share project with group" do
+    it "shares project with group" do
       expect do
         post api("/projects/#{project.id}/share", user), group_id: group.id, group_access: Gitlab::Access::DEVELOPER
       end.to change { ProjectGroupLink.count }.by(1)
@@ -807,23 +735,23 @@ describe API::API, api: true  do
       expect(json_response['group_access']).to eq Gitlab::Access::DEVELOPER
     end
 
-    it "should return a 400 error when group id is not given" do
+    it "returns a 400 error when group id is not given" do
       post api("/projects/#{project.id}/share", user), group_access: Gitlab::Access::DEVELOPER
       expect(response.status).to eq 400
     end
 
-    it "should return a 400 error when access level is not given" do
+    it "returns a 400 error when access level is not given" do
       post api("/projects/#{project.id}/share", user), group_id: group.id
       expect(response.status).to eq 400
     end
 
-    it "should return a 400 error when sharing is disabled" do
+    it "returns a 400 error when sharing is disabled" do
       project.namespace.update(share_with_group_lock: true)
       post api("/projects/#{project.id}/share", user), group_id: group.id, group_access: Gitlab::Access::DEVELOPER
       expect(response.status).to eq 400
     end
 
-    it "should return a 409 error when wrong params passed" do
+    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
       expect(json_response['message']).to eq 'Group access is not included in the list'
@@ -843,14 +771,14 @@ describe API::API, api: true  do
     let!(:unfound_public)   { create(:empty_project, :public, name: 'unfound public') }
 
     context 'when unauthenticated' do
-      it 'should return authentication error' do
+      it 'returns authentication error' do
         get api("/projects/search/#{query}")
         expect(response).to have_http_status(401)
       end
     end
 
     context 'when authenticated' do
-      it 'should return an array of projects' do
+      it 'returns an array of projects' do
         get api("/projects/search/#{query}", user)
         expect(response).to have_http_status(200)
         expect(json_response).to be_an Array
@@ -860,7 +788,7 @@ describe API::API, api: true  do
     end
 
     context 'when authenticated as a different user' do
-      it 'should return matching public projects' do
+      it 'returns matching public projects' do
         get api("/projects/search/#{query}", user2)
         expect(response).to have_http_status(200)
         expect(json_response).to be_an Array
@@ -881,7 +809,7 @@ describe API::API, api: true  do
     before { project_member2 }
 
     context 'when unauthenticated' do
-      it 'should return authentication error' do
+      it 'returns authentication error' do
         project_param = { name: 'bar' }
         put api("/projects/#{project.id}"), project_param
         expect(response).to have_http_status(401)
@@ -889,7 +817,7 @@ describe API::API, api: true  do
     end
 
     context 'when authenticated as project owner' do
-      it 'should update name' do
+      it 'updates name' do
         project_param = { name: 'bar' }
         put api("/projects/#{project.id}", user), project_param
         expect(response).to have_http_status(200)
@@ -898,7 +826,7 @@ describe API::API, api: true  do
         end
       end
 
-      it 'should update visibility_level' do
+      it 'updates visibility_level' do
         project_param = { visibility_level: 20 }
         put api("/projects/#{project3.id}", user), project_param
         expect(response).to have_http_status(200)
@@ -907,7 +835,7 @@ describe API::API, api: true  do
         end
       end
 
-      it 'should update visibility_level from public to private' do
+      it 'updates visibility_level from public to private' do
         project3.update_attributes({ visibility_level: Gitlab::VisibilityLevel::PUBLIC })
 
         project_param = { public: false }
@@ -919,14 +847,14 @@ describe API::API, api: true  do
         expect(json_response['visibility_level']).to eq(Gitlab::VisibilityLevel::PRIVATE)
       end
 
-      it 'should not update name to existing name' do
+      it 'does not update name to existing name' do
         project_param = { name: project3.name }
         put api("/projects/#{project.id}", user), project_param
         expect(response).to have_http_status(400)
         expect(json_response['message']['name']).to eq(['has already been taken'])
       end
 
-      it 'should update path & name to existing path & name in different namespace' do
+      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
         expect(response).to have_http_status(200)
@@ -937,7 +865,7 @@ describe API::API, api: true  do
     end
 
     context 'when authenticated as project master' do
-      it 'should update path' do
+      it 'updates path' do
         project_param = { path: 'bar' }
         put api("/projects/#{project3.id}", user4), project_param
         expect(response).to have_http_status(200)
@@ -946,7 +874,7 @@ describe API::API, api: true  do
         end
       end
 
-      it 'should update other attributes' do
+      it 'updates other attributes' do
         project_param = { issues_enabled: true,
                           wiki_enabled: true,
                           snippets_enabled: true,
@@ -960,20 +888,20 @@ describe API::API, api: true  do
         end
       end
 
-      it 'should not update path to existing path' do
+      it 'does not update path to existing path' do
         project_param = { path: project.path }
         put api("/projects/#{project3.id}", user4), project_param
         expect(response).to have_http_status(400)
         expect(json_response['message']['path']).to eq(['has already been taken'])
       end
 
-      it 'should not update name' do
+      it 'does not update name' do
         project_param = { name: 'bar' }
         put api("/projects/#{project3.id}", user4), project_param
         expect(response).to have_http_status(403)
       end
 
-      it 'should not update visibility_level' do
+      it 'does not update visibility_level' do
         project_param = { visibility_level: 20 }
         put api("/projects/#{project3.id}", user4), project_param
         expect(response).to have_http_status(403)
@@ -981,7 +909,7 @@ describe API::API, api: true  do
     end
 
     context 'when authenticated as project developer' do
-      it 'should not update other attributes' do
+      it 'does not update other attributes' do
         project_param = { path: 'bar',
                           issues_enabled: true,
                           wiki_enabled: true,
@@ -1116,36 +1044,36 @@ describe API::API, api: true  do
 
   describe 'DELETE /projects/:id' do
     context 'when authenticated as user' do
-      it 'should remove project' do
+      it 'removes project' do
         delete api("/projects/#{project.id}", user)
         expect(response).to have_http_status(200)
       end
 
-      it 'should not remove a project if not an owner' do
+      it 'does not remove a project if not an owner' do
         user3 = create(:user)
         project.team << [user3, :developer]
         delete api("/projects/#{project.id}", user3)
         expect(response).to have_http_status(403)
       end
 
-      it 'should not remove a non existing project' do
+      it 'does not remove a non existing project' do
         delete api('/projects/1328', user)
         expect(response).to have_http_status(404)
       end
 
-      it 'should not remove a project not attached to user' do
+      it 'does not remove a project not attached to user' do
         delete api("/projects/#{project.id}", user2)
         expect(response).to have_http_status(404)
       end
     end
 
     context 'when authenticated as admin' do
-      it 'should remove any existing project' do
+      it 'removes any existing project' do
         delete api("/projects/#{project.id}", admin)
         expect(response).to have_http_status(200)
       end
 
-      it 'should not remove a non existing project' do
+      it 'does not remove a non existing project' do
         delete api('/projects/1328', admin)
         expect(response).to have_http_status(404)
       end
diff --git a/spec/requests/api/repositories_spec.rb b/spec/requests/api/repositories_spec.rb
index 5890e9c9d3d97f3ef431d2f447120399aec790d5..80a856a6e9095dcd4bc771e72b6b594dfb22232d 100644
--- a/spec/requests/api/repositories_spec.rb
+++ b/spec/requests/api/repositories_spec.rb
@@ -16,7 +16,7 @@ describe API::API, api: true  do
     context "authorized user" do
       before { project.team << [user2, :reporter] }
 
-      it "should return project commits" do
+      it "returns project commits" do
         get api("/projects/#{project.id}/repository/tree", user)
         expect(response).to have_http_status(200)
 
@@ -26,7 +26,7 @@ describe API::API, api: true  do
         expect(json_response.first['mode']).to eq('040000')
       end
 
-      it 'should return a 404 for unknown ref' do
+      it 'returns a 404 for unknown ref' do
         get api("/projects/#{project.id}/repository/tree?ref_name=foo", user)
         expect(response).to have_http_status(404)
 
@@ -36,7 +36,7 @@ describe API::API, api: true  do
     end
 
     context "unauthorized user" do
-      it "should not return project commits" do
+      it "does not return project commits" do
         get api("/projects/#{project.id}/repository/tree")
         expect(response).to have_http_status(401)
       end
@@ -44,41 +44,41 @@ describe API::API, api: true  do
   end
 
   describe "GET /projects/:id/repository/blobs/:sha" do
-    it "should get the raw file contents" do
+    it "gets the raw file contents" do
       get api("/projects/#{project.id}/repository/blobs/master?filepath=README.md", user)
       expect(response).to have_http_status(200)
     end
 
-    it "should return 404 for invalid branch_name" do
+    it "returns 404 for invalid branch_name" do
       get api("/projects/#{project.id}/repository/blobs/invalid_branch_name?filepath=README.md", user)
       expect(response).to have_http_status(404)
     end
 
-    it "should return 404 for invalid file" do
+    it "returns 404 for invalid file" do
       get api("/projects/#{project.id}/repository/blobs/master?filepath=README.invalid", user)
       expect(response).to have_http_status(404)
     end
 
-    it "should return a 400 error if filepath is missing" do
+    it "returns a 400 error if filepath is missing" do
       get api("/projects/#{project.id}/repository/blobs/master", user)
       expect(response).to have_http_status(400)
     end
   end
 
   describe "GET /projects/:id/repository/commits/:sha/blob" do
-    it "should get the raw file contents" do
+    it "gets the raw file contents" do
       get api("/projects/#{project.id}/repository/commits/master/blob?filepath=README.md", user)
       expect(response).to have_http_status(200)
     end
   end
 
   describe "GET /projects/:id/repository/raw_blobs/:sha" do
-    it "should get the raw file contents" do
+    it "gets the raw file contents" do
       get api("/projects/#{project.id}/repository/raw_blobs/#{sample_blob.oid}", user)
       expect(response).to have_http_status(200)
     end
 
-    it 'should return a 404 for unknown blob' do
+    it 'returns a 404 for unknown blob' do
       get api("/projects/#{project.id}/repository/raw_blobs/123456", user)
       expect(response).to have_http_status(404)
 
@@ -88,7 +88,7 @@ describe API::API, api: true  do
   end
 
   describe "GET /projects/:id/repository/archive(.:format)?:sha" do
-    it "should get the archive" do
+    it "gets the archive" do
       get api("/projects/#{project.id}/repository/archive", user)
       repo_name = project.repository.name.gsub("\.git", "")
       expect(response).to have_http_status(200)
@@ -97,7 +97,7 @@ describe API::API, api: true  do
       expect(params['ArchivePath']).to match(/#{repo_name}\-[^\.]+\.tar.gz/)
     end
 
-    it "should get the archive.zip" do
+    it "gets the archive.zip" do
       get api("/projects/#{project.id}/repository/archive.zip", user)
       repo_name = project.repository.name.gsub("\.git", "")
       expect(response).to have_http_status(200)
@@ -106,7 +106,7 @@ describe API::API, api: true  do
       expect(params['ArchivePath']).to match(/#{repo_name}\-[^\.]+\.zip/)
     end
 
-    it "should get the archive.tar.bz2" do
+    it "gets the archive.tar.bz2" do
       get api("/projects/#{project.id}/repository/archive.tar.bz2", user)
       repo_name = project.repository.name.gsub("\.git", "")
       expect(response).to have_http_status(200)
@@ -115,28 +115,28 @@ describe API::API, api: true  do
       expect(params['ArchivePath']).to match(/#{repo_name}\-[^\.]+\.tar.bz2/)
     end
 
-    it "should return 404 for invalid sha" do
+    it "returns 404 for invalid sha" do
       get api("/projects/#{project.id}/repository/archive/?sha=xxx", user)
       expect(response).to have_http_status(404)
     end
   end
 
   describe 'GET /projects/:id/repository/compare' do
-    it "should compare branches" do
+    it "compares branches" do
       get api("/projects/#{project.id}/repository/compare", user), from: 'master', to: 'feature'
       expect(response).to have_http_status(200)
       expect(json_response['commits']).to be_present
       expect(json_response['diffs']).to be_present
     end
 
-    it "should compare tags" do
+    it "compares tags" do
       get api("/projects/#{project.id}/repository/compare", user), from: 'v1.0.0', to: 'v1.1.0'
       expect(response).to have_http_status(200)
       expect(json_response['commits']).to be_present
       expect(json_response['diffs']).to be_present
     end
 
-    it "should compare commits" do
+    it "compares commits" do
       get api("/projects/#{project.id}/repository/compare", user), from: sample_commit.id, to: sample_commit.parent_id
       expect(response).to have_http_status(200)
       expect(json_response['commits']).to be_empty
@@ -144,14 +144,14 @@ describe API::API, api: true  do
       expect(json_response['compare_same_ref']).to be_falsey
     end
 
-    it "should compare commits in reverse order" do
+    it "compares commits in reverse order" do
       get api("/projects/#{project.id}/repository/compare", user), from: sample_commit.parent_id, to: sample_commit.id
       expect(response).to have_http_status(200)
       expect(json_response['commits']).to be_present
       expect(json_response['diffs']).to be_present
     end
 
-    it "should compare same refs" do
+    it "compares same refs" do
       get api("/projects/#{project.id}/repository/compare", user), from: 'master', to: 'master'
       expect(response).to have_http_status(200)
       expect(json_response['commits']).to be_empty
@@ -161,7 +161,7 @@ describe API::API, api: true  do
   end
 
   describe 'GET /projects/:id/repository/contributors' do
-    it 'should return valid data' do
+    it 'returns valid data' do
       get api("/projects/#{project.id}/repository/contributors", user)
       expect(response).to have_http_status(200)
       expect(json_response).to be_an Array
diff --git a/spec/requests/api/runners_spec.rb b/spec/requests/api/runners_spec.rb
index 00a3c917b6a3e3fd1c6499546e9cde412d98104a..f46f016135ee0b7b815e486ba0c39057c9024ef3 100644
--- a/spec/requests/api/runners_spec.rb
+++ b/spec/requests/api/runners_spec.rb
@@ -35,7 +35,7 @@ describe API::Runners, api: true  do
 
   describe 'GET /runners' do
     context 'authorized user' do
-      it 'should return user available runners' do
+      it 'returns user available runners' do
         get api('/runners', user)
         shared = json_response.any?{ |r| r['is_shared'] }
 
@@ -44,7 +44,7 @@ describe API::Runners, api: true  do
         expect(shared).to be_falsey
       end
 
-      it 'should filter runners by scope' do
+      it 'filters runners by scope' do
         get api('/runners?scope=active', user)
         shared = json_response.any?{ |r| r['is_shared'] }
 
@@ -53,14 +53,14 @@ describe API::Runners, api: true  do
         expect(shared).to be_falsey
       end
 
-      it 'should avoid filtering if scope is invalid' do
+      it 'avoids filtering if scope is invalid' do
         get api('/runners?scope=unknown', user)
         expect(response).to have_http_status(400)
       end
     end
 
     context 'unauthorized user' do
-      it 'should not return runners' do
+      it 'does not return runners' do
         get api('/runners')
 
         expect(response).to have_http_status(401)
@@ -71,7 +71,7 @@ describe API::Runners, api: true  do
   describe 'GET /runners/all' do
     context 'authorized user' do
       context 'with admin privileges' do
-        it 'should return all runners' do
+        it 'returns all runners' do
           get api('/runners/all', admin)
           shared = json_response.any?{ |r| r['is_shared'] }
 
@@ -82,14 +82,14 @@ describe API::Runners, api: true  do
       end
 
       context 'without admin privileges' do
-        it 'should not return runners list' do
+        it 'does not return runners list' do
           get api('/runners/all', user)
 
           expect(response).to have_http_status(403)
         end
       end
 
-      it 'should filter runners by scope' do
+      it 'filters runners by scope' do
         get api('/runners/all?scope=specific', admin)
         shared = json_response.any?{ |r| r['is_shared'] }
 
@@ -98,14 +98,14 @@ describe API::Runners, api: true  do
         expect(shared).to be_falsey
       end
 
-      it 'should avoid filtering if scope is invalid' do
+      it 'avoids filtering if scope is invalid' do
         get api('/runners?scope=unknown', admin)
         expect(response).to have_http_status(400)
       end
     end
 
     context 'unauthorized user' do
-      it 'should not return runners' do
+      it 'does not return runners' do
         get api('/runners')
 
         expect(response).to have_http_status(401)
@@ -116,7 +116,7 @@ describe API::Runners, api: true  do
   describe 'GET /runners/:id' do
     context 'admin user' do
       context 'when runner is shared' do
-        it "should return runner's details" do
+        it "returns runner's details" do
           get api("/runners/#{shared_runner.id}", admin)
 
           expect(response).to have_http_status(200)
@@ -125,7 +125,7 @@ describe API::Runners, api: true  do
       end
 
       context 'when runner is not shared' do
-        it "should return runner's details" do
+        it "returns runner's details" do
           get api("/runners/#{specific_runner.id}", admin)
 
           expect(response).to have_http_status(200)
@@ -133,7 +133,7 @@ describe API::Runners, api: true  do
         end
       end
 
-      it 'should return 404 if runner does not exists' do
+      it 'returns 404 if runner does not exists' do
         get api('/runners/9999', admin)
 
         expect(response).to have_http_status(404)
@@ -142,7 +142,7 @@ describe API::Runners, api: true  do
 
     context "runner project's administrative user" do
       context 'when runner is not shared' do
-        it "should return runner's details" do
+        it "returns runner's details" do
           get api("/runners/#{specific_runner.id}", user)
 
           expect(response).to have_http_status(200)
@@ -151,7 +151,7 @@ describe API::Runners, api: true  do
       end
 
       context 'when runner is shared' do
-        it "should return runner's details" do
+        it "returns runner's details" do
           get api("/runners/#{shared_runner.id}", user)
 
           expect(response).to have_http_status(200)
@@ -161,7 +161,7 @@ describe API::Runners, api: true  do
     end
 
     context 'other authorized user' do
-      it "should not return runner's details" do
+      it "does not return runner's details" do
         get api("/runners/#{specific_runner.id}", user2)
 
         expect(response).to have_http_status(403)
@@ -169,7 +169,7 @@ describe API::Runners, api: true  do
     end
 
     context 'unauthorized user' do
-      it "should not return runner's details" do
+      it "does not return runner's details" do
         get api("/runners/#{specific_runner.id}")
 
         expect(response).to have_http_status(401)
@@ -180,7 +180,7 @@ describe API::Runners, api: true  do
   describe 'PUT /runners/:id' do
     context 'admin user' do
       context 'when runner is shared' do
-        it 'should update runner' do
+        it 'updates runner' do
           description = shared_runner.description
           active = shared_runner.active
 
@@ -201,7 +201,7 @@ describe API::Runners, api: true  do
       end
 
       context 'when runner is not shared' do
-        it 'should update runner' do
+        it 'updates runner' do
           description = specific_runner.description
           update_runner(specific_runner.id, admin, description: 'test')
           specific_runner.reload
@@ -212,7 +212,7 @@ describe API::Runners, api: true  do
         end
       end
 
-      it 'should return 404 if runner does not exists' do
+      it 'returns 404 if runner does not exists' do
         update_runner(9999, admin, description: 'test')
 
         expect(response).to have_http_status(404)
@@ -225,7 +225,7 @@ describe API::Runners, api: true  do
 
     context 'authorized user' do
       context 'when runner is shared' do
-        it 'should not update runner' do
+        it 'does not update runner' do
           put api("/runners/#{shared_runner.id}", user)
 
           expect(response).to have_http_status(403)
@@ -233,13 +233,13 @@ describe API::Runners, api: true  do
       end
 
       context 'when runner is not shared' do
-        it 'should not update runner without access to it' do
+        it 'does not update runner without access to it' do
           put api("/runners/#{specific_runner.id}", user2)
 
           expect(response).to have_http_status(403)
         end
 
-        it 'should update runner with access to it' do
+        it 'updates runner with access to it' do
           description = specific_runner.description
           put api("/runners/#{specific_runner.id}", admin), description: 'test'
           specific_runner.reload
@@ -252,7 +252,7 @@ describe API::Runners, api: true  do
     end
 
     context 'unauthorized user' do
-      it 'should not delete runner' do
+      it 'does not delete runner' do
         put api("/runners/#{specific_runner.id}")
 
         expect(response).to have_http_status(401)
@@ -263,7 +263,7 @@ describe API::Runners, api: true  do
   describe 'DELETE /runners/:id' do
     context 'admin user' do
       context 'when runner is shared' do
-        it 'should delete runner' do
+        it 'deletes runner' do
           expect do
             delete api("/runners/#{shared_runner.id}", admin)
           end.to change{ Ci::Runner.shared.count }.by(-1)
@@ -272,14 +272,14 @@ describe API::Runners, api: true  do
       end
 
       context 'when runner is not shared' do
-        it 'should delete unused runner' do
+        it 'deletes unused runner' do
           expect do
             delete api("/runners/#{unused_specific_runner.id}", admin)
           end.to change{ Ci::Runner.specific.count }.by(-1)
           expect(response).to have_http_status(200)
         end
 
-        it 'should delete used runner' do
+        it 'deletes used runner' do
           expect do
             delete api("/runners/#{specific_runner.id}", admin)
           end.to change{ Ci::Runner.specific.count }.by(-1)
@@ -287,7 +287,7 @@ describe API::Runners, api: true  do
         end
       end
 
-      it 'should return 404 if runner does not exists' do
+      it 'returns 404 if runner does not exists' do
         delete api('/runners/9999', admin)
 
         expect(response).to have_http_status(404)
@@ -296,24 +296,24 @@ describe API::Runners, api: true  do
 
     context 'authorized user' do
       context 'when runner is shared' do
-        it 'should not delete runner' do
+        it 'does not delete runner' do
           delete api("/runners/#{shared_runner.id}", user)
           expect(response).to have_http_status(403)
         end
       end
 
       context 'when runner is not shared' do
-        it 'should not delete runner without access to it' do
+        it 'does not delete runner without access to it' do
           delete api("/runners/#{specific_runner.id}", user2)
           expect(response).to have_http_status(403)
         end
 
-        it 'should not delete runner with more than one associated project' do
+        it 'does not delete runner with more than one associated project' do
           delete api("/runners/#{two_projects_runner.id}", user)
           expect(response).to have_http_status(403)
         end
 
-        it 'should delete runner for one owned project' do
+        it 'deletes runner for one owned project' do
           expect do
             delete api("/runners/#{specific_runner.id}", user)
           end.to change{ Ci::Runner.specific.count }.by(-1)
@@ -323,7 +323,7 @@ describe API::Runners, api: true  do
     end
 
     context 'unauthorized user' do
-      it 'should not delete runner' do
+      it 'does not delete runner' do
         delete api("/runners/#{specific_runner.id}")
 
         expect(response).to have_http_status(401)
@@ -333,7 +333,7 @@ describe API::Runners, api: true  do
 
   describe 'GET /projects/:id/runners' do
     context 'authorized user with master privileges' do
-      it "should return project's runners" do
+      it "returns project's runners" do
         get api("/projects/#{project.id}/runners", user)
         shared = json_response.any?{ |r| r['is_shared'] }
 
@@ -344,7 +344,7 @@ describe API::Runners, api: true  do
     end
 
     context 'authorized user without master privileges' do
-      it "should not return project's runners" do
+      it "does not return project's runners" do
         get api("/projects/#{project.id}/runners", user2)
 
         expect(response).to have_http_status(403)
@@ -352,7 +352,7 @@ describe API::Runners, api: true  do
     end
 
     context 'unauthorized user' do
-      it "should not return project's runners" do
+      it "does not return project's runners" do
         get api("/projects/#{project.id}/runners")
 
         expect(response).to have_http_status(401)
@@ -368,21 +368,21 @@ describe API::Runners, api: true  do
         end
       end
 
-      it 'should enable specific runner' do
+      it 'enables specific runner' do
         expect do
           post api("/projects/#{project.id}/runners", user), runner_id: specific_runner2.id
         end.to change{ project.runners.count }.by(+1)
         expect(response).to have_http_status(201)
       end
 
-      it 'should avoid changes when enabling already enabled runner' do
+      it 'avoids changes when enabling already enabled runner' do
         expect do
           post api("/projects/#{project.id}/runners", user), runner_id: specific_runner.id
         end.to change{ project.runners.count }.by(0)
         expect(response).to have_http_status(409)
       end
 
-      it 'should not enable locked runner' do
+      it 'does not enable locked runner' do
         specific_runner2.update(locked: true)
 
         expect do
@@ -392,14 +392,14 @@ describe API::Runners, api: true  do
         expect(response).to have_http_status(403)
       end
 
-      it 'should not enable shared runner' do
+      it 'does not enable shared runner' do
         post api("/projects/#{project.id}/runners", user), runner_id: shared_runner.id
 
         expect(response).to have_http_status(403)
       end
 
       context 'user is admin' do
-        it 'should enable any specific runner' do
+        it 'enables any specific runner' do
           expect do
             post api("/projects/#{project.id}/runners", admin), runner_id: unused_specific_runner.id
           end.to change{ project.runners.count }.by(+1)
@@ -408,14 +408,14 @@ describe API::Runners, api: true  do
       end
 
       context 'user is not admin' do
-        it 'should not enable runner without access to' do
+        it 'does not enable runner without access to' do
           post api("/projects/#{project.id}/runners", user), runner_id: unused_specific_runner.id
 
           expect(response).to have_http_status(403)
         end
       end
 
-      it 'should raise an error when no runner_id param is provided' do
+      it 'raises an error when no runner_id param is provided' do
         post api("/projects/#{project.id}/runners", admin)
 
         expect(response).to have_http_status(400)
@@ -423,7 +423,7 @@ describe API::Runners, api: true  do
     end
 
     context 'authorized user without permissions' do
-      it 'should not enable runner' do
+      it 'does not enable runner' do
         post api("/projects/#{project.id}/runners", user2)
 
         expect(response).to have_http_status(403)
@@ -431,7 +431,7 @@ describe API::Runners, api: true  do
     end
 
     context 'unauthorized user' do
-      it 'should not enable runner' do
+      it 'does not enable runner' do
         post api("/projects/#{project.id}/runners")
 
         expect(response).to have_http_status(401)
@@ -442,7 +442,7 @@ describe API::Runners, api: true  do
   describe 'DELETE /projects/:id/runners/:runner_id' do
     context 'authorized user' do
       context 'when runner have more than one associated projects' do
-        it "should disable project's runner" do
+        it "disables project's runner" do
           expect do
             delete api("/projects/#{project.id}/runners/#{two_projects_runner.id}", user)
           end.to change{ project.runners.count }.by(-1)
@@ -451,7 +451,7 @@ describe API::Runners, api: true  do
       end
 
       context 'when runner have one associated projects' do
-        it "should not disable project's runner" do
+        it "does not disable project's runner" do
           expect do
             delete api("/projects/#{project.id}/runners/#{specific_runner.id}", user)
           end.to change{ project.runners.count }.by(0)
@@ -459,7 +459,7 @@ describe API::Runners, api: true  do
         end
       end
 
-      it 'should return 404 is runner is not found' do
+      it 'returns 404 is runner is not found' do
         delete api("/projects/#{project.id}/runners/9999", user)
 
         expect(response).to have_http_status(404)
@@ -467,7 +467,7 @@ describe API::Runners, api: true  do
     end
 
     context 'authorized user without permissions' do
-      it "should not disable project's runner" do
+      it "does not disable project's runner" do
         delete api("/projects/#{project.id}/runners/#{specific_runner.id}", user2)
 
         expect(response).to have_http_status(403)
@@ -475,7 +475,7 @@ describe API::Runners, api: true  do
     end
 
     context 'unauthorized user' do
-      it "should not disable project's runner" do
+      it "does not disable project's runner" do
         delete api("/projects/#{project.id}/runners/#{specific_runner.id}")
 
         expect(response).to have_http_status(401)
diff --git a/spec/requests/api/services_spec.rb b/spec/requests/api/services_spec.rb
index a2446e12804762c36bd956a86512a01efb61ff95..375671bca4c27b5e47c97195c751c62a1bc5ca15 100644
--- a/spec/requests/api/services_spec.rb
+++ b/spec/requests/api/services_spec.rb
@@ -11,13 +11,13 @@ describe API::API, api: true  do
     describe "PUT /projects/:id/services/#{service.dasherize}" do
       include_context service
 
-      it "should update #{service} settings" do
+      it "updates #{service} settings" do
         put api("/projects/#{project.id}/services/#{dashed_service}", user), service_attrs
 
         expect(response).to have_http_status(200)
       end
 
-      it "should return if required fields missing" do
+      it "returns if required fields missing" do
         attrs = service_attrs
 
         required_attributes = service_attrs_list.select do |attr|
@@ -32,7 +32,7 @@ describe API::API, api: true  do
           attrs.delete(required_attributes.sample)
           expected_code = 400
         end
-        
+
         put api("/projects/#{project.id}/services/#{dashed_service}", user), attrs
 
         expect(response.status).to eq(expected_code)
@@ -42,7 +42,7 @@ describe API::API, api: true  do
     describe "DELETE /projects/:id/services/#{service.dasherize}" do
       include_context service
 
-      it "should delete #{service}" do
+      it "deletes #{service}" do
         delete api("/projects/#{project.id}/services/#{dashed_service}", user)
 
         expect(response).to have_http_status(200)
@@ -62,29 +62,29 @@ describe API::API, api: true  do
         service_object.save
       end
 
-      it 'should return authentication error when unauthenticated' do
+      it 'returns authentication error when unauthenticated' do
         get api("/projects/#{project.id}/services/#{dashed_service}")
         expect(response).to have_http_status(401)
       end
-      
-      it "should return all properties of service #{service} when authenticated as admin" do
+
+      it "returns all properties of service #{service} when authenticated as admin" do
         get api("/projects/#{project.id}/services/#{dashed_service}", admin)
-        
+
         expect(response).to have_http_status(200)
         expect(json_response['properties'].keys.map(&:to_sym)).to match_array(service_attrs_list.map)
       end
 
-      it "should return properties of service #{service} other than passwords when authenticated as project owner" do
+      it "returns properties of service #{service} other than passwords when authenticated as project owner" do
         get api("/projects/#{project.id}/services/#{dashed_service}", user)
 
         expect(response).to have_http_status(200)
         expect(json_response['properties'].keys.map(&:to_sym)).to match_array(service_attrs_list_without_passwords)
       end
 
-      it "should return error when authenticated but not a project owner" do
+      it "returns error when authenticated but not a project owner" do
         project.team << [user2, :developer]
         get api("/projects/#{project.id}/services/#{dashed_service}", user2)
-        
+
         expect(response).to have_http_status(403)
       end
     end
diff --git a/spec/requests/api/session_spec.rb b/spec/requests/api/session_spec.rb
index c15b7ff97928c710cd8cf2d988db22164d1edc4e..acad1365ace3539167bcb1e8f5907a7bb912d314 100644
--- a/spec/requests/api/session_spec.rb
+++ b/spec/requests/api/session_spec.rb
@@ -7,7 +7,7 @@ describe API::API, api: true  do
 
   describe "POST /session" do
     context "when valid password" do
-      it "should return private token" do
+      it "returns private token" do
         post api("/session"), email: user.email, password: '12345678'
         expect(response).to have_http_status(201)
 
@@ -17,10 +17,21 @@ 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
-      it 'should return private token' do
+      it 'returns private token' do
         post api('/session'), email: user.email.upcase, password: '12345678'
         expect(response.status).to eq 201
 
@@ -33,7 +44,7 @@ describe API::API, api: true  do
     end
 
     context 'when login has case-typo and password is valid' do
-      it 'should return private token' do
+      it 'returns private token' do
         post api('/session'), login: user.username.upcase, password: '12345678'
         expect(response.status).to eq 201
 
@@ -46,7 +57,7 @@ describe API::API, api: true  do
     end
 
     context "when invalid password" do
-      it "should return authentication error" do
+      it "returns authentication error" do
         post api("/session"), email: user.email, password: '123'
         expect(response).to have_http_status(401)
 
@@ -56,7 +67,7 @@ describe API::API, api: true  do
     end
 
     context "when empty password" do
-      it "should return authentication error" do
+      it "returns authentication error" do
         post api("/session"), email: user.email
         expect(response).to have_http_status(401)
 
@@ -66,7 +77,7 @@ describe API::API, api: true  do
     end
 
     context "when empty name" do
-      it "should return authentication error" do
+      it "returns authentication error" do
         post api("/session"), password: user.password
         expect(response).to have_http_status(401)
 
diff --git a/spec/requests/api/settings_spec.rb b/spec/requests/api/settings_spec.rb
index 684c2cd8e244557bf58706453e113915d9e74d3d..54d096e8b7ffd9d66dc2d8a4301c421af8647882 100644
--- a/spec/requests/api/settings_spec.rb
+++ b/spec/requests/api/settings_spec.rb
@@ -7,7 +7,7 @@ describe API::API, 'Settings', api: true  do
   let(:admin) { create(:admin) }
 
   describe "GET /application/settings" do
-    it "should return application settings" do
+    it "returns application settings" do
       get api("/application/settings", admin)
       expect(response).to have_http_status(200)
       expect(json_response).to be_an Hash
@@ -23,7 +23,7 @@ describe API::API, 'Settings', api: true  do
       allow(Gitlab.config.repositories).to receive(:storages).and_return(storages)
     end
 
-    it "should update application settings" do
+    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)
diff --git a/spec/requests/api/system_hooks_spec.rb b/spec/requests/api/system_hooks_spec.rb
index cf66f261ade2daa4b832b2b2e5ed09a7a686945c..1ce2658569eabcf0a8b4746f747f63b531351a3f 100644
--- a/spec/requests/api/system_hooks_spec.rb
+++ b/spec/requests/api/system_hooks_spec.rb
@@ -11,21 +11,21 @@ describe API::API, api: true  do
 
   describe "GET /hooks" do
     context "when no user" do
-      it "should return authentication error" do
+      it "returns authentication error" do
         get api("/hooks")
         expect(response).to have_http_status(401)
       end
     end
 
     context "when not an admin" do
-      it "should return forbidden error" do
+      it "returns forbidden error" do
         get api("/hooks", user)
         expect(response).to have_http_status(403)
       end
     end
 
     context "when authenticated as admin" do
-      it "should return an array of hooks" 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
@@ -35,18 +35,18 @@ describe API::API, api: true  do
   end
 
   describe "POST /hooks" do
-    it "should create new hook" do
+    it "creates new hook" do
       expect do
         post api("/hooks", admin), url: 'http://example.com'
       end.to change { SystemHook.count }.by(1)
     end
 
-    it "should respond with 400 if url not given" do
+    it "responds with 400 if url not given" do
       post api("/hooks", admin)
       expect(response).to have_http_status(400)
     end
 
-    it "should not create new hook without url" do
+    it "does not create new hook without url" do
       expect do
         post api("/hooks", admin)
       end.not_to change { SystemHook.count }
@@ -54,26 +54,26 @@ describe API::API, api: true  do
   end
 
   describe "GET /hooks/:id" do
-    it "should return hook by id" do
+    it "returns hook by id" do
       get api("/hooks/#{hook.id}", admin)
       expect(response).to have_http_status(200)
       expect(json_response['event_name']).to eq('project_create')
     end
 
-    it "should return 404 on failure" do
+    it "returns 404 on failure" do
       get api("/hooks/404", admin)
       expect(response).to have_http_status(404)
     end
   end
 
   describe "DELETE /hooks/:id" do
-    it "should delete a hook" do
+    it "deletes a hook" do
       expect do
         delete api("/hooks/#{hook.id}", admin)
       end.to change { SystemHook.count }.by(-1)
     end
 
-    it "should return success if hook id not found" do
+    it "returns success if hook id not found" do
       delete api("/hooks/12345", admin)
       expect(response).to have_http_status(200)
     end
diff --git a/spec/requests/api/tags_spec.rb b/spec/requests/api/tags_spec.rb
index fa700ab73436e3b196d96bb09283f4c8255e62af..d563883cd4767fb66a2ce74fdb7c18bd984044a6 100644
--- a/spec/requests/api/tags_spec.rb
+++ b/spec/requests/api/tags_spec.rb
@@ -16,7 +16,7 @@ describe API::API, api: true  do
     let(:description) { 'Awesome release!' }
 
     context 'without releases' do
-      it "should return an array of project tags" do
+      it "returns an array of project tags" do
         get api("/projects/#{project.id}/repository/tags", user)
         expect(response).to have_http_status(200)
         expect(json_response).to be_an Array
@@ -30,7 +30,7 @@ describe API::API, api: true  do
         release.update_attributes(description: description)
       end
 
-      it "should return an array of project tags with release info" do
+      it "returns an array of project tags with release info" do
         get api("/projects/#{project.id}/repository/tags", user)
 
         expect(response).to have_http_status(200)
@@ -61,7 +61,7 @@ describe API::API, api: true  do
 
   describe 'POST /projects/:id/repository/tags' do
     context 'lightweight tags' do
-      it 'should create a new tag' do
+      it 'creates a new tag' do
         post api("/projects/#{project.id}/repository/tags", user),
              tag_name: 'v7.0.1',
              ref: 'master'
@@ -72,7 +72,7 @@ describe API::API, api: true  do
     end
 
     context 'lightweight tags with release notes' do
-      it 'should create a new tag' do
+      it 'creates a new tag' do
         post api("/projects/#{project.id}/repository/tags", user),
              tag_name: 'v7.0.1',
              ref: 'master',
@@ -92,13 +92,13 @@ describe API::API, api: true  do
       end
 
       context 'delete tag' do
-        it 'should delete an existing tag' do
+        it 'deletes an existing tag' do
           delete api("/projects/#{project.id}/repository/tags/#{tag_name}", user)
           expect(response).to have_http_status(200)
           expect(json_response['tag_name']).to eq(tag_name)
         end
 
-        it 'should raise 404 if the tag does not exist' do
+        it 'raises 404 if the tag does not exist' do
           delete api("/projects/#{project.id}/repository/tags/foobar", user)
           expect(response).to have_http_status(404)
         end
@@ -106,7 +106,7 @@ describe API::API, api: true  do
     end
 
     context 'annotated tag' do
-      it 'should create a new annotated tag' do
+      it 'creates a new annotated tag' do
         # Identity must be set in .gitconfig to create annotated tag.
         repo_path = project.repository.path_to_repo
         system(*%W(#{Gitlab.config.git.bin_path} --git-dir=#{repo_path} config user.name #{user.name}))
@@ -123,14 +123,14 @@ describe API::API, api: true  do
       end
     end
 
-    it 'should deny for user without push access' do
+    it 'denies for user without push access' do
       post api("/projects/#{project.id}/repository/tags", user2),
            tag_name: 'v1.9.0',
            ref: '621491c677087aa243f165eab467bfdfbee00be1'
       expect(response).to have_http_status(403)
     end
 
-    it 'should return 400 if tag name is invalid' do
+    it 'returns 400 if tag name is invalid' do
       post api("/projects/#{project.id}/repository/tags", user),
            tag_name: 'v 1.0.0',
            ref: 'master'
@@ -138,7 +138,7 @@ describe API::API, api: true  do
       expect(json_response['message']).to eq('Tag name invalid')
     end
 
-    it 'should return 400 if tag already exists' do
+    it 'returns 400 if tag already exists' do
       post api("/projects/#{project.id}/repository/tags", user),
            tag_name: 'v8.0.0',
            ref: 'master'
@@ -150,7 +150,7 @@ describe API::API, api: true  do
       expect(json_response['message']).to eq('Tag v8.0.0 already exists')
     end
 
-    it 'should return 400 if ref name is invalid' do
+    it 'returns 400 if ref name is invalid' do
       post api("/projects/#{project.id}/repository/tags", user),
            tag_name: 'mytag',
            ref: 'foo'
@@ -163,7 +163,7 @@ describe API::API, api: true  do
     let(:tag_name) { project.repository.tag_names.first }
     let(:description) { 'Awesome release!' }
 
-    it 'should create description for existing git tag' do
+    it 'creates description for existing git tag' do
       post api("/projects/#{project.id}/repository/tags/#{tag_name}/release", user),
         description: description
 
@@ -172,7 +172,7 @@ describe API::API, api: true  do
       expect(json_response['description']).to eq(description)
     end
 
-    it 'should return 404 if the tag does not exist' do
+    it 'returns 404 if the tag does not exist' do
       post api("/projects/#{project.id}/repository/tags/foobar/release", user),
         description: description
 
@@ -186,7 +186,7 @@ describe API::API, api: true  do
         release.update_attributes(description: description)
       end
 
-      it 'should return 409 if there is already a release' do
+      it 'returns 409 if there is already a release' do
         post api("/projects/#{project.id}/repository/tags/#{tag_name}/release", user),
           description: description
 
@@ -207,7 +207,7 @@ describe API::API, api: true  do
         release.update_attributes(description: description)
       end
 
-      it 'should update the release description' do
+      it 'updates the release description' do
         put api("/projects/#{project.id}/repository/tags/#{tag_name}/release", user),
           description: new_description
 
@@ -217,7 +217,7 @@ describe API::API, api: true  do
       end
     end
 
-    it 'should return 404 if the tag does not exist' do
+    it 'returns 404 if the tag does not exist' do
       put api("/projects/#{project.id}/repository/tags/foobar/release", user),
         description: new_description
 
@@ -225,7 +225,7 @@ describe API::API, api: true  do
       expect(json_response['message']).to eq('Tag does not exist')
     end
 
-    it 'should return 404 if the release does not exist' do
+    it 'returns 404 if the release does not exist' do
       put api("/projects/#{project.id}/repository/tags/#{tag_name}/release", user),
         description: new_description
 
diff --git a/spec/requests/api/templates_spec.rb b/spec/requests/api/templates_spec.rb
index 68d0f41b489b239ffe4cf1d70e8b1a214180407f..5bd5b861792da3b92f187bbcf4688547d4e17d95 100644
--- a/spec/requests/api/templates_spec.rb
+++ b/spec/requests/api/templates_spec.rb
@@ -3,50 +3,53 @@ require 'spec_helper'
 describe API::Templates, api: true  do
   include ApiHelpers
 
-  describe 'the Template Entity' do
-    before { get api('/gitignores/Ruby') }
+  context 'global templates' do
+    describe 'the Template Entity' do
+      before { get api('/gitignores/Ruby') }
 
-    it { expect(json_response['name']).to eq('Ruby') }
-    it { expect(json_response['content']).to include('*.gem') }
-  end
+      it { expect(json_response['name']).to eq('Ruby') }
+      it { expect(json_response['content']).to include('*.gem') }
+    end
 
-  describe 'the TemplateList Entity' do
-    before { get api('/gitignores') }
+    describe 'the TemplateList Entity' do
+      before { get api('/gitignores') }
 
-    it { expect(json_response.first['name']).not_to be_nil }
-    it { expect(json_response.first['content']).to be_nil }
-  end
+      it { expect(json_response.first['name']).not_to be_nil }
+      it { expect(json_response.first['content']).to be_nil }
+    end
 
-  context 'requesting gitignores' do
-    describe 'GET /gitignores' do
-      it 'returns a list of available gitignore templates' do
-        get api('/gitignores')
+    context 'requesting gitignores' do
+      describe 'GET /gitignores' do
+        it 'returns a list of available gitignore templates' do
+          get api('/gitignores')
 
-        expect(response).to have_http_status(200)
-        expect(json_response).to be_an Array
-        expect(json_response.size).to be > 15
+          expect(response.status).to eq(200)
+          expect(json_response).to be_an Array
+          expect(json_response.size).to be > 15
+        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')
+    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')
 
-        expect(response).to have_http_status(200)
-        expect(json_response).to be_an Array
-        expect(json_response.first['name']).not_to be_nil
+          expect(response.status).to eq(200)
+          expect(json_response).to be_an Array
+          expect(json_response.first['name']).not_to be_nil
+        end
       end
     end
-  end
 
-  describe 'GET /gitlab_ci_ymls/Ruby' do
-    it 'adds a disclaimer on the top' do
-      get api('/gitlab_ci_ymls/Ruby')
+    describe 'GET /gitlab_ci_ymls/Ruby' do
+      it 'adds a disclaimer on the top' do
+        get api('/gitlab_ci_ymls/Ruby')
 
-      expect(response).to have_http_status(200)
-      expect(json_response['content']).to start_with("# This file is a template,")
+        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,")
+      end
     end
   end
 end
diff --git a/spec/requests/api/todos_spec.rb b/spec/requests/api/todos_spec.rb
index 3ccd0af652f7a05fb1f9c7743cca64c432482429..887a2ba5b846017c411d4dba4958c46dd767d912 100644
--- a/spec/requests/api/todos_spec.rb
+++ b/spec/requests/api/todos_spec.rb
@@ -117,6 +117,12 @@ describe API::Todos, api: true do
         expect(response.status).to eq(200)
         expect(pending_1.reload).to be_done
       end
+
+      it 'updates todos cache' do
+        expect_any_instance_of(User).to receive(:update_todos_count_cache).and_call_original
+
+        delete api("/todos/#{pending_1.id}", john_doe)
+      end
     end
   end
 
@@ -139,6 +145,12 @@ describe API::Todos, api: true do
         expect(pending_2.reload).to be_done
         expect(pending_3.reload).to be_done
       end
+
+      it 'updates todos cache' do
+        expect_any_instance_of(User).to receive(:update_todos_count_cache).and_call_original
+
+        delete api("/todos", john_doe)
+      end
     end
   end
 
diff --git a/spec/requests/api/triggers_spec.rb b/spec/requests/api/triggers_spec.rb
index 8992996c30aaca5ec33c955b840693c91ecbfda4..82bba1ce8a40fd8bc94c13b235de6d64d8ff10da 100644
--- a/spec/requests/api/triggers_spec.rb
+++ b/spec/requests/api/triggers_spec.rb
@@ -27,17 +27,17 @@ describe API::API do
     end
 
     context 'Handles errors' do
-      it 'should return bad request if token is missing' do
+      it 'returns bad request if token is missing' do
         post api("/projects/#{project.id}/trigger/builds"), ref: 'master'
         expect(response).to have_http_status(400)
       end
 
-      it 'should return not found if project is not found' do
+      it 'returns not found if project is not found' do
         post api('/projects/0/trigger/builds'), options.merge(ref: 'master')
         expect(response).to have_http_status(404)
       end
 
-      it 'should return unauthorized if token is for different project' do
+      it 'returns unauthorized if token is for different project' do
         post api("/projects/#{project2.id}/trigger/builds"), options.merge(ref: 'master')
         expect(response).to have_http_status(401)
       end
@@ -46,14 +46,15 @@ describe API::API do
     context 'Have a commit' do
       let(:pipeline) { project.pipelines.last }
 
-      it 'should create builds' do
+      it 'creates builds' do
         post api("/projects/#{project.id}/trigger/builds"), options.merge(ref: 'master')
         expect(response).to have_http_status(201)
         pipeline.builds.reload
-        expect(pipeline.builds.size).to eq(2)
+        expect(pipeline.builds.pending.size).to eq(2)
+        expect(pipeline.builds.size).to eq(5)
       end
 
-      it 'should return bad request with no builds created if there\'s no commit for that ref' do
+      it 'returns bad request with no builds created if there\'s no commit for that ref' do
         post api("/projects/#{project.id}/trigger/builds"), options.merge(ref: 'other-branch')
         expect(response).to have_http_status(400)
         expect(json_response['message']).to eq('No builds created')
@@ -64,19 +65,19 @@ describe API::API do
           { 'TRIGGER_KEY' => 'TRIGGER_VALUE' }
         end
 
-        it 'should validate variables to be a hash' 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')
         end
 
-        it 'should validate variables needs to be a map of key-valued strings' do
+        it 'validates variables needs to be a map of key-valued strings' do
           post api("/projects/#{project.id}/trigger/builds"), options.merge(variables: { key: %w(1 2) }, ref: 'master')
           expect(response).to have_http_status(400)
           expect(json_response['message']).to eq('variables needs to be a map of key-valued strings')
         end
 
-        it 'create trigger request with variables' do
+        it 'creates trigger request with variables' do
           post api("/projects/#{project.id}/trigger/builds"), options.merge(variables: variables, ref: 'master')
           expect(response).to have_http_status(201)
           pipeline.builds.reload
@@ -88,7 +89,7 @@ describe API::API do
 
   describe 'GET /projects/:id/triggers' do
     context 'authenticated user with valid permissions' do
-      it 'should return list of triggers' do
+      it 'returns list of triggers' do
         get api("/projects/#{project.id}/triggers", user)
 
         expect(response).to have_http_status(200)
@@ -98,7 +99,7 @@ describe API::API do
     end
 
     context 'authenticated user with invalid permissions' do
-      it 'should not return triggers list' do
+      it 'does not return triggers list' do
         get api("/projects/#{project.id}/triggers", user2)
 
         expect(response).to have_http_status(403)
@@ -106,7 +107,7 @@ describe API::API do
     end
 
     context 'unauthenticated user' do
-      it 'should not return triggers list' do
+      it 'does not return triggers list' do
         get api("/projects/#{project.id}/triggers")
 
         expect(response).to have_http_status(401)
@@ -116,14 +117,14 @@ describe API::API do
 
   describe 'GET /projects/:id/triggers/:token' do
     context 'authenticated user with valid permissions' do
-      it 'should return trigger details' do
+      it 'returns trigger details' do
         get api("/projects/#{project.id}/triggers/#{trigger.token}", user)
 
         expect(response).to have_http_status(200)
         expect(json_response).to be_a(Hash)
       end
 
-      it 'should respond with 404 Not Found if requesting non-existing trigger' do
+      it 'responds with 404 Not Found if requesting non-existing trigger' do
         get api("/projects/#{project.id}/triggers/abcdef012345", user)
 
         expect(response).to have_http_status(404)
@@ -131,7 +132,7 @@ describe API::API do
     end
 
     context 'authenticated user with invalid permissions' do
-      it 'should not return triggers list' do
+      it 'does not return triggers list' do
         get api("/projects/#{project.id}/triggers/#{trigger.token}", user2)
 
         expect(response).to have_http_status(403)
@@ -139,7 +140,7 @@ describe API::API do
     end
 
     context 'unauthenticated user' do
-      it 'should not return triggers list' do
+      it 'does not return triggers list' do
         get api("/projects/#{project.id}/triggers/#{trigger.token}")
 
         expect(response).to have_http_status(401)
@@ -149,7 +150,7 @@ describe API::API do
 
   describe 'POST /projects/:id/triggers' do
     context 'authenticated user with valid permissions' do
-      it 'should create trigger' do
+      it 'creates trigger' do
         expect do
           post api("/projects/#{project.id}/triggers", user)
         end.to change{project.triggers.count}.by(1)
@@ -160,7 +161,7 @@ describe API::API do
     end
 
     context 'authenticated user with invalid permissions' do
-      it 'should not create trigger' do
+      it 'does not create trigger' do
         post api("/projects/#{project.id}/triggers", user2)
 
         expect(response).to have_http_status(403)
@@ -168,7 +169,7 @@ describe API::API do
     end
 
     context 'unauthenticated user' do
-      it 'should not create trigger' do
+      it 'does not create trigger' do
         post api("/projects/#{project.id}/triggers")
 
         expect(response).to have_http_status(401)
@@ -178,14 +179,14 @@ describe API::API do
 
   describe 'DELETE /projects/:id/triggers/:token' do
     context 'authenticated user with valid permissions' do
-      it 'should delete trigger' do
+      it 'deletes trigger' do
         expect do
           delete api("/projects/#{project.id}/triggers/#{trigger.token}", user)
         end.to change{project.triggers.count}.by(-1)
         expect(response).to have_http_status(200)
       end
 
-      it 'should respond with 404 Not Found if requesting non-existing trigger' do
+      it 'responds with 404 Not Found if requesting non-existing trigger' do
         delete api("/projects/#{project.id}/triggers/abcdef012345", user)
 
         expect(response).to have_http_status(404)
@@ -193,7 +194,7 @@ describe API::API do
     end
 
     context 'authenticated user with invalid permissions' do
-      it 'should not delete trigger' do
+      it 'does not delete trigger' do
         delete api("/projects/#{project.id}/triggers/#{trigger.token}", user2)
 
         expect(response).to have_http_status(403)
@@ -201,7 +202,7 @@ describe API::API do
     end
 
     context 'unauthenticated user' do
-      it 'should not delete trigger' do
+      it 'does not delete trigger' do
         delete api("/projects/#{project.id}/triggers/#{trigger.token}")
 
         expect(response).to have_http_status(401)
diff --git a/spec/requests/api/users_spec.rb b/spec/requests/api/users_spec.rb
index e43e3e269bfc28146101c4d6b491e70dbb57372a..0bbba64a6d581973ddadbfc91354c1e4ba585434 100644
--- a/spec/requests/api/users_spec.rb
+++ b/spec/requests/api/users_spec.rb
@@ -13,7 +13,7 @@ describe API::API, api: true  do
 
   describe "GET /users" do
     context "when unauthenticated" do
-      it "should return authentication error" do
+      it "returns authentication error" do
         get api("/users")
         expect(response).to have_http_status(401)
       end
@@ -38,7 +38,7 @@ describe API::API, api: true  do
         end
       end
 
-      it "should return an array of users" do
+      it "returns an array of users" do
         get api("/users", user)
         expect(response).to have_http_status(200)
         expect(json_response).to be_an Array
@@ -48,7 +48,7 @@ describe API::API, api: true  do
         end['username']).to eq(username)
       end
 
-      it "should return one user" do
+      it "returns one user" do
         get api("/users?username=#{omniauth_user.username}", user)
         expect(response).to have_http_status(200)
         expect(json_response).to be_an Array
@@ -57,7 +57,7 @@ describe API::API, api: true  do
     end
 
     context "when admin" do
-      it "should return an array of users" do
+      it "returns an array of users" do
         get api("/users", admin)
         expect(response).to have_http_status(200)
         expect(json_response).to be_an Array
@@ -72,24 +72,24 @@ describe API::API, api: true  do
   end
 
   describe "GET /users/:id" do
-    it "should return a user by id" do
+    it "returns a user by id" do
       get api("/users/#{user.id}", user)
       expect(response).to have_http_status(200)
       expect(json_response['username']).to eq(user.username)
     end
 
-    it "should return a 401 if unauthenticated" do
+    it "returns a 401 if unauthenticated" do
       get api("/users/9998")
       expect(response).to have_http_status(401)
     end
 
-    it "should return a 404 error if user id not found" do
+    it "returns a 404 error if user id not found" do
       get api("/users/9999", user)
       expect(response).to have_http_status(404)
       expect(json_response['message']).to eq('404 Not found')
     end
 
-    it "should return a 404 if invalid ID" do
+    it "returns a 404 if invalid ID" do
       get api("/users/1ASDF", user)
       expect(response).to have_http_status(404)
     end
@@ -98,13 +98,13 @@ describe API::API, api: true  do
   describe "POST /users" do
     before{ admin }
 
-    it "should create user" do
+    it "creates user" do
       expect do
         post api("/users", admin), attributes_for(:user, projects_limit: 3)
       end.to change { User.count }.by(1)
     end
 
-    it "should create user with correct attributes" do
+    it "creates user with correct attributes" do
       post api('/users', admin), attributes_for(:user, admin: true, can_create_group: true)
       expect(response).to have_http_status(201)
       user_id = json_response['id']
@@ -114,7 +114,7 @@ describe API::API, api: true  do
       expect(new_user.can_create_group).to eq(true)
     end
 
-    it "should create non-admin user" do
+    it "creates non-admin user" do
       post api('/users', admin), attributes_for(:user, admin: false, can_create_group: false)
       expect(response).to have_http_status(201)
       user_id = json_response['id']
@@ -124,7 +124,7 @@ describe API::API, api: true  do
       expect(new_user.can_create_group).to eq(false)
     end
 
-    it "should create non-admin users by default" do
+    it "creates non-admin users by default" do
       post api('/users', admin), attributes_for(:user)
       expect(response).to have_http_status(201)
       user_id = json_response['id']
@@ -133,7 +133,7 @@ describe API::API, api: true  do
       expect(new_user.admin).to eq(false)
     end
 
-    it "should return 201 Created on success" do
+    it "returns 201 Created on success" do
       post api("/users", admin), attributes_for(:user, projects_limit: 3)
       expect(response).to have_http_status(201)
     end
@@ -148,7 +148,7 @@ describe API::API, api: true  do
       expect(new_user.external).to be_falsy
     end
 
-    it 'should allow an external user to be created' do
+    it 'allows an external user to be created' do
       post api("/users", admin), attributes_for(:user, external: true)
       expect(response).to have_http_status(201)
 
@@ -158,7 +158,7 @@ describe API::API, api: true  do
       expect(new_user.external).to be_truthy
     end
 
-    it "should not create user with invalid email" do
+    it "does not create user with invalid email" do
       post api('/users', admin),
         email: 'invalid email',
         password: 'password',
@@ -166,27 +166,27 @@ describe API::API, api: true  do
       expect(response).to have_http_status(400)
     end
 
-    it 'should return 400 error if name not given' do
+    it 'returns 400 error if name not given' do
       post api('/users', admin), attributes_for(:user).except(:name)
       expect(response).to have_http_status(400)
     end
 
-    it 'should return 400 error if password not given' do
+    it 'returns 400 error if password not given' do
       post api('/users', admin), attributes_for(:user).except(:password)
       expect(response).to have_http_status(400)
     end
 
-    it 'should return 400 error if email not given' do
+    it 'returns 400 error if email not given' do
       post api('/users', admin), attributes_for(:user).except(:email)
       expect(response).to have_http_status(400)
     end
 
-    it 'should return 400 error if username not given' do
+    it 'returns 400 error if username not given' do
       post api('/users', admin), attributes_for(:user).except(:username)
       expect(response).to have_http_status(400)
     end
 
-    it 'should return 400 error if user does not validate' do
+    it 'returns 400 error if user does not validate' do
       post api('/users', admin),
         password: 'pass',
         email: 'test@example.com',
@@ -205,7 +205,7 @@ describe API::API, api: true  do
         to eq([Gitlab::Regex.namespace_regex_message])
     end
 
-    it "shouldn't available for non admin users" do
+    it "is not available for non admin users" do
       post api("/users", user), attributes_for(:user)
       expect(response).to have_http_status(403)
     end
@@ -219,7 +219,7 @@ describe API::API, api: true  do
           name: 'foo'
       end
 
-      it 'should return 409 conflict error if user with same email exists' do
+      it 'returns 409 conflict error if user with same email exists' do
         expect do
           post api('/users', admin),
             name: 'foo',
@@ -231,7 +231,7 @@ describe API::API, api: true  do
         expect(json_response['message']).to eq('Email has already been taken')
       end
 
-      it 'should return 409 conflict error if same username exists' do
+      it 'returns 409 conflict error if same username exists' do
         expect do
           post api('/users', admin),
             name: 'foo',
@@ -246,7 +246,7 @@ describe API::API, api: true  do
   end
 
   describe "GET /users/sign_up" do
-    it "should redirect to sign in page" do
+    it "redirects to sign in page" do
       get "/users/sign_up"
       expect(response).to have_http_status(302)
       expect(response).to redirect_to(new_user_session_path)
@@ -258,55 +258,55 @@ describe API::API, api: true  do
 
     before { admin }
 
-    it "should update user with new bio" do
+    it "updates user with new bio" do
       put api("/users/#{user.id}", admin), { bio: 'new test bio' }
       expect(response).to have_http_status(200)
       expect(json_response['bio']).to eq('new test bio')
       expect(user.reload.bio).to eq('new test bio')
     end
 
-    it 'should update user with his own email' do
+    it 'updates user with his own email' do
       put api("/users/#{user.id}", admin), email: user.email
       expect(response).to have_http_status(200)
       expect(json_response['email']).to eq(user.email)
       expect(user.reload.email).to eq(user.email)
     end
 
-    it 'should update user with his own username' do
+    it 'updates user with his own username' do
       put api("/users/#{user.id}", admin), username: user.username
       expect(response).to have_http_status(200)
       expect(json_response['username']).to eq(user.username)
       expect(user.reload.username).to eq(user.username)
     end
 
-    it "should update user's existing identity" do
+    it "updates user's existing identity" do
       put api("/users/#{omniauth_user.id}", admin), provider: 'ldapmain', extern_uid: '654321'
       expect(response).to have_http_status(200)
       expect(omniauth_user.reload.identities.first.extern_uid).to eq('654321')
     end
 
-    it 'should update user with new identity' do
+    it 'updates user with new identity' do
       put api("/users/#{user.id}", admin), provider: 'github', extern_uid: '67890'
       expect(response).to have_http_status(200)
       expect(user.reload.identities.first.extern_uid).to eq('67890')
       expect(user.reload.identities.first.provider).to eq('github')
     end
 
-    it "should update admin status" do
+    it "updates admin status" do
       put api("/users/#{user.id}", admin), { admin: true }
       expect(response).to have_http_status(200)
       expect(json_response['is_admin']).to eq(true)
       expect(user.reload.admin).to eq(true)
     end
 
-    it "should update external status" do
+    it "updates external status" do
       put api("/users/#{user.id}", admin), { external: true }
       expect(response.status).to eq 200
       expect(json_response['external']).to eq(true)
       expect(user.reload.external?).to be_truthy
     end
 
-    it "should not update admin status" do
+    it "does not update admin status" do
       put api("/users/#{admin_user.id}", admin), { can_create_group: false }
       expect(response).to have_http_status(200)
       expect(json_response['is_admin']).to eq(true)
@@ -314,28 +314,28 @@ describe API::API, api: true  do
       expect(admin_user.can_create_group).to eq(false)
     end
 
-    it "should not allow invalid update" do
+    it "does not allow invalid update" do
       put api("/users/#{user.id}", admin), { email: 'invalid email' }
       expect(response).to have_http_status(400)
       expect(user.reload.email).not_to eq('invalid email')
     end
 
-    it "shouldn't available for non admin users" do
+    it "is not available for non admin users" do
       put api("/users/#{user.id}", user), attributes_for(:user)
       expect(response).to have_http_status(403)
     end
 
-    it "should return 404 for non-existing user" do
+    it "returns 404 for non-existing user" do
       put api("/users/999999", admin), { bio: 'update should fail' }
       expect(response).to have_http_status(404)
       expect(json_response['message']).to eq('404 Not found')
     end
 
-    it "should raise error for invalid ID" do
+    it "raises error for invalid ID" do
       expect{put api("/users/ASDF", admin) }.to raise_error(ActionController::RoutingError)
     end
 
-    it 'should return 400 error if user does not validate' do
+    it 'returns 400 error if user does not validate' do
       put api("/users/#{user.id}", admin),
         password: 'pass',
         email: 'test@example.com',
@@ -361,13 +361,13 @@ describe API::API, api: true  do
         @user = User.all.last
       end
 
-      it 'should return 409 conflict error if email address exists' do
+      it 'returns 409 conflict error if email address exists' do
         put api("/users/#{@user.id}", admin), email: 'test@example.com'
         expect(response).to have_http_status(409)
         expect(@user.reload.email).to eq(@user.email)
       end
 
-      it 'should return 409 conflict error if username taken' do
+      it 'returns 409 conflict error if username taken' do
         @user_id = User.all.last.id
         put api("/users/#{@user.id}", admin), username: 'test'
         expect(response).to have_http_status(409)
@@ -379,28 +379,28 @@ describe API::API, api: true  do
   describe "POST /users/:id/keys" do
     before { admin }
 
-    it "should not create invalid ssh key" do
+    it "does not create invalid ssh key" do
       post api("/users/#{user.id}/keys", admin), { title: "invalid key" }
       expect(response).to have_http_status(400)
       expect(json_response['message']).to eq('400 (Bad request) "key" not given')
     end
 
-    it 'should not create key without title' do
+    it 'does not create key without title' do
       post api("/users/#{user.id}/keys", admin), key: 'some key'
       expect(response).to have_http_status(400)
       expect(json_response['message']).to eq('400 (Bad request) "title" not given')
     end
 
-    it "should create ssh key" do
+    it "creates ssh key" do
       key_attrs = attributes_for :key
       expect do
         post api("/users/#{user.id}/keys", admin), key_attrs
       end.to change{ user.keys.count }.by(1)
     end
 
-    it "should return 405 for invalid ID" do
-      post api("/users/ASDF/keys", admin)
-      expect(response).to have_http_status(405)
+    it "returns 400 for invalid ID" do
+      post api("/users/999999/keys", admin)
+      expect(response).to have_http_status(400)
     end
   end
 
@@ -408,20 +408,20 @@ describe API::API, api: true  do
     before { admin }
 
     context 'when unauthenticated' do
-      it 'should return authentication error' do
+      it 'returns authentication error' do
         get api("/users/#{user.id}/keys")
         expect(response).to have_http_status(401)
       end
     end
 
     context 'when authenticated' do
-      it 'should return 404 for non-existing user' do
+      it 'returns 404 for non-existing user' do
         get api('/users/999999/keys', admin)
         expect(response).to have_http_status(404)
         expect(json_response['message']).to eq('404 User Not Found')
       end
 
-      it 'should return array of ssh keys' do
+      it 'returns array of ssh keys' do
         user.keys << key
         user.save
         get api("/users/#{user.id}/keys", admin)
@@ -429,11 +429,6 @@ describe API::API, api: true  do
         expect(json_response).to be_an Array
         expect(json_response.first['title']).to eq(key.title)
       end
-
-      it "should return 405 for invalid ID" do
-        get api("/users/ASDF/keys", admin)
-        expect(response).to have_http_status(405)
-      end
     end
   end
 
@@ -441,14 +436,14 @@ describe API::API, api: true  do
     before { admin }
 
     context 'when unauthenticated' do
-      it 'should return authentication error' do
+      it 'returns authentication error' do
         delete api("/users/#{user.id}/keys/42")
         expect(response).to have_http_status(401)
       end
     end
 
     context 'when authenticated' do
-      it 'should delete existing key' do
+      it 'deletes existing key' do
         user.keys << key
         user.save
         expect do
@@ -457,7 +452,7 @@ describe API::API, api: true  do
         expect(response).to have_http_status(200)
       end
 
-      it 'should return 404 error if user not found' do
+      it 'returns 404 error if user not found' do
         user.keys << key
         user.save
         delete api("/users/999999/keys/#{key.id}", admin)
@@ -465,7 +460,7 @@ describe API::API, api: true  do
         expect(json_response['message']).to eq('404 User Not Found')
       end
 
-      it 'should return 404 error if key not foud' do
+      it 'returns 404 error if key not foud' do
         delete api("/users/#{user.id}/keys/42", admin)
         expect(response).to have_http_status(404)
         expect(json_response['message']).to eq('404 Key Not Found')
@@ -476,22 +471,22 @@ describe API::API, api: true  do
   describe "POST /users/:id/emails" do
     before { admin }
 
-    it "should not create invalid email" do
+    it "does not create invalid email" do
       post api("/users/#{user.id}/emails", admin), {}
       expect(response).to have_http_status(400)
       expect(json_response['message']).to eq('400 (Bad request) "email" not given')
     end
 
-    it "should create email" do
+    it "creates email" do
       email_attrs = attributes_for :email
       expect do
         post api("/users/#{user.id}/emails", admin), email_attrs
       end.to change{ user.emails.count }.by(1)
     end
 
-    it "should raise error for invalid ID" do
-      post api("/users/ASDF/emails", admin)
-      expect(response).to have_http_status(405)
+    it "raises error for invalid ID" do
+      post api("/users/999999/emails", admin)
+      expect(response).to have_http_status(400)
     end
   end
 
@@ -499,20 +494,20 @@ describe API::API, api: true  do
     before { admin }
 
     context 'when unauthenticated' do
-      it 'should return authentication error' do
+      it 'returns authentication error' do
         get api("/users/#{user.id}/emails")
         expect(response).to have_http_status(401)
       end
     end
 
     context 'when authenticated' do
-      it 'should return 404 for non-existing user' do
+      it 'returns 404 for non-existing user' do
         get api('/users/999999/emails', admin)
         expect(response).to have_http_status(404)
         expect(json_response['message']).to eq('404 User Not Found')
       end
 
-      it 'should return array of emails' do
+      it 'returns array of emails' do
         user.emails << email
         user.save
         get api("/users/#{user.id}/emails", admin)
@@ -521,7 +516,7 @@ describe API::API, api: true  do
         expect(json_response.first['email']).to eq(email.email)
       end
 
-      it "should raise error for invalid ID" do
+      it "raises error for invalid ID" do
         put api("/users/ASDF/emails", admin)
         expect(response).to have_http_status(405)
       end
@@ -532,14 +527,14 @@ describe API::API, api: true  do
     before { admin }
 
     context 'when unauthenticated' do
-      it 'should return authentication error' do
+      it 'returns authentication error' do
         delete api("/users/#{user.id}/emails/42")
         expect(response).to have_http_status(401)
       end
     end
 
     context 'when authenticated' do
-      it 'should delete existing email' do
+      it 'deletes existing email' do
         user.emails << email
         user.save
         expect do
@@ -548,7 +543,7 @@ describe API::API, api: true  do
         expect(response).to have_http_status(200)
       end
 
-      it 'should return 404 error if user not found' do
+      it 'returns 404 error if user not found' do
         user.emails << email
         user.save
         delete api("/users/999999/emails/#{email.id}", admin)
@@ -556,51 +551,53 @@ describe API::API, api: true  do
         expect(json_response['message']).to eq('404 User Not Found')
       end
 
-      it 'should return 404 error if email not foud' do
+      it 'returns 404 error if email not foud' do
         delete api("/users/#{user.id}/emails/42", admin)
         expect(response).to have_http_status(404)
         expect(json_response['message']).to eq('404 Email Not Found')
       end
 
-      it "should raise error for invalid ID" do
+      it "raises error for invalid ID" do
         expect{delete api("/users/ASDF/emails/bar", admin) }.to raise_error(ActionController::RoutingError)
       end
     end
   end
 
   describe "DELETE /users/:id" do
+    let!(:namespace) { user.namespace }
     before { admin }
 
-    it "should delete user" do
+    it "deletes user" do
       delete api("/users/#{user.id}", admin)
       expect(response).to have_http_status(200)
       expect { User.find(user.id) }.to raise_error ActiveRecord::RecordNotFound
+      expect { Namespace.find(namespace.id) }.to raise_error ActiveRecord::RecordNotFound
       expect(json_response['email']).to eq(user.email)
     end
 
-    it "should not delete for unauthenticated user" do
+    it "does not delete for unauthenticated user" do
       delete api("/users/#{user.id}")
       expect(response).to have_http_status(401)
     end
 
-    it "shouldn't available for non admin users" do
+    it "is not available for non admin users" do
       delete api("/users/#{user.id}", user)
       expect(response).to have_http_status(403)
     end
 
-    it "should return 404 for non-existing user" do
+    it "returns 404 for non-existing user" do
       delete api("/users/999999", admin)
       expect(response).to have_http_status(404)
       expect(json_response['message']).to eq('404 User Not Found')
     end
 
-    it "should raise error for invalid ID" do
+    it "raises error for invalid ID" do
       expect{delete api("/users/ASDF", admin) }.to raise_error(ActionController::RoutingError)
     end
   end
 
   describe "GET /user" do
-    it "should return current user" do
+    it "returns current user" do
       get api("/user", user)
       expect(response).to have_http_status(200)
       expect(json_response['email']).to eq(user.email)
@@ -610,7 +607,7 @@ describe API::API, api: true  do
       expect(json_response['projects_limit']).to eq(user.projects_limit)
     end
 
-    it "should return 401 error if user is unauthenticated" do
+    it "returns 401 error if user is unauthenticated" do
       get api("/user")
       expect(response).to have_http_status(401)
     end
@@ -618,14 +615,14 @@ describe API::API, api: true  do
 
   describe "GET /user/keys" do
     context "when unauthenticated" do
-      it "should return authentication error" do
+      it "returns authentication error" do
         get api("/user/keys")
         expect(response).to have_http_status(401)
       end
     end
 
     context "when authenticated" do
-      it "should return array of ssh keys" do
+      it "returns array of ssh keys" do
         user.keys << key
         user.save
         get api("/user/keys", user)
@@ -637,7 +634,7 @@ describe API::API, api: true  do
   end
 
   describe "GET /user/keys/:id" do
-    it "should return single key" do
+    it "returns single key" do
       user.keys << key
       user.save
       get api("/user/keys/#{key.id}", user)
@@ -645,13 +642,13 @@ describe API::API, api: true  do
       expect(json_response["title"]).to eq(key.title)
     end
 
-    it "should return 404 Not Found within invalid ID" 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
 
-    it "should return 404 error if admin accesses user's ssh key" do
+    it "returns 404 error if admin accesses user's ssh key" do
       user.keys << key
       user.save
       admin
@@ -660,14 +657,14 @@ describe API::API, api: true  do
       expect(json_response['message']).to eq('404 Not found')
     end
 
-    it "should return 404 for invalid ID" do
+    it "returns 404 for invalid ID" do
       get api("/users/keys/ASDF", admin)
       expect(response).to have_http_status(404)
     end
   end
 
   describe "POST /user/keys" do
-    it "should create ssh key" do
+    it "creates ssh key" do
       key_attrs = attributes_for :key
       expect do
         post api("/user/keys", user), key_attrs
@@ -675,31 +672,31 @@ describe API::API, api: true  do
       expect(response).to have_http_status(201)
     end
 
-    it "should return a 401 error if unauthorized" do
+    it "returns a 401 error if unauthorized" do
       post api("/user/keys"), title: 'some title', key: 'some key'
       expect(response).to have_http_status(401)
     end
 
-    it "should not create ssh key without key" do
+    it "does not create ssh key without key" do
       post api("/user/keys", user), title: 'title'
       expect(response).to have_http_status(400)
       expect(json_response['message']).to eq('400 (Bad request) "key" not given')
     end
 
-    it 'should not create ssh key without title' do
+    it 'does not create ssh key without title' do
       post api('/user/keys', user), key: 'some key'
       expect(response).to have_http_status(400)
       expect(json_response['message']).to eq('400 (Bad request) "title" not given')
     end
 
-    it "should not create ssh key without title" do
+    it "does not create ssh key without title" do
       post api("/user/keys", user), key: "somekey"
       expect(response).to have_http_status(400)
     end
   end
 
   describe "DELETE /user/keys/:id" do
-    it "should delete existed key" do
+    it "deletes existed key" do
       user.keys << key
       user.save
       expect do
@@ -708,33 +705,33 @@ describe API::API, api: true  do
       expect(response).to have_http_status(200)
     end
 
-    it "should return success if key ID not found" do
+    it "returns success if key ID not found" do
       delete api("/user/keys/42", user)
       expect(response).to have_http_status(200)
     end
 
-    it "should return 401 error if unauthorized" do
+    it "returns 401 error if unauthorized" do
       user.keys << key
       user.save
       delete api("/user/keys/#{key.id}")
       expect(response).to have_http_status(401)
     end
 
-    it "should raise error for invalid ID" do
+    it "raises error for invalid ID" do
       expect{delete api("/users/keys/ASDF", admin) }.to raise_error(ActionController::RoutingError)
     end
   end
 
   describe "GET /user/emails" do
     context "when unauthenticated" do
-      it "should return authentication error" do
+      it "returns authentication error" do
         get api("/user/emails")
         expect(response).to have_http_status(401)
       end
     end
 
     context "when authenticated" do
-      it "should return array of emails" do
+      it "returns array of emails" do
         user.emails << email
         user.save
         get api("/user/emails", user)
@@ -746,7 +743,7 @@ describe API::API, api: true  do
   end
 
   describe "GET /user/emails/:id" do
-    it "should return single email" do
+    it "returns single email" do
       user.emails << email
       user.save
       get api("/user/emails/#{email.id}", user)
@@ -754,13 +751,13 @@ describe API::API, api: true  do
       expect(json_response["email"]).to eq(email.email)
     end
 
-    it "should return 404 Not Found within invalid ID" do
+    it "returns 404 Not Found within invalid ID" do
       get api("/user/emails/42", user)
       expect(response).to have_http_status(404)
       expect(json_response['message']).to eq('404 Not found')
     end
 
-    it "should return 404 error if admin accesses user's email" do
+    it "returns 404 error if admin accesses user's email" do
       user.emails << email
       user.save
       admin
@@ -769,14 +766,14 @@ describe API::API, api: true  do
       expect(json_response['message']).to eq('404 Not found')
     end
 
-    it "should return 404 for invalid ID" do
+    it "returns 404 for invalid ID" do
       get api("/users/emails/ASDF", admin)
       expect(response).to have_http_status(404)
     end
   end
 
   describe "POST /user/emails" do
-    it "should create email" do
+    it "creates email" do
       email_attrs = attributes_for :email
       expect do
         post api("/user/emails", user), email_attrs
@@ -784,12 +781,12 @@ describe API::API, api: true  do
       expect(response).to have_http_status(201)
     end
 
-    it "should return a 401 error if unauthorized" do
+    it "returns a 401 error if unauthorized" do
       post api("/user/emails"), email: 'some email'
       expect(response).to have_http_status(401)
     end
 
-    it "should not create email with invalid email" do
+    it "does not create email with invalid email" do
       post api("/user/emails", user), {}
       expect(response).to have_http_status(400)
       expect(json_response['message']).to eq('400 (Bad request) "email" not given')
@@ -797,7 +794,7 @@ describe API::API, api: true  do
   end
 
   describe "DELETE /user/emails/:id" do
-    it "should delete existed email" do
+    it "deletes existed email" do
       user.emails << email
       user.save
       expect do
@@ -806,44 +803,44 @@ describe API::API, api: true  do
       expect(response).to have_http_status(200)
     end
 
-    it "should return success if email ID not found" do
+    it "returns success if email ID not found" do
       delete api("/user/emails/42", user)
       expect(response).to have_http_status(200)
     end
 
-    it "should return 401 error if unauthorized" do
+    it "returns 401 error if unauthorized" do
       user.emails << email
       user.save
       delete api("/user/emails/#{email.id}")
       expect(response).to have_http_status(401)
     end
 
-    it "should raise error for invalid ID" do
+    it "raises error for invalid ID" do
       expect{delete api("/users/emails/ASDF", admin) }.to raise_error(ActionController::RoutingError)
     end
   end
 
   describe 'PUT /user/:id/block' do
     before { admin }
-    it 'should block existing user' do
+    it 'blocks existing user' do
       put api("/users/#{user.id}/block", admin)
       expect(response).to have_http_status(200)
       expect(user.reload.state).to eq('blocked')
     end
 
-    it 'should not re-block ldap blocked users' do
+    it 'does not re-block ldap blocked users' do
       put api("/users/#{ldap_blocked_user.id}/block", admin)
       expect(response).to have_http_status(403)
       expect(ldap_blocked_user.reload.state).to eq('ldap_blocked')
     end
 
-    it 'should not be available for non admin users' do
+    it 'does not be available for non admin users' do
       put api("/users/#{user.id}/block", user)
       expect(response).to have_http_status(403)
       expect(user.reload.state).to eq('active')
     end
 
-    it 'should return a 404 error if user id not found' do
+    it 'returns a 404 error if user id not found' do
       put api('/users/9999/block', admin)
       expect(response).to have_http_status(404)
       expect(json_response['message']).to eq('404 User Not Found')
@@ -854,37 +851,37 @@ describe API::API, api: true  do
     let(:blocked_user)  { create(:user, state: 'blocked') }
     before { admin }
 
-    it 'should unblock existing user' do
+    it 'unblocks existing user' do
       put api("/users/#{user.id}/unblock", admin)
       expect(response).to have_http_status(200)
       expect(user.reload.state).to eq('active')
     end
 
-    it 'should unblock a blocked user' do
+    it 'unblocks a blocked user' do
       put api("/users/#{blocked_user.id}/unblock", admin)
       expect(response).to have_http_status(200)
       expect(blocked_user.reload.state).to eq('active')
     end
 
-    it 'should not unblock ldap blocked users' do
+    it 'does not unblock ldap blocked users' do
       put api("/users/#{ldap_blocked_user.id}/unblock", admin)
       expect(response).to have_http_status(403)
       expect(ldap_blocked_user.reload.state).to eq('ldap_blocked')
     end
 
-    it 'should not be available for non admin users' do
+    it 'does not be available for non admin users' do
       put api("/users/#{user.id}/unblock", user)
       expect(response).to have_http_status(403)
       expect(user.reload.state).to eq('active')
     end
 
-    it 'should return a 404 error if user id not found' do
+    it 'returns a 404 error if user id not found' do
       put api('/users/9999/block', admin)
       expect(response).to have_http_status(404)
       expect(json_response['message']).to eq('404 User Not Found')
     end
 
-    it "should raise error for invalid ID" do
+    it "raises error for invalid ID" do
       expect{put api("/users/ASDF/block", admin) }.to raise_error(ActionController::RoutingError)
     end
   end
diff --git a/spec/requests/api/variables_spec.rb b/spec/requests/api/variables_spec.rb
index ddba18245f862b0c31d168c8746d1d0b6152c5a3..05fbdb909dce664920b5a6f932f5f4228712be68 100644
--- a/spec/requests/api/variables_spec.rb
+++ b/spec/requests/api/variables_spec.rb
@@ -12,7 +12,7 @@ describe API::API, api: true do
 
   describe 'GET /projects/:id/variables' do
     context 'authorized user with proper permissions' do
-      it 'should return project variables' do
+      it 'returns project variables' do
         get api("/projects/#{project.id}/variables", user)
 
         expect(response).to have_http_status(200)
@@ -21,7 +21,7 @@ describe API::API, api: true do
     end
 
     context 'authorized user with invalid permissions' do
-      it 'should not return project variables' do
+      it 'does not return project variables' do
         get api("/projects/#{project.id}/variables", user2)
 
         expect(response).to have_http_status(403)
@@ -29,7 +29,7 @@ describe API::API, api: true do
     end
 
     context 'unauthorized user' do
-      it 'should not return project variables' do
+      it 'does not return project variables' do
         get api("/projects/#{project.id}/variables")
 
         expect(response).to have_http_status(401)
@@ -39,14 +39,14 @@ describe API::API, api: true do
 
   describe 'GET /projects/:id/variables/:key' do
     context 'authorized user with proper permissions' do
-      it 'should return project variable details' do
+      it 'returns project variable details' do
         get api("/projects/#{project.id}/variables/#{variable.key}", user)
 
         expect(response).to have_http_status(200)
         expect(json_response['value']).to eq(variable.value)
       end
 
-      it 'should respond with 404 Not Found if requesting non-existing variable' do
+      it 'responds with 404 Not Found if requesting non-existing variable' do
         get api("/projects/#{project.id}/variables/non_existing_variable", user)
 
         expect(response).to have_http_status(404)
@@ -54,7 +54,7 @@ describe API::API, api: true do
     end
 
     context 'authorized user with invalid permissions' do
-      it 'should not return project variable details' do
+      it 'does not return project variable details' do
         get api("/projects/#{project.id}/variables/#{variable.key}", user2)
 
         expect(response).to have_http_status(403)
@@ -62,7 +62,7 @@ describe API::API, api: true do
     end
 
     context 'unauthorized user' do
-      it 'should not return project variable details' do
+      it 'does not return project variable details' do
         get api("/projects/#{project.id}/variables/#{variable.key}")
 
         expect(response).to have_http_status(401)
@@ -72,7 +72,7 @@ describe API::API, api: true do
 
   describe 'POST /projects/:id/variables' do
     context 'authorized user with proper permissions' do
-      it 'should create variable' do
+      it 'creates variable' do
         expect do
           post api("/projects/#{project.id}/variables", user), key: 'TEST_VARIABLE_2', value: 'VALUE_2'
         end.to change{project.variables.count}.by(1)
@@ -82,7 +82,7 @@ describe API::API, api: true do
         expect(json_response['value']).to eq('VALUE_2')
       end
 
-      it 'should not allow to duplicate variable key' do
+      it 'does not allow to duplicate variable key' do
         expect do
           post api("/projects/#{project.id}/variables", user), key: variable.key, value: 'VALUE_2'
         end.to change{project.variables.count}.by(0)
@@ -92,7 +92,7 @@ describe API::API, api: true do
     end
 
     context 'authorized user with invalid permissions' do
-      it 'should not create variable' do
+      it 'does not create variable' do
         post api("/projects/#{project.id}/variables", user2)
 
         expect(response).to have_http_status(403)
@@ -100,7 +100,7 @@ describe API::API, api: true do
     end
 
     context 'unauthorized user' do
-      it 'should not create variable' do
+      it 'does not create variable' do
         post api("/projects/#{project.id}/variables")
 
         expect(response).to have_http_status(401)
@@ -110,7 +110,7 @@ describe API::API, api: true do
 
   describe 'PUT /projects/:id/variables/:key' do
     context 'authorized user with proper permissions' do
-      it 'should update variable data' do
+      it 'updates variable data' do
         initial_variable = project.variables.first
         value_before = initial_variable.value
 
@@ -123,7 +123,7 @@ describe API::API, api: true do
         expect(updated_variable.value).to eq('VALUE_1_UP')
       end
 
-      it 'should responde with 404 Not Found if requesting non-existing variable' do
+      it 'responds with 404 Not Found if requesting non-existing variable' do
         put api("/projects/#{project.id}/variables/non_existing_variable", user)
 
         expect(response).to have_http_status(404)
@@ -131,7 +131,7 @@ describe API::API, api: true do
     end
 
     context 'authorized user with invalid permissions' do
-      it 'should not update variable' do
+      it 'does not update variable' do
         put api("/projects/#{project.id}/variables/#{variable.key}", user2)
 
         expect(response).to have_http_status(403)
@@ -139,7 +139,7 @@ describe API::API, api: true do
     end
 
     context 'unauthorized user' do
-      it 'should not update variable' do
+      it 'does not update variable' do
         put api("/projects/#{project.id}/variables/#{variable.key}")
 
         expect(response).to have_http_status(401)
@@ -149,14 +149,14 @@ describe API::API, api: true do
 
   describe 'DELETE /projects/:id/variables/:key' do
     context 'authorized user with proper permissions' do
-      it 'should delete variable' do
+      it 'deletes variable' do
         expect do
           delete api("/projects/#{project.id}/variables/#{variable.key}", user)
         end.to change{project.variables.count}.by(-1)
         expect(response).to have_http_status(200)
       end
 
-      it 'should responde with 404 Not Found if requesting non-existing variable' do
+      it 'responds with 404 Not Found if requesting non-existing variable' do
         delete api("/projects/#{project.id}/variables/non_existing_variable", user)
 
         expect(response).to have_http_status(404)
@@ -164,7 +164,7 @@ describe API::API, api: true do
     end
 
     context 'authorized user with invalid permissions' do
-      it 'should not delete variable' do
+      it 'does not delete variable' do
         delete api("/projects/#{project.id}/variables/#{variable.key}", user2)
 
         expect(response).to have_http_status(403)
@@ -172,7 +172,7 @@ describe API::API, api: true do
     end
 
     context 'unauthorized user' do
-      it 'should not delete variable' do
+      it 'does not delete variable' do
         delete api("/projects/#{project.id}/variables/#{variable.key}")
 
         expect(response).to have_http_status(401)
diff --git a/spec/requests/ci/api/builds_spec.rb b/spec/requests/ci/api/builds_spec.rb
index cf1e8d9b514740d5e4544b60ba04c23dca420cec..ca7932dc5da3d1ef2aa71232121ab8f84000367c 100644
--- a/spec/requests/ci/api/builds_spec.rb
+++ b/spec/requests/ci/api/builds_spec.rb
@@ -6,112 +6,102 @@ describe Ci::API::API do
   let(:runner) { FactoryGirl.create(:ci_runner, tag_list: ["mysql", "ruby"]) }
   let(:project) { FactoryGirl.create(:empty_project) }
 
-  before do
-    stub_ci_pipeline_to_return_yaml_file
-  end
-
   describe "Builds API for runners" do
-    let(:shared_runner) { FactoryGirl.create(:ci_runner, token: "SharedRunner") }
-    let(:shared_project) { FactoryGirl.create(:empty_project, name: "SharedProject") }
+    let(:pipeline) { create(:ci_pipeline_without_jobs, project: project, ref: 'master') }
 
     before do
-      FactoryGirl.create :ci_runner_project, project: project, runner: runner
+      project.runners << runner
     end
 
     describe "POST /builds/register" do
-      it "should start a build" do
-        pipeline = FactoryGirl.create(:ci_pipeline, project: project, ref: 'master')
-        pipeline.create_builds(nil)
-        build = pipeline.builds.first
+      let!(:build) { create(:ci_build, pipeline: pipeline, name: 'spinach', stage: 'test', stage_idx: 0) }
 
-        post ci_api("/builds/register"), token: runner.token, info: { platform: :darwin }
+      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 "should return 404 error if no pending build found" do
-        post ci_api("/builds/register"), token: runner.token
-
-        expect(response).to have_http_status(404)
-      end
-
-      it "should return 404 error if no builds for specific runner" do
-        pipeline = FactoryGirl.create(:ci_pipeline, project: shared_project)
-        FactoryGirl.create(:ci_build, pipeline: pipeline, status: 'pending')
+      context 'when builds are finished' do
+        before do
+          build.success
+        end
 
-        post ci_api("/builds/register"), token: runner.token
+        it "returns 404 error if no builds for specific runner" do
+          register_builds
 
-        expect(response).to have_http_status(404)
+          expect(response).to have_http_status(404)
+        end
       end
 
-      it "should return 404 error if no builds for shared runner" do
-        pipeline = FactoryGirl.create(:ci_pipeline, project: project)
-        FactoryGirl.create(:ci_build, pipeline: pipeline, status: 'pending')
+      context 'for other project with builds' do
+        before do
+          build.success
+          create(:ci_build, :pending)
+        end
 
-        post ci_api("/builds/register"), token: shared_runner.token
+        it "returns 404 error if no builds for shared runner" do
+          register_builds
 
-        expect(response).to have_http_status(404)
+          expect(response).to have_http_status(404)
+        end
       end
 
-      it "returns options" do
-        pipeline = FactoryGirl.create(:ci_pipeline, project: project, ref: 'master')
-        pipeline.create_builds(nil)
+      context 'for shared runner' do
+        let(:shared_runner) { create(:ci_runner, token: "SharedRunner") }
 
-        post ci_api("/builds/register"), token: runner.token, info: { platform: :darwin }
+        it "should return 404 error if no builds for shared runner" do
+          register_builds shared_runner.token
 
-        expect(response).to have_http_status(201)
-        expect(json_response["options"]).to eq({ "image" => "ruby:2.1", "services" => ["postgres"] })
+          expect(response).to have_http_status(404)
+        end
       end
 
-      it "returns variables" do
-        pipeline = FactoryGirl.create(:ci_pipeline, project: project, ref: 'master')
-        pipeline.create_builds(nil)
-        project.variables << Ci::Variable.new(key: "SECRET_KEY", value: "secret_value")
-
-        post ci_api("/builds/register"), token: runner.token, info: { platform: :darwin }
+      context 'for triggered build' do
+        before do
+          trigger = create(:ci_trigger, project: project)
+          create(:ci_trigger_request_with_variables, pipeline: pipeline, builds: [build], trigger: trigger)
+          project.variables << Ci::Variable.new(key: "SECRET_KEY", value: "secret_value")
+        end
 
-        expect(response).to have_http_status(201)
-        expect(json_response["variables"]).to include(
-          { "key" => "CI_BUILD_NAME", "value" => "spinach", "public" => true },
-          { "key" => "CI_BUILD_STAGE", "value" => "test", "public" => true },
-          { "key" => "DB_NAME", "value" => "postgres", "public" => true },
-          { "key" => "SECRET_KEY", "value" => "secret_value", "public" => false }
-        )
+        it "returns variables for triggers" do
+          register_builds info: { platform: :darwin }
+
+          expect(response).to have_http_status(201)
+          expect(json_response["variables"]).to include(
+            { "key" => "CI_BUILD_NAME", "value" => "spinach", "public" => true },
+            { "key" => "CI_BUILD_STAGE", "value" => "test", "public" => true },
+            { "key" => "CI_BUILD_TRIGGERED", "value" => "true", "public" => true },
+            { "key" => "DB_NAME", "value" => "postgres", "public" => true },
+            { "key" => "SECRET_KEY", "value" => "secret_value", "public" => false },
+            { "key" => "TRIGGER_KEY_1", "value" => "TRIGGER_VALUE_1", "public" => false },
+          )
+        end
       end
 
-      it "returns variables for triggers" do
-        trigger = FactoryGirl.create(:ci_trigger, project: project)
-        pipeline = FactoryGirl.create(:ci_pipeline, project: project, ref: 'master')
-
-        trigger_request = FactoryGirl.create(:ci_trigger_request_with_variables, pipeline: pipeline, trigger: trigger)
-        pipeline.create_builds(nil, trigger_request)
-        project.variables << Ci::Variable.new(key: "SECRET_KEY", value: "secret_value")
-
-        post ci_api("/builds/register"), token: runner.token, info: { platform: :darwin }
-
-        expect(response).to have_http_status(201)
-        expect(json_response["variables"]).to include(
-          { "key" => "CI_BUILD_NAME", "value" => "spinach", "public" => true },
-          { "key" => "CI_BUILD_STAGE", "value" => "test", "public" => true },
-          { "key" => "CI_BUILD_TRIGGERED", "value" => "true", "public" => true },
-          { "key" => "DB_NAME", "value" => "postgres", "public" => true },
-          { "key" => "SECRET_KEY", "value" => "secret_value", "public" => false },
-          { "key" => "TRIGGER_KEY_1", "value" => "TRIGGER_VALUE_1", "public" => false }
-        )
-      end
+      context 'with multiple builds' do
+        before do
+          build.success
+        end
 
-      it "returns dependent builds" do
-        pipeline = FactoryGirl.create(:ci_pipeline, project: project, ref: 'master')
-        pipeline.create_builds(nil, nil)
-        pipeline.builds.where(stage: 'test').each(&:success)
+        let!(:test_build) { create(:ci_build, pipeline: pipeline, name: 'deploy', stage: 'deploy', stage_idx: 1) }
 
-        post ci_api("/builds/register"), token: runner.token, info: { platform: :darwin }
+        it "returns dependent builds" do
+          register_builds info: { platform: :darwin }
 
-        expect(response).to have_http_status(201)
-        expect(json_response["depends_on_builds"].count).to eq(2)
-        expect(json_response["depends_on_builds"][0]["name"]).to eq("rspec")
+          expect(response).to have_http_status(201)
+          expect(json_response["id"]).to eq(test_build.id)
+          expect(json_response["depends_on_builds"].count).to eq(1)
+          expect(json_response["depends_on_builds"][0]).to include('id' => build.id, 'name' => 'spinach')
+        end
       end
 
       %w(name version revision platform architecture).each do |param|
@@ -121,8 +111,9 @@ describe Ci::API::API do
           subject { runner.read_attribute(param.to_sym) }
 
           it do
-            post ci_api("/builds/register"), token: runner.token, info: { param => value }
-            expect(response).to have_http_status(404)
+            register_builds info: { param => value }
+
+            expect(response).to have_http_status(201)
             runner.reload
             is_expected.to eq(value)
           end
@@ -131,8 +122,7 @@ describe Ci::API::API do
 
       context 'when build has no tags' do
         before do
-          pipeline = create(:ci_pipeline, project: project)
-          create(:ci_build, pipeline: pipeline, tags: [])
+          build.update(tags: [])
         end
 
         context 'when runner is allowed to pick untagged builds' do
@@ -154,42 +144,40 @@ describe Ci::API::API do
             expect(response).to have_http_status 404
           end
         end
+      end
 
-        def register_builds
-          post ci_api("/builds/register"), token: runner.token,
-                                           info: { platform: :darwin }
-        end
+      def register_builds(token = runner.token, **params)
+        post ci_api("/builds/register"), params.merge(token: token)
       end
     end
 
     describe "PUT /builds/:id" do
-      let(:pipeline) {create(:ci_pipeline, project: project)}
-      let(:build) { create(:ci_build, :trace, pipeline: pipeline, runner_id: runner.id) }
+      let(:build) { create(:ci_build, :pending, :trace, pipeline: pipeline, runner_id: runner.id) }
 
       before do
         build.run!
         put ci_api("/builds/#{build.id}"), token: runner.token
       end
 
-      it "should update a running build" do
+      it "updates a running build" do
         expect(response).to have_http_status(200)
       end
 
-      it 'should not override trace information when no trace is given' do
+      it 'does not override trace information when no trace is given' do
         expect(build.reload.trace).to eq 'BUILD TRACE'
       end
 
       context 'build has been erased' do
         let(:build) { create(:ci_build, runner_id: runner.id, erased_at: Time.now) }
 
-        it 'should respond with forbidden' do
+        it 'responds with forbidden' do
           expect(response.status).to eq 403
         end
       end
     end
 
     describe 'PATCH /builds/:id/trace.txt' do
-      let(:build) { create(:ci_build, :trace, runner_id: runner.id) }
+      let(:build) { create(:ci_build, :pending, :trace, runner_id: runner.id) }
       let(:headers) { { Ci::API::Helpers::BUILD_TOKEN_HEADER => build.token, 'Content-Type' => 'text/plain' } }
       let(:headers_with_range) { headers.merge({ 'Content-Range' => '11-20' }) }
 
@@ -237,8 +225,7 @@ describe Ci::API::API do
     context "Artifacts" do
       let(:file_upload) { fixture_file_upload(Rails.root + 'spec/fixtures/banana_sample.gif', 'image/gif') }
       let(:file_upload2) { fixture_file_upload(Rails.root + 'spec/fixtures/dk.png', 'image/gif') }
-      let(:pipeline) { create(:ci_pipeline, project: project) }
-      let(:build) { create(:ci_build, pipeline: pipeline, runner_id: runner.id) }
+      let(:build) { create(:ci_build, :pending, pipeline: pipeline, runner_id: runner.id) }
       let(:authorize_url) { ci_api("/builds/#{build.id}/artifacts/authorize") }
       let(:post_url) { ci_api("/builds/#{build.id}/artifacts") }
       let(:delete_url) { ci_api("/builds/#{build.id}/artifacts") }
@@ -280,7 +267,7 @@ describe Ci::API::API do
         context 'authorization token is invalid' do
           before { post authorize_url, { token: 'invalid', filesize: 100 } }
 
-          it 'should respond with forbidden' do
+          it 'responds with forbidden' do
             expect(response).to have_http_status(403)
           end
         end
@@ -300,7 +287,7 @@ describe Ci::API::API do
               upload_artifacts(file_upload, headers_with_token)
             end
 
-            it 'should respond with forbidden' do
+            it 'responds with forbidden' do
               expect(response.status).to eq 403
             end
           end
@@ -342,7 +329,7 @@ describe Ci::API::API do
             end
           end
 
-          context 'should post artifacts file and metadata file' do
+          context 'posts artifacts file and metadata file' do
             let!(:artifacts) { file_upload }
             let!(:metadata) { file_upload2 }
 
@@ -354,7 +341,7 @@ describe Ci::API::API do
               post(post_url, post_data, headers_with_token)
             end
 
-            context 'post data accelerated by workhorse is correct' do
+            context 'posts data accelerated by workhorse is correct' do
               let(:post_data) do
                 { 'file.path' => artifacts.path,
                   'file.name' => artifacts.original_filename,
@@ -422,7 +409,7 @@ describe Ci::API::API do
           end
 
           context "artifacts file is too large" do
-            it "should fail to post too large artifact" do
+            it "fails to post too large artifact" do
               stub_application_setting(max_artifacts_size: 0)
               upload_artifacts(file_upload, headers_with_token)
               expect(response).to have_http_status(413)
@@ -430,14 +417,14 @@ describe Ci::API::API do
           end
 
           context "artifacts post request does not contain file" do
-            it "should fail to post artifacts without file" do
+            it "fails to post artifacts without file" do
               post post_url, {}, headers_with_token
               expect(response).to have_http_status(400)
             end
           end
 
           context 'GitLab Workhorse is not configured' do
-            it "should fail to post artifacts without GitLab-Workhorse" do
+            it "fails to post artifacts without GitLab-Workhorse" do
               post post_url, { token: build.token }, {}
               expect(response).to have_http_status(403)
             end
@@ -456,7 +443,7 @@ describe Ci::API::API do
             FileUtils.remove_entry @tmpdir
           end
 
-          it "should fail to post artifacts for outside of tmp path" do
+          it "fails to post artifacts for outside of tmp path" do
             upload_artifacts(file_upload, headers_with_token)
             expect(response).to have_http_status(400)
           end
@@ -482,7 +469,7 @@ describe Ci::API::API do
           build.reload
         end
 
-        it 'should remove build artifacts' do
+        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
@@ -500,14 +487,14 @@ describe Ci::API::API do
               'Content-Disposition' => 'attachment; filename=ci_build_artifacts.zip' }
           end
 
-          it 'should download artifact' do
+          it 'downloads artifact' do
             expect(response).to have_http_status(200)
             expect(response.headers).to include download_headers
           end
         end
 
         context 'build does not has artifacts' do
-          it 'should respond with not found' do
+          it 'responds with not found' do
             expect(response).to have_http_status(404)
           end
         end
diff --git a/spec/requests/ci/api/triggers_spec.rb b/spec/requests/ci/api/triggers_spec.rb
index f12678e5a8ecd0fe36306c7962b13780f9ef6be3..0a0f979f57d659e57acb9a1748e614ad88f1f90f 100644
--- a/spec/requests/ci/api/triggers_spec.rb
+++ b/spec/requests/ci/api/triggers_spec.rb
@@ -19,17 +19,17 @@ describe Ci::API::API do
     end
 
     context 'Handles errors' do
-      it 'should return bad request if token is missing' do
+      it 'returns bad request if token is missing' do
         post ci_api("/projects/#{project.ci_id}/refs/master/trigger")
         expect(response).to have_http_status(400)
       end
 
-      it 'should return not found if project is not found' do
+      it 'returns not found if project is not found' do
         post ci_api('/projects/0/refs/master/trigger'), options
         expect(response).to have_http_status(404)
       end
 
-      it 'should return unauthorized if token is for different project' do
+      it 'returns unauthorized if token is for different project' do
         post ci_api("/projects/#{project2.ci_id}/refs/master/trigger"), options
         expect(response).to have_http_status(401)
       end
@@ -38,14 +38,15 @@ describe Ci::API::API do
     context 'Have a commit' do
       let(:pipeline) { project.pipelines.last }
 
-      it 'should create builds' do
+      it 'creates builds' do
         post ci_api("/projects/#{project.ci_id}/refs/master/trigger"), options
         expect(response).to have_http_status(201)
         pipeline.builds.reload
-        expect(pipeline.builds.size).to eq(2)
+        expect(pipeline.builds.pending.size).to eq(2)
+        expect(pipeline.builds.size).to eq(5)
       end
 
-      it 'should return bad request with no builds created if there\'s no commit for that ref' do
+      it 'returns bad request with no builds created if there\'s no commit for that ref' do
         post ci_api("/projects/#{project.ci_id}/refs/other-branch/trigger"), options
         expect(response).to have_http_status(400)
         expect(json_response['message']).to eq('No builds created')
@@ -56,19 +57,19 @@ describe Ci::API::API do
           { 'TRIGGER_KEY' => 'TRIGGER_VALUE' }
         end
 
-        it 'should validate variables to be a hash' do
+        it 'validates variables to be a hash' do
           post ci_api("/projects/#{project.ci_id}/refs/master/trigger"), options.merge(variables: 'value')
           expect(response).to have_http_status(400)
           expect(json_response['message']).to eq('variables needs to be a hash')
         end
 
-        it 'should validate variables needs to be a map of key-valued strings' do
+        it 'validates variables needs to be a map of key-valued strings' do
           post ci_api("/projects/#{project.ci_id}/refs/master/trigger"), options.merge(variables: { key: %w(1 2) })
           expect(response).to have_http_status(400)
           expect(json_response['message']).to eq('variables needs to be a map of key-valued strings')
         end
 
-        it 'create trigger request with variables' do
+        it 'creates trigger request with variables' do
           post ci_api("/projects/#{project.ci_id}/refs/master/trigger"), options.merge(variables: variables)
           expect(response).to have_http_status(201)
           pipeline.builds.reload
diff --git a/spec/requests/git_http_spec.rb b/spec/requests/git_http_spec.rb
index 82ab582beace731f4a57baee10450cca3f5691a1..afaf4b7cefbe0578daa2097e7475dddcfc495f86 100644
--- a/spec/requests/git_http_spec.rb
+++ b/spec/requests/git_http_spec.rb
@@ -75,9 +75,9 @@ describe 'Git HTTP requests', lib: true do
       context "with correct credentials" do
         let(:env) { { user: user.username, password: user.password } }
 
-        it "uploads get status 200 (because Git hooks do the real check)" do
+        it "uploads get status 403" do
           upload(path, env) do |response|
-            expect(response).to have_http_status(200)
+            expect(response).to have_http_status(403)
           end
         end
 
@@ -86,7 +86,7 @@ describe 'Git HTTP requests', lib: true do
             allow(Gitlab.config.gitlab_shell).to receive(:receive_pack).and_return(false)
 
             upload(path, env) do |response|
-              expect(response).to have_http_status(404)
+              expect(response).to have_http_status(403)
             end
           end
         end
@@ -198,6 +198,45 @@ describe 'Git HTTP requests', lib: true do
               end
             end
 
+            context 'when user has 2FA enabled' do
+              let(:user) { create(:user, :two_factor) }
+              let(:access_token) { create(:personal_access_token, user: user) }
+
+              before do
+                project.team << [user, :master]
+              end
+
+              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
+
+              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
+
             context "when blank password attempts follow a valid login" do
               def attempt_login(include_password)
                 password = include_password ? user.password : ""
@@ -236,9 +275,9 @@ describe 'Git HTTP requests', lib: true do
               end
             end
 
-            it "uploads get status 200 (because Git hooks do the real check)" do
+            it "uploads get status 404" do
               upload(path, user: user.username, password: user.password) do |response|
-                expect(response).to have_http_status(200)
+                expect(response).to have_http_status(404)
               end
             end
           end
@@ -349,19 +388,19 @@ describe 'Git HTTP requests', lib: true do
     end
   end
 
-  def clone_get(project, options={})
+  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={})
+  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={})
+  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={})
+  def push_post(project, options = {})
     post "/#{project}/git-receive-pack", {}, auth_env(*options.values_at(:user, :password, :spnego_request_token))
   end
 
diff --git a/spec/requests/lfs_http_spec.rb b/spec/requests/lfs_http_spec.rb
index 93d2bc160cc8fc3ca3abcc52b3c773417a24c0b5..4c9b4a8ba422ef6f921b5130557ca82628bcdec2 100644
--- a/spec/requests/lfs_http_spec.rb
+++ b/spec/requests/lfs_http_spec.rb
@@ -1,6 +1,6 @@
 require 'spec_helper'
 
-describe Gitlab::Lfs::Router do
+describe 'Git LFS API and storage' do
   let(:user) { create(:user) }
   let!(:lfs_object) { create(:lfs_object, :with_file) }
 
@@ -31,10 +31,11 @@ describe Gitlab::Lfs::Router do
         'operation' => 'upload'
       }
     end
+    let(:authorization) { authorize_user }
 
     before do
       allow(Gitlab.config.lfs).to receive(:enabled).and_return(false)
-      post_json "#{project.http_url_to_repo}/info/lfs/objects/batch", body, headers
+      post_lfs_json "#{project.http_url_to_repo}/info/lfs/objects/batch", body, headers
     end
 
     it 'responds with 501' do
@@ -71,8 +72,9 @@ describe Gitlab::Lfs::Router do
     end
 
     context 'when handling lfs request using deprecated API' do
+      let(:authorization) { authorize_user }
       before do
-        post_json "#{project.http_url_to_repo}/info/lfs/objects", nil, headers
+        post_lfs_json "#{project.http_url_to_repo}/info/lfs/objects", nil, headers
       end
 
       it_behaves_like 'a deprecated'
@@ -118,8 +120,8 @@ describe Gitlab::Lfs::Router do
               project.lfs_objects << lfs_object
             end
 
-            it 'responds with status 403' do
-              expect(response).to have_http_status(403)
+            it 'responds with status 404' do
+              expect(response).to have_http_status(404)
             end
           end
 
@@ -147,8 +149,8 @@ describe Gitlab::Lfs::Router do
       context 'without required headers' do
         let(:authorization) { authorize_user }
 
-        it 'responds with status 403' do
-          expect(response).to have_http_status(403)
+        it 'responds with status 404' do
+          expect(response).to have_http_status(404)
         end
       end
     end
@@ -162,7 +164,7 @@ describe Gitlab::Lfs::Router do
       enable_lfs
       update_lfs_permissions
       update_user_permissions
-      post_json "#{project.http_url_to_repo}/info/lfs/objects/batch", body, headers
+      post_lfs_json "#{project.http_url_to_repo}/info/lfs/objects/batch", body, headers
     end
 
     describe 'download' do
@@ -304,10 +306,10 @@ describe Gitlab::Lfs::Router do
         end
 
         context 'when user does is not member of the project' do
-          let(:role) { :guest }
+          let(:update_user_permissions) { nil }
 
-          it 'responds with 403' do
-            expect(response).to have_http_status(403)
+          it 'responds with 404' do
+            expect(response).to have_http_status(404)
           end
         end
 
@@ -510,6 +512,7 @@ describe Gitlab::Lfs::Router do
 
     describe 'unsupported' do
       let(:project) { create(:empty_project) }
+      let(:authorization) { authorize_user }
       let(:body) do
         { 'operation' => 'other',
           'objects' => [
@@ -553,11 +556,11 @@ describe Gitlab::Lfs::Router do
 
       context 'and request is sent with a malformed headers' do
         before do
-          put_finalize('cat /etc/passwd')
+          put_finalize('/etc/passwd')
         end
 
         it 'does not recognize it as a valid lfs command' do
-          expect(response).to have_http_status(403)
+          expect(response).to have_http_status(401)
         end
       end
     end
@@ -582,6 +585,16 @@ describe Gitlab::Lfs::Router do
           expect(response).to have_http_status(403)
         end
       end
+
+      context 'and request is sent with a malformed headers' do
+        before do
+          put_finalize('/etc/passwd')
+        end
+
+        it 'does not recognize it as a valid lfs command' do
+          expect(response).to have_http_status(403)
+        end
+      end
     end
 
     describe 'to one project' do
@@ -624,9 +637,25 @@ describe Gitlab::Lfs::Router do
               expect(lfs_object.projects.pluck(:id)).to include(project.id)
             end
           end
+
+          context 'invalid tempfiles' do
+            it 'rejects slashes in the tempfile name (path traversal' do
+              put_finalize('foo/bar')
+              expect(response).to have_http_status(403)
+            end
+
+            it 'rejects tempfile names that do not start with the oid' do
+              put_finalize("foo#{sample_oid}")
+              expect(response).to have_http_status(403)
+            end
+          end
         end
 
         describe 'and user does not have push access' do
+          before do
+            project.team << [user, :reporter]
+          end
+
           it_behaves_like 'forbidden'
         end
       end
@@ -758,8 +787,8 @@ describe Gitlab::Lfs::Router do
     Projects::ForkService.new(project, user, {}).execute
   end
 
-  def post_json(url, body = nil, headers = nil)
-    post(url, body.try(:to_json), (headers || {}).merge('Content-Type' => 'application/json'))
+  def post_lfs_json(url, body = nil, headers = nil)
+    post(url, body.try(:to_json), (headers || {}).merge('Content-Type' => 'application/vnd.git-lfs+json'))
   end
 
   def json_response
diff --git a/spec/routing/project_routing_spec.rb b/spec/routing/project_routing_spec.rb
index 9151cd3aefed4f7d0fdb147c9990e022f7e877a2..77842057a1044917fb1a61a1ef406ea8f044fe5a 100644
--- a/spec/routing/project_routing_spec.rb
+++ b/spec/routing/project_routing_spec.rb
@@ -60,7 +60,7 @@ end
 #                  project GET    /:id(.:format)          projects#show
 #                          PUT    /:id(.:format)          projects#update
 #                          DELETE /:id(.:format)          projects#destroy
-# markdown_preview_project POST   /:id/markdown_preview(.:format) projects#markdown_preview
+# preview_markdown_project POST   /:id/preview_markdown(.:format) projects#preview_markdown
 describe ProjectsController, 'routing' do
   it 'to #create' do
     expect(post('/projects')).to route_to('projects#create')
@@ -91,9 +91,9 @@ describe ProjectsController, 'routing' do
     expect(delete('/gitlab/gitlabhq')).to route_to('projects#destroy', namespace_id: 'gitlab', id: 'gitlabhq')
   end
 
-  it 'to #markdown_preview' do
-    expect(post('/gitlab/gitlabhq/markdown_preview')).to(
-      route_to('projects#markdown_preview', namespace_id: 'gitlab', id: 'gitlabhq')
+  it 'to #preview_markdown' do
+    expect(post('/gitlab/gitlabhq/preview_markdown')).to(
+      route_to('projects#preview_markdown', namespace_id: 'gitlab', id: 'gitlabhq')
     )
   end
 end
@@ -479,13 +479,16 @@ end
 describe Projects::NetworkController, 'routing' do
   it 'to #show' do
     expect(get('/gitlab/gitlabhq/network/master')).to route_to('projects/network#show', namespace_id: 'gitlab', project_id: 'gitlabhq', id: 'master')
-    expect(get('/gitlab/gitlabhq/network/master.json')).to route_to('projects/network#show', namespace_id: 'gitlab', project_id: 'gitlabhq', id: 'master', format: 'json')
+    expect(get('/gitlab/gitlabhq/network/ends-with.json')).to route_to('projects/network#show', namespace_id: 'gitlab', project_id: 'gitlabhq', id: 'ends-with.json')
+    expect(get('/gitlab/gitlabhq/network/master?format=json')).to route_to('projects/network#show', namespace_id: 'gitlab', project_id: 'gitlabhq', id: 'master', format: 'json')
   end
 end
 
 describe Projects::GraphsController, 'routing' do
   it 'to #show' do
     expect(get('/gitlab/gitlabhq/graphs/master')).to route_to('projects/graphs#show', namespace_id: 'gitlab', project_id: 'gitlabhq', id: 'master')
+    expect(get('/gitlab/gitlabhq/graphs/ends-with.json')).to route_to('projects/graphs#show', namespace_id: 'gitlab', project_id: 'gitlabhq', id: 'ends-with.json')
+    expect(get('/gitlab/gitlabhq/graphs/master?format=json')).to route_to('projects/graphs#show', namespace_id: 'gitlab', project_id: 'gitlabhq', id: 'master', format: 'json')
   end
 end
 
diff --git a/spec/routing/routing_spec.rb b/spec/routing/routing_spec.rb
index 1d4df9197f68824fc2aac271d1f76f9276b5b230..d65648dd0b29b5c813908fb1977efbd0245c77c9 100644
--- a/spec/routing/routing_spec.rb
+++ b/spec/routing/routing_spec.rb
@@ -116,12 +116,19 @@ describe HelpController, "routing" do
     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
diff --git a/spec/services/boards/create_service_spec.rb b/spec/services/boards/create_service_spec.rb
new file mode 100644
index 0000000000000000000000000000000000000000..a1a4dd4c57c991b5a468ec9494bd7686bf525ed6
--- /dev/null
+++ b/spec/services/boards/create_service_spec.rb
@@ -0,0 +1,35 @@
+require 'spec_helper'
+
+describe Boards::CreateService, services: true do
+  describe '#execute' do
+    subject(:service) { described_class.new(project, double) }
+
+    context 'when project does not have a board' do
+      let(:project) { create(:empty_project, board: nil) }
+
+      it 'creates a new board' do
+        expect { service.execute }.to change(Board, :count).by(1)
+      end
+
+      it 'creates default lists' do
+        service.execute
+
+        expect(project.board.lists.size).to eq 2
+        expect(project.board.lists.first).to be_backlog
+        expect(project.board.lists.last).to be_done
+      end
+    end
+
+    context 'when project has a board' do
+      let!(:project) { create(:project_with_board) }
+
+      it 'does not create a new board' do
+        expect { service.execute }.not_to change(Board, :count)
+      end
+
+      it 'does not create board lists' do
+        expect { service.execute }.not_to change(project.board.lists, :count)
+      end
+    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..f7f45983d26e6a4490fde59d8aedfcb637f01d63
--- /dev/null
+++ b/spec/services/boards/issues/list_service_spec.rb
@@ -0,0 +1,73 @@
+require 'spec_helper'
+
+describe Boards::Issues::ListService, services: true do
+  describe '#execute' do
+    let(:user)    { create(:user) }
+    let(:project) { create(:project_with_board) }
+    let(:board)   { project.board }
+
+    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 = { 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 = { 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 = { 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 = { id: list1.id }
+
+        issues = described_class.new(project, user, params).execute
+
+        expect(issues).to eq [list1_issue3, list1_issue1, list1_issue2]
+      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..0122159cab8066218a47a52673679a7401ee52bc
--- /dev/null
+++ b/spec/services/boards/issues/move_service_spec.rb
@@ -0,0 +1,140 @@
+require 'spec_helper'
+
+describe Boards::Issues::MoveService, services: true do
+  describe '#execute' do
+    let(:user)    { create(:user) }
+    let(:project) { create(:project_with_board) }
+    let(:board)   { project.board }
+
+    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: 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) }
+
+    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 = { 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 = { 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 = { 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) { { 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(:issue)  { create(:labeled_issue, project: project, labels: [bug, development, testing]) }
+      let(:params) { { 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 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) { { 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 = { 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) { { 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/lists/create_service_spec.rb b/spec/services/boards/lists/create_service_spec.rb
new file mode 100644
index 0000000000000000000000000000000000000000..5e7e145065ed43dd99dd37bc015f9f71a13b655c
--- /dev/null
+++ b/spec/services/boards/lists/create_service_spec.rb
@@ -0,0 +1,54 @@
+require 'spec_helper'
+
+describe Boards::Lists::CreateService, services: true do
+  describe '#execute' do
+    let(:project) { create(:project_with_board) }
+    let(:board)   { project.board }
+    let(:user)    { create(:user) }
+    let(:label)   { create(:label, name: 'in-progress') }
+
+    subject(:service) { described_class.new(project, user, label_id: label.id) }
+
+    context 'when board lists is empty' do
+      it 'creates a new list at beginning of the list' do
+        list = service.execute
+
+        expect(list.position).to eq 0
+      end
+    end
+
+    context 'when board lists has only a backlog list' do
+      it 'creates a new list at beginning of the list' do
+        create(:backlog_list, board: board)
+
+        list = service.execute
+
+        expect(list.position).to eq 0
+      end
+    end
+
+    context 'when board lists has only 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
+
+        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
+        create(:backlog_list, board: board)
+        create(:done_list, board: board)
+        list1 = create(:list, board: board, position: 0)
+
+        list2 = service.execute
+
+        expect(list1.reload.position).to eq 0
+        expect(list2.reload.position).to eq 1
+      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..6eff445feeef6353afcb91d0327e605541a79842
--- /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(:project_with_board) }
+    let(:board)   { project.board }
+    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     = create(:backlog_list, board: board)
+        development = create(:list, board: board, position: 0)
+        review      = create(:list, board: board, position: 1)
+        staging     = create(:list, board: board, position: 2)
+        done        = create(:done_list, board: board)
+
+        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 = create(:backlog_list, board: board)
+      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 = create(:done_list, board: board)
+      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..9fd39122737088d4ceeb5b4815ecc99df685698d
--- /dev/null
+++ b/spec/services/boards/lists/generate_service_spec.rb
@@ -0,0 +1,40 @@
+require 'spec_helper'
+
+describe Boards::Lists::GenerateService, services: true do
+  describe '#execute' do
+    let(:project) { create(:project_with_board) }
+    let(:board)   { project.board }
+    let(:user)    { create(:user) }
+
+    subject(:service) { described_class.new(project, user) }
+
+    context 'when board lists is empty' do
+      it 'creates the default lists' do
+        expect { service.execute }.to change(board.lists, :count).by(4)
+      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 }.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 }.to change(project.labels, :count).by(4)
+      end
+    end
+
+    context 'when project labels contains some of list label' do
+      it 'creates the missing labels' do
+        create(:label, project: project, name: 'Development')
+        create(:label, project: project, name: 'Ready')
+
+        expect { service.execute }.to change(project.labels, :count).by(2)
+      end
+    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..3e9b7d07fc68464e939b766353b00af1c1be029d
--- /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(:project_with_board) }
+    let(:board)   { project.board }
+    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/create_builds_service_spec.rb b/spec/services/ci/create_builds_service_spec.rb
deleted file mode 100644
index 8b0becd83d3805ceb6a47ae87cea2c26fb576455..0000000000000000000000000000000000000000
--- a/spec/services/ci/create_builds_service_spec.rb
+++ /dev/null
@@ -1,32 +0,0 @@
-require 'spec_helper'
-
-describe Ci::CreateBuildsService, services: true do
-  let(:pipeline) { create(:ci_pipeline, ref: 'master') }
-  let(:user) { create(:user) }
-
-  describe '#execute' do
-    # Using stubbed .gitlab-ci.yml created in commit factory
-    #
-
-    subject do
-      described_class.new(pipeline).execute('test', user, status, nil)
-    end
-
-    context 'next builds available' do
-      let(:status) { 'success' }
-
-      it { is_expected.to be_an_instance_of Array }
-      it { is_expected.to all(be_an_instance_of Ci::Build) }
-
-      it 'does not persist created builds' do
-        expect(subject.first).not_to be_persisted
-      end
-    end
-
-    context 'builds skipped' do
-      let(:status) { 'skipped' }
-
-      it { is_expected.to be_empty }
-    end
-  end
-end
diff --git a/spec/services/ci/create_pipeline_service_spec.rb b/spec/services/ci/create_pipeline_service_spec.rb
new file mode 100644
index 0000000000000000000000000000000000000000..4aadd009f3ecf0316e6ae28b9de734f317a29e69
--- /dev/null
+++ b/spec/services/ci/create_pipeline_service_spec.rb
@@ -0,0 +1,214 @@
+require 'spec_helper'
+
+describe Ci::CreatePipelineService, services: true do
+  let(:project) { FactoryGirl.create(:project) }
+  let(:user) { create(:admin) }
+
+  before do
+    stub_ci_pipeline_to_return_yaml_file
+  end
+
+  describe '#execute' do
+    def execute(params)
+      described_class.new(project, user, params).execute
+    end
+
+    context 'valid params' do
+      let(:pipeline) do
+        execute(ref: 'refs/heads/master',
+                before: '00000000',
+                after: project.commit.id,
+                commits: [{ message: "Message" }])
+      end
+
+      it { expect(pipeline).to be_kind_of(Ci::Pipeline) }
+      it { expect(pipeline).to be_valid }
+      it { expect(pipeline).to be_persisted }
+      it { expect(pipeline).to eq(project.pipelines.last) }
+      it { expect(pipeline).to have_attributes(user: user) }
+      it { expect(pipeline.builds.first).to be_kind_of(Ci::Build) }
+    end
+
+    context "skip tag if there is no build for it" do
+      it "creates commit if there is appropriate job" do
+        result = execute(ref: 'refs/heads/master',
+                         before: '00000000',
+                         after: project.commit.id,
+                         commits: [{ message: "Message" }])
+        expect(result).to be_persisted
+      end
+
+      it "creates commit if there is no appropriate job but deploy job has right ref setting" do
+        config = YAML.dump({ deploy: { script: "ls", only: ["master"] } })
+        stub_ci_pipeline_yaml_file(config)
+        result = execute(ref: 'refs/heads/master',
+                         before: '00000000',
+                         after: project.commit.id,
+                         commits: [{ message: "Message" }])
+
+        expect(result).to be_persisted
+      end
+    end
+
+    it 'skips creating pipeline for refs without .gitlab-ci.yml' do
+      stub_ci_pipeline_yaml_file(nil)
+      result = execute(ref: 'refs/heads/master',
+                       before: '00000000',
+                       after: project.commit.id,
+                       commits: [{ message: 'Message' }])
+
+      expect(result).not_to be_persisted
+      expect(Ci::Pipeline.count).to eq(0)
+    end
+
+    it 'fails commits if yaml is invalid' do
+      message = 'message'
+      allow_any_instance_of(Ci::Pipeline).to receive(:git_commit_message) { message }
+      stub_ci_pipeline_yaml_file('invalid: file: file')
+      commits = [{ message: message }]
+      pipeline = execute(ref: 'refs/heads/master',
+                         before: '00000000',
+                         after: project.commit.id,
+                         commits: commits)
+
+      expect(pipeline).to be_persisted
+      expect(pipeline.builds.any?).to be false
+      expect(pipeline.status).to eq('failed')
+      expect(pipeline.yaml_errors).not_to be_nil
+    end
+
+    context 'when commit contains a [ci skip] directive' do
+      let(:message) { "some message[ci skip]" }
+      let(:messageFlip) { "some message[skip ci]" }
+      let(:capMessage) { "some message[CI SKIP]" }
+      let(:capMessageFlip) { "some message[SKIP CI]" }
+
+      before do
+        allow_any_instance_of(Ci::Pipeline).to receive(:git_commit_message) { message }
+      end
+
+      it "skips builds creation if there is [ci skip] tag in commit message" do
+        commits = [{ message: message }]
+        pipeline = execute(ref: 'refs/heads/master',
+                           before: '00000000',
+                           after: project.commit.id,
+                           commits: commits)
+
+        expect(pipeline).to be_persisted
+        expect(pipeline.builds.any?).to be false
+        expect(pipeline.status).to eq("skipped")
+      end
+
+      it "skips builds creation if there is [skip ci] tag in commit message" do
+        commits = [{ message: messageFlip }]
+        pipeline = execute(ref: 'refs/heads/master',
+                           before: '00000000',
+                           after: project.commit.id,
+                           commits: commits)
+
+        expect(pipeline).to be_persisted
+        expect(pipeline.builds.any?).to be false
+        expect(pipeline.status).to eq("skipped")
+      end
+
+      it "skips builds creation if there is [CI SKIP] tag in commit message" do
+        commits = [{ message: capMessage }]
+        pipeline = execute(ref: 'refs/heads/master',
+                           before: '00000000',
+                           after: project.commit.id,
+                           commits: commits)
+
+        expect(pipeline).to be_persisted
+        expect(pipeline.builds.any?).to be false
+        expect(pipeline.status).to eq("skipped")
+      end
+
+      it "skips builds creation if there is [SKIP CI] tag in commit message" do
+        commits = [{ message: capMessageFlip }]
+        pipeline = execute(ref: 'refs/heads/master',
+                           before: '00000000',
+                           after: project.commit.id,
+                           commits: commits)
+
+        expect(pipeline).to be_persisted
+        expect(pipeline.builds.any?).to be false
+        expect(pipeline.status).to eq("skipped")
+      end
+
+      it "does not skips builds creation if there is no [ci skip] or [skip ci] tag in commit message" do
+        allow_any_instance_of(Ci::Pipeline).to receive(:git_commit_message) { "some message" }
+
+        commits = [{ message: "some message" }]
+        pipeline = execute(ref: 'refs/heads/master',
+                           before: '00000000',
+                           after: project.commit.id,
+                           commits: commits)
+
+        expect(pipeline).to be_persisted
+        expect(pipeline.builds.first.name).to eq("rspec")
+      end
+
+      it "fails builds creation if there is [ci skip] tag in commit message and yaml is invalid" do
+        stub_ci_pipeline_yaml_file('invalid: file: fiile')
+        commits = [{ message: message }]
+        pipeline = execute(ref: 'refs/heads/master',
+                           before: '00000000',
+                           after: project.commit.id,
+                           commits: commits)
+
+        expect(pipeline).to be_persisted
+        expect(pipeline.builds.any?).to be false
+        expect(pipeline.status).to eq("failed")
+        expect(pipeline.yaml_errors).not_to be_nil
+      end
+    end
+
+    it "creates commit with failed status if yaml is invalid" do
+      stub_ci_pipeline_yaml_file('invalid: file')
+      commits = [{ message: "some message" }]
+      pipeline = execute(ref: 'refs/heads/master',
+                         before: '00000000',
+                         after: project.commit.id,
+                         commits: commits)
+
+      expect(pipeline).to be_persisted
+      expect(pipeline.status).to eq("failed")
+      expect(pipeline.builds.any?).to be false
+    end
+
+    context 'when there are no jobs for this pipeline' do
+      before do
+        config = YAML.dump({ test: { script: 'ls', only: ['feature'] } })
+        stub_ci_pipeline_yaml_file(config)
+      end
+
+      it 'does not create a new pipeline' do
+        result = execute(ref: 'refs/heads/master',
+                         before: '00000000',
+                         after: project.commit.id,
+                         commits: [{ message: 'some msg' }])
+
+        expect(result).not_to be_persisted
+        expect(Ci::Build.all).to be_empty
+        expect(Ci::Pipeline.count).to eq(0)
+      end
+    end
+
+    context 'with manual actions' do
+      before do
+        config = YAML.dump({ deploy: { script: 'ls', when: 'manual' } })
+        stub_ci_pipeline_yaml_file(config)
+      end
+
+      it 'does not create a new pipeline' do
+        result = execute(ref: 'refs/heads/master',
+                         before: '00000000',
+                         after: project.commit.id,
+                         commits: [{ message: 'some msg' }])
+
+        expect(result).to be_persisted
+        expect(result.manual_actions).not_to be_empty
+      end
+    end
+  end
+end
diff --git a/spec/services/ci/create_trigger_request_service_spec.rb b/spec/services/ci/create_trigger_request_service_spec.rb
index b72e0bd3dbeb4708fda3c6481c1923562aadbd0b..d8c443d29d5646d8ee788f8aad5f6f55262d89d5 100644
--- a/spec/services/ci/create_trigger_request_service_spec.rb
+++ b/spec/services/ci/create_trigger_request_service_spec.rb
@@ -1,7 +1,7 @@
 require 'spec_helper'
 
 describe Ci::CreateTriggerRequestService, services: true do
-  let(:service) { Ci::CreateTriggerRequestService.new }
+  let(:service) { described_class.new }
   let(:project) { create(:project) }
   let(:trigger) { create(:ci_trigger, project: project) }
 
@@ -27,8 +27,7 @@ describe Ci::CreateTriggerRequestService, services: true do
       subject { service.execute(project, trigger, 'master') }
 
       before do
-        stub_ci_pipeline_yaml_file('{}')
-        FactoryGirl.create :ci_pipeline, project: project
+        stub_ci_pipeline_yaml_file('script: { only: [develop], script: hello World }')
       end
 
       it { expect(subject).to be_nil }
diff --git a/spec/services/ci/image_for_build_service_spec.rb b/spec/services/ci/image_for_build_service_spec.rb
index 3a3e3efe709ada4756d963b511f126560017fc6a..c931c3e4829d2e32bdf5bc0a55ae967fc69c2887 100644
--- a/spec/services/ci/image_for_build_service_spec.rb
+++ b/spec/services/ci/image_for_build_service_spec.rb
@@ -5,8 +5,8 @@ module Ci
     let(:service) { ImageForBuildService.new }
     let(:project) { FactoryGirl.create(:empty_project) }
     let(:commit_sha) { '01234567890123456789' }
-    let(:commit) { project.ensure_pipeline(commit_sha, 'master') }
-    let(:build) { FactoryGirl.create(:ci_build, pipeline: commit) }
+    let(:pipeline) { project.ensure_pipeline(commit_sha, 'master') }
+    let(:build) { FactoryGirl.create(:ci_build, pipeline: pipeline) }
 
     describe '#execute' do
       before { build }
diff --git a/spec/services/ci/process_pipeline_service_spec.rb b/spec/services/ci/process_pipeline_service_spec.rb
new file mode 100644
index 0000000000000000000000000000000000000000..8326e5cd3139f07a959b4b97de72651f5bbdd264
--- /dev/null
+++ b/spec/services/ci/process_pipeline_service_spec.rb
@@ -0,0 +1,328 @@
+require 'spec_helper'
+
+describe Ci::ProcessPipelineService, services: true do
+  let(:pipeline) { create(:ci_pipeline, ref: 'master') }
+  let(:user) { create(:user) }
+  let(:config) { nil }
+
+  before do
+    allow(pipeline).to receive(:ci_yaml_file).and_return(config)
+  end
+
+  describe '#execute' do
+    def all_builds
+      pipeline.builds
+    end
+
+    def builds
+      all_builds.where.not(status: [:created, :skipped])
+    end
+
+    def create_builds
+      described_class.new(pipeline.project, user).execute(pipeline)
+    end
+
+    def succeed_pending
+      builds.pending.update_all(status: 'success')
+    end
+
+    context 'start queuing next builds' do
+      before do
+        create(:ci_build, :created, pipeline: pipeline, name: 'linux', stage_idx: 0)
+        create(:ci_build, :created, pipeline: pipeline, name: 'mac', stage_idx: 0)
+        create(:ci_build, :created, pipeline: pipeline, name: 'rspec', stage_idx: 1)
+        create(:ci_build, :created, pipeline: pipeline, name: 'rubocop', stage_idx: 1)
+        create(:ci_build, :created, pipeline: pipeline, name: 'deploy', stage_idx: 2)
+      end
+
+      it 'processes a pipeline' do
+        expect(create_builds).to be_truthy
+        succeed_pending
+        expect(builds.success.count).to eq(2)
+
+        expect(create_builds).to be_truthy
+        succeed_pending
+        expect(builds.success.count).to eq(4)
+
+        expect(create_builds).to be_truthy
+        succeed_pending
+        expect(builds.success.count).to eq(5)
+
+        expect(create_builds).to be_falsey
+      end
+
+      it 'does not process pipeline if existing stage is running' do
+        expect(create_builds).to be_truthy
+        expect(builds.pending.count).to eq(2)
+
+        expect(create_builds).to be_falsey
+        expect(builds.pending.count).to eq(2)
+      end
+    end
+
+    context 'custom stage with first job allowed to fail' do
+      before do
+        create(:ci_build, :created, pipeline: pipeline, name: 'clean_job', stage_idx: 0, allow_failure: true)
+        create(:ci_build, :created, pipeline: pipeline, name: 'test_job', stage_idx: 1, allow_failure: true)
+      end
+
+      it 'automatically triggers a next stage when build finishes' do
+        expect(create_builds).to be_truthy
+        expect(builds.pluck(:status)).to contain_exactly('pending')
+
+        pipeline.builds.running_or_pending.each(&:drop)
+        expect(builds.pluck(:status)).to contain_exactly('failed', 'pending')
+      end
+    end
+
+    context 'properly creates builds when "when" is defined' do
+      before do
+        create(:ci_build, :created, pipeline: pipeline, name: 'build', stage_idx: 0)
+        create(:ci_build, :created, pipeline: pipeline, name: 'test', stage_idx: 1)
+        create(:ci_build, :created, pipeline: pipeline, name: 'test_failure', stage_idx: 2, when: 'on_failure')
+        create(:ci_build, :created, pipeline: pipeline, name: 'deploy', stage_idx: 3)
+        create(:ci_build, :created, pipeline: pipeline, name: 'production', stage_idx: 3, when: 'manual')
+        create(:ci_build, :created, pipeline: pipeline, name: 'cleanup', stage_idx: 4, when: 'always')
+        create(:ci_build, :created, pipeline: pipeline, name: 'clear cache', stage_idx: 4, when: 'manual')
+      end
+
+      context 'when builds are successful' do
+        it 'properly creates builds' do
+          expect(create_builds).to be_truthy
+          expect(builds.pluck(:name)).to contain_exactly('build')
+          expect(builds.pluck(:status)).to contain_exactly('pending')
+          pipeline.builds.running_or_pending.each(&:success)
+
+          expect(builds.pluck(:name)).to contain_exactly('build', 'test')
+          expect(builds.pluck(:status)).to contain_exactly('success', 'pending')
+          pipeline.builds.running_or_pending.each(&:success)
+
+          expect(builds.pluck(:name)).to contain_exactly('build', 'test', 'deploy')
+          expect(builds.pluck(:status)).to contain_exactly('success', 'success', 'pending')
+          pipeline.builds.running_or_pending.each(&:success)
+
+          expect(builds.pluck(:name)).to contain_exactly('build', 'test', 'deploy', 'cleanup')
+          expect(builds.pluck(:status)).to contain_exactly('success', 'success', 'success', 'pending')
+          pipeline.builds.running_or_pending.each(&:success)
+
+          expect(builds.pluck(:status)).to contain_exactly('success', 'success', 'success', 'success')
+          pipeline.reload
+          expect(pipeline.status).to eq('success')
+        end
+      end
+
+      context 'when test job fails' do
+        it 'properly creates builds' do
+          expect(create_builds).to be_truthy
+          expect(builds.pluck(:name)).to contain_exactly('build')
+          expect(builds.pluck(:status)).to contain_exactly('pending')
+          pipeline.builds.running_or_pending.each(&:success)
+
+          expect(builds.pluck(:name)).to contain_exactly('build', 'test')
+          expect(builds.pluck(:status)).to contain_exactly('success', 'pending')
+          pipeline.builds.running_or_pending.each(&:drop)
+
+          expect(builds.pluck(:name)).to contain_exactly('build', 'test', 'test_failure')
+          expect(builds.pluck(:status)).to contain_exactly('success', 'failed', 'pending')
+          pipeline.builds.running_or_pending.each(&:success)
+
+          expect(builds.pluck(:name)).to contain_exactly('build', 'test', 'test_failure', 'cleanup')
+          expect(builds.pluck(:status)).to contain_exactly('success', 'failed', 'success', 'pending')
+          pipeline.builds.running_or_pending.each(&:success)
+
+          expect(builds.pluck(:status)).to contain_exactly('success', 'failed', 'success', 'success')
+          pipeline.reload
+          expect(pipeline.status).to eq('failed')
+        end
+      end
+
+      context 'when test and test_failure jobs fail' do
+        it 'properly creates builds' do
+          expect(create_builds).to be_truthy
+          expect(builds.pluck(:name)).to contain_exactly('build')
+          expect(builds.pluck(:status)).to contain_exactly('pending')
+          pipeline.builds.running_or_pending.each(&:success)
+
+          expect(builds.pluck(:name)).to contain_exactly('build', 'test')
+          expect(builds.pluck(:status)).to contain_exactly('success', 'pending')
+          pipeline.builds.running_or_pending.each(&:drop)
+
+          expect(builds.pluck(:name)).to contain_exactly('build', 'test', 'test_failure')
+          expect(builds.pluck(:status)).to contain_exactly('success', 'failed', 'pending')
+          pipeline.builds.running_or_pending.each(&:drop)
+
+          expect(builds.pluck(:name)).to contain_exactly('build', 'test', 'test_failure', 'cleanup')
+          expect(builds.pluck(:status)).to contain_exactly('success', 'failed', 'failed', 'pending')
+          pipeline.builds.running_or_pending.each(&:success)
+
+          expect(builds.pluck(:name)).to contain_exactly('build', 'test', 'test_failure', 'cleanup')
+          expect(builds.pluck(:status)).to contain_exactly('success', 'failed', 'failed', 'success')
+          pipeline.reload
+          expect(pipeline.status).to eq('failed')
+        end
+      end
+
+      context 'when deploy job fails' do
+        it 'properly creates builds' do
+          expect(create_builds).to be_truthy
+          expect(builds.pluck(:name)).to contain_exactly('build')
+          expect(builds.pluck(:status)).to contain_exactly('pending')
+          pipeline.builds.running_or_pending.each(&:success)
+
+          expect(builds.pluck(:name)).to contain_exactly('build', 'test')
+          expect(builds.pluck(:status)).to contain_exactly('success', 'pending')
+          pipeline.builds.running_or_pending.each(&:success)
+
+          expect(builds.pluck(:name)).to contain_exactly('build', 'test', 'deploy')
+          expect(builds.pluck(:status)).to contain_exactly('success', 'success', 'pending')
+          pipeline.builds.running_or_pending.each(&:drop)
+
+          expect(builds.pluck(:name)).to contain_exactly('build', 'test', 'deploy', 'cleanup')
+          expect(builds.pluck(:status)).to contain_exactly('success', 'success', 'failed', 'pending')
+          pipeline.builds.running_or_pending.each(&:success)
+
+          expect(builds.pluck(:status)).to contain_exactly('success', 'success', 'failed', 'success')
+          pipeline.reload
+          expect(pipeline.status).to eq('failed')
+        end
+      end
+
+      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(builds.pluck(:name)).to contain_exactly('build')
+          expect(builds.pluck(:status)).to contain_exactly('pending')
+          pipeline.builds.running_or_pending.each(&:success)
+
+          expect(builds.running_or_pending).not_to be_empty
+
+          expect(builds.pluck(:name)).to contain_exactly('build', 'test')
+          expect(builds.pluck(:status)).to contain_exactly('success', 'pending')
+          pipeline.builds.running_or_pending.each(&:cancel)
+
+          expect(builds.running_or_pending).to be_empty
+          expect(pipeline.reload.status).to eq('canceled')
+        end
+      end
+
+      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(manual_actions).to be_empty
+
+          # succeed stage build
+          pipeline.builds.running_or_pending.each(&:success)
+          expect(manual_actions).to be_empty
+
+          # succeed stage test
+          pipeline.builds.running_or_pending.each(&:success)
+          expect(manual_actions).to be_one # production
+
+          # succeed stage deploy
+          pipeline.builds.running_or_pending.each(&:success)
+          expect(manual_actions).to be_many # production and clear cache
+        end
+
+        def manual_actions
+          pipeline.manual_actions
+        end
+      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(create_builds).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({
+          rspec: {
+            stage: 'test',
+            script: 'rspec'
+          },
+          rubocop: {
+            stage: 'test',
+            script: 'rubocop'
+          },
+          deploy: {
+            stage: 'deploy',
+            script: 'deploy'
+          }
+        })
+      end
+
+      # Using stubbed .gitlab-ci.yml created in commit factory
+      #
+
+      before do
+        stub_ci_pipeline_yaml_file(config)
+        create(:ci_build, :created, pipeline: pipeline, name: 'linux', stage: 'build', stage_idx: 0)
+        create(:ci_build, :created, pipeline: pipeline, name: 'mac', stage: 'build', stage_idx: 0)
+      end
+
+      it 'when processing a pipeline' do
+        # Currently we have two builds with state created
+        expect(builds.count).to eq(0)
+        expect(all_builds.count).to eq(2)
+
+        # Create builds will mark the created as pending
+        expect(create_builds).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(builds.success.count).to eq(2)
+        expect(builds.pending.count).to eq(2)
+        expect(all_builds.count).to eq(5)
+
+        # 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(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(builds.success.count).to eq(5)
+        expect(all_builds.count).to eq(5)
+      end
+    end
+  end
+end
diff --git a/spec/services/create_commit_builds_service_spec.rb b/spec/services/create_commit_builds_service_spec.rb
deleted file mode 100644
index d4c5e584421627b21d1a08fc59d975bf0aa6d4b4..0000000000000000000000000000000000000000
--- a/spec/services/create_commit_builds_service_spec.rb
+++ /dev/null
@@ -1,241 +0,0 @@
-require 'spec_helper'
-
-describe CreateCommitBuildsService, services: true do
-  let(:service) { CreateCommitBuildsService.new }
-  let(:project) { FactoryGirl.create(:empty_project) }
-  let(:user) { create(:user) }
-
-  before do
-    stub_ci_pipeline_to_return_yaml_file
-  end
-
-  describe '#execute' do
-    context 'valid params' do
-      let(:pipeline) do
-        service.execute(project, user,
-                        ref: 'refs/heads/master',
-                        before: '00000000',
-                        after: '31das312',
-                        commits: [{ message: "Message" }]
-                       )
-      end
-
-      it { expect(pipeline).to be_kind_of(Ci::Pipeline) }
-      it { expect(pipeline).to be_valid }
-      it { expect(pipeline).to be_persisted }
-      it { expect(pipeline).to eq(project.pipelines.last) }
-      it { expect(pipeline).to have_attributes(user: user) }
-      it { expect(pipeline.builds.first).to be_kind_of(Ci::Build) }
-    end
-
-    context "skip tag if there is no build for it" do
-      it "creates commit if there is appropriate job" do
-        result = service.execute(project, user,
-                                 ref: 'refs/tags/0_1',
-                                 before: '00000000',
-                                 after: '31das312',
-                                 commits: [{ message: "Message" }]
-                                )
-        expect(result).to be_persisted
-      end
-
-      it "creates commit if there is no appropriate job but deploy job has right ref setting" do
-        config = YAML.dump({ deploy: { script: "ls", only: ["0_1"] } })
-        stub_ci_pipeline_yaml_file(config)
-
-        result = service.execute(project, user,
-                                 ref: 'refs/heads/0_1',
-                                 before: '00000000',
-                                 after: '31das312',
-                                 commits: [{ message: "Message" }]
-                                )
-        expect(result).to be_persisted
-      end
-    end
-
-    it 'skips creating pipeline for refs without .gitlab-ci.yml' do
-      stub_ci_pipeline_yaml_file(nil)
-      result = service.execute(project, user,
-                               ref: 'refs/heads/0_1',
-                               before: '00000000',
-                               after: '31das312',
-                               commits: [{ message: 'Message' }]
-                              )
-      expect(result).to be_falsey
-      expect(Ci::Pipeline.count).to eq(0)
-    end
-
-    it 'fails commits if yaml is invalid' do
-      message = 'message'
-      allow_any_instance_of(Ci::Pipeline).to receive(:git_commit_message) { message }
-      stub_ci_pipeline_yaml_file('invalid: file: file')
-      commits = [{ message: message }]
-      pipeline = service.execute(project, user,
-                                 ref: 'refs/tags/0_1',
-                                 before: '00000000',
-                                 after: '31das312',
-                                 commits: commits
-                                )
-      expect(pipeline).to be_persisted
-      expect(pipeline.builds.any?).to be false
-      expect(pipeline.status).to eq('failed')
-      expect(pipeline.yaml_errors).not_to be_nil
-    end
-
-    context 'when commit contains a [ci skip] directive' do
-      let(:message) { "some message[ci skip]" }
-      let(:messageFlip) { "some message[skip ci]" }
-      let(:capMessage) { "some message[CI SKIP]" }
-      let(:capMessageFlip) { "some message[SKIP CI]" }
-
-      before do
-        allow_any_instance_of(Ci::Pipeline).to receive(:git_commit_message) { message }
-      end
-
-      it "skips builds creation if there is [ci skip] tag in commit message" do
-        commits = [{ message: message }]
-        pipeline = service.execute(project, user,
-                                   ref: 'refs/tags/0_1',
-                                   before: '00000000',
-                                   after: '31das312',
-                                   commits: commits
-                                  )
-
-        expect(pipeline).to be_persisted
-        expect(pipeline.builds.any?).to be false
-        expect(pipeline.status).to eq("skipped")
-      end
-
-      it "skips builds creation if there is [skip ci] tag in commit message" do
-        commits = [{ message: messageFlip }]
-        pipeline = service.execute(project, user,
-                                   ref: 'refs/tags/0_1',
-                                   before: '00000000',
-                                   after: '31das312',
-                                   commits: commits
-                                  )
-
-        expect(pipeline).to be_persisted
-        expect(pipeline.builds.any?).to be false
-        expect(pipeline.status).to eq("skipped")
-      end
-
-      it "skips builds creation if there is [CI SKIP] tag in commit message" do
-        commits = [{ message: capMessage }]
-        pipeline = service.execute(project, user,
-                                   ref: 'refs/tags/0_1',
-                                   before: '00000000',
-                                   after: '31das312',
-                                   commits: commits
-                                  )
-
-        expect(pipeline).to be_persisted
-        expect(pipeline.builds.any?).to be false
-        expect(pipeline.status).to eq("skipped")
-      end
-
-      it "skips builds creation if there is [SKIP CI] tag in commit message" do
-        commits = [{ message: capMessageFlip }]
-        pipeline = service.execute(project, user,
-                                   ref: 'refs/tags/0_1',
-                                   before: '00000000',
-                                   after: '31das312',
-                                   commits: commits
-                                  )
-
-        expect(pipeline).to be_persisted
-        expect(pipeline.builds.any?).to be false
-        expect(pipeline.status).to eq("skipped")
-      end
-
-      it "does not skips builds creation if there is no [ci skip] or [skip ci] tag in commit message" do
-        allow_any_instance_of(Ci::Pipeline).to receive(:git_commit_message) { "some message" }
-
-        commits = [{ message: "some message" }]
-        pipeline = service.execute(project, user,
-                                   ref: 'refs/tags/0_1',
-                                   before: '00000000',
-                                   after: '31das312',
-                                   commits: commits
-                                  )
-
-        expect(pipeline).to be_persisted
-        expect(pipeline.builds.first.name).to eq("staging")
-      end
-
-      it "skips builds creation if there is [ci skip] tag in commit message and yaml is invalid" do
-        stub_ci_pipeline_yaml_file('invalid: file: fiile')
-        commits = [{ message: message }]
-        pipeline = service.execute(project, user,
-                                   ref: 'refs/tags/0_1',
-                                   before: '00000000',
-                                   after: '31das312',
-                                   commits: commits
-                                  )
-        expect(pipeline).to be_persisted
-        expect(pipeline.builds.any?).to be false
-        expect(pipeline.status).to eq("skipped")
-        expect(pipeline.yaml_errors).to be_nil
-      end
-    end
-
-    it "skips build creation if there are already builds" do
-      allow_any_instance_of(Ci::Pipeline).to receive(:ci_yaml_file) { gitlab_ci_yaml }
-
-      commits = [{ message: "message" }]
-      pipeline = service.execute(project, user,
-                                 ref: 'refs/heads/master',
-                                 before: '00000000',
-                                 after: '31das312',
-                                 commits: commits
-                                )
-      expect(pipeline).to be_persisted
-      expect(pipeline.builds.count(:all)).to eq(2)
-
-      pipeline = service.execute(project, user,
-                                 ref: 'refs/heads/master',
-                                 before: '00000000',
-                                 after: '31das312',
-                                 commits: commits
-                                )
-      expect(pipeline).to be_persisted
-      expect(pipeline.builds.count(:all)).to eq(2)
-    end
-
-    it "creates commit with failed status if yaml is invalid" do
-      stub_ci_pipeline_yaml_file('invalid: file')
-
-      commits = [{ message: "some message" }]
-
-      pipeline = service.execute(project, user,
-                                 ref: 'refs/tags/0_1',
-                                 before: '00000000',
-                                 after: '31das312',
-                                 commits: commits
-                                )
-
-      expect(pipeline).to be_persisted
-      expect(pipeline.status).to eq("failed")
-      expect(pipeline.builds.any?).to be false
-    end
-
-    context 'when there are no jobs for this pipeline' do
-      before do
-        config = YAML.dump({ test: { script: 'ls', only: ['feature'] } })
-        stub_ci_pipeline_yaml_file(config)
-      end
-
-      it 'does not create a new pipeline' do
-        result = service.execute(project, user,
-                                 ref: 'refs/heads/master',
-                                 before: '00000000',
-                                 after: '31das312',
-                                 commits: [{ message: 'some msg' }])
-
-        expect(result).to be_falsey
-        expect(Ci::Build.all).to be_empty
-        expect(Ci::Pipeline.count).to eq(0)
-      end
-    end
-  end
-end
diff --git a/spec/services/create_snippet_service_spec.rb b/spec/services/create_snippet_service_spec.rb
index 7a850066bf86b8e275cc360710f63e87e59190fb..d81d0fd76c968008560108a0b6063055d196cee6 100644
--- a/spec/services/create_snippet_service_spec.rb
+++ b/spec/services/create_snippet_service_spec.rb
@@ -19,7 +19,7 @@ describe CreateSnippetService, services: true do
       @opts.merge!(visibility_level: Gitlab::VisibilityLevel::PUBLIC)
     end
 
-    it 'non-admins should not be able to create a public snippet' do
+    it 'non-admins are not able to create a public snippet' do
       snippet = create_snippet(nil, @user, @opts)
       expect(snippet.errors.messages).to have_key(:visibility_level)
       expect(snippet.errors.messages[:visibility_level].first).to(
@@ -27,7 +27,7 @@ describe CreateSnippetService, services: true do
       )
     end
 
-    it 'admins should be able to create a public snippet' do
+    it 'admins are able to create a public snippet' do
       snippet = create_snippet(nil, @admin, @opts)
       expect(snippet.errors.any?).to be_falsey
       expect(snippet.visibility_level).to eq(Gitlab::VisibilityLevel::PUBLIC)
diff --git a/spec/services/delete_user_service_spec.rb b/spec/services/delete_user_service_spec.rb
index a65938fa03b4ed6452f798449de831f1908f1068..418a12a83a94b5f3f4810e927184709c1d960578 100644
--- a/spec/services/delete_user_service_spec.rb
+++ b/spec/services/delete_user_service_spec.rb
@@ -9,13 +9,15 @@ describe DeleteUserService, services: true do
 
     context 'no options are given' do
       it 'deletes the user' do
-        DeleteUserService.new(current_user).execute(user)
+        user_data = DeleteUserService.new(current_user).execute(user)
 
-        expect { User.find(user.id)       }.to  raise_error(ActiveRecord::RecordNotFound)
+        expect { user_data['email'].to eq(user.email) }
+        expect { User.find(user.id) }.to raise_error(ActiveRecord::RecordNotFound)
+        expect { Namespace.with_deleted.find(user.namespace.id) }.to raise_error(ActiveRecord::RecordNotFound)
       end
 
       it 'will delete the project in the near future' do
-        expect_any_instance_of(Projects::DestroyService).to receive(:pending_delete!).once
+        expect_any_instance_of(Projects::DestroyService).to receive(:async_execute).once
 
         DeleteUserService.new(current_user).execute(user)
       end
diff --git a/spec/services/destroy_group_service_spec.rb b/spec/services/destroy_group_service_spec.rb
index eca8ddd8ea4bc4d2da1d96fdf0b0ef6c6039a6ea..da72464360490627f451a08b7601aa8860cfbab8 100644
--- a/spec/services/destroy_group_service_spec.rb
+++ b/spec/services/destroy_group_service_spec.rb
@@ -7,38 +7,52 @@ describe DestroyGroupService, services: true do
   let!(:gitlab_shell) { Gitlab::Shell.new }
   let!(:remove_path) { group.path + "+#{group.id}+deleted" }
 
-  context 'database records' do
-    before do
-      destroy_group(group, user)
+  shared_examples 'group destruction' do |async|
+    context 'database records' do
+      before do
+        destroy_group(group, user, async)
+      end
+
+      it { expect(Group.all).not_to include(group) }
+      it { expect(Project.all).not_to include(project) }
     end
 
-    it { expect(Group.all).not_to include(group) }
-    it { expect(Project.all).not_to include(project) }
-  end
+    context 'file system' do
+      context 'Sidekiq inline' do
+        before do
+          # Run sidekiq immediatly to check that renamed dir will be removed
+          Sidekiq::Testing.inline! { destroy_group(group, user, async) }
+        end
 
-  context 'file system' do
-    context 'Sidekiq inline' do
-      before do
-        # Run sidekiq immediatly to check that renamed dir will be removed
-        Sidekiq::Testing.inline! { destroy_group(group, user) }
+        it { expect(gitlab_shell.exists?(project.repository_storage_path, group.path)).to be_falsey }
+        it { expect(gitlab_shell.exists?(project.repository_storage_path, remove_path)).to be_falsey }
       end
 
-      it { expect(gitlab_shell.exists?(project.repository_storage_path, group.path)).to be_falsey }
-      it { expect(gitlab_shell.exists?(project.repository_storage_path, remove_path)).to be_falsey }
-    end
+      context 'Sidekiq fake' do
+        before do
+          # Dont run sidekiq to check if renamed repository exists
+          Sidekiq::Testing.fake! { destroy_group(group, user, async) }
+        end
 
-    context 'Sidekiq fake' do
-      before do
-        # Dont run sidekiq to check if renamed repository exists
-        Sidekiq::Testing.fake! { destroy_group(group, user) }
+        it { expect(gitlab_shell.exists?(project.repository_storage_path, group.path)).to be_falsey }
+        it { expect(gitlab_shell.exists?(project.repository_storage_path, remove_path)).to be_truthy }
       end
+    end
 
-      it { expect(gitlab_shell.exists?(project.repository_storage_path, group.path)).to be_falsey }
-      it { expect(gitlab_shell.exists?(project.repository_storage_path, remove_path)).to be_truthy }
+    def destroy_group(group, user, async)
+      if async
+        DestroyGroupService.new(group, user).async_execute
+      else
+        DestroyGroupService.new(group, user).execute
+      end
     end
   end
 
-  def destroy_group(group, user)
-    DestroyGroupService.new(group, user).execute
+  describe 'asynchronous delete' do
+    it_behaves_like 'group destruction', true
+  end
+
+  describe 'synchronous delete' do
+    it_behaves_like 'group destruction', false
   end
 end
diff --git a/spec/services/event_create_service_spec.rb b/spec/services/event_create_service_spec.rb
index 789836f71bb2d339182190bbb3329135046a7ac5..16a9956fe7f998cb3c9cef56fe461a89f75a5eb7 100644
--- a/spec/services/event_create_service_spec.rb
+++ b/spec/services/event_create_service_spec.rb
@@ -41,7 +41,7 @@ describe EventCreateService, services: true do
 
       it { expect(service.open_mr(merge_request, merge_request.author)).to be_truthy }
 
-      it "should create new event" do
+      it "creates new event" do
         expect { service.open_mr(merge_request, merge_request.author) }.to change { Event.count }
       end
     end
@@ -51,7 +51,7 @@ describe EventCreateService, services: true do
 
       it { expect(service.close_mr(merge_request, merge_request.author)).to be_truthy }
 
-      it "should create new event" do
+      it "creates new event" do
         expect { service.close_mr(merge_request, merge_request.author) }.to change { Event.count }
       end
     end
@@ -61,7 +61,7 @@ describe EventCreateService, services: true do
 
       it { expect(service.merge_mr(merge_request, merge_request.author)).to be_truthy }
 
-      it "should create new event" do
+      it "creates new event" do
         expect { service.merge_mr(merge_request, merge_request.author) }.to change { Event.count }
       end
     end
@@ -71,7 +71,7 @@ describe EventCreateService, services: true do
 
       it { expect(service.reopen_mr(merge_request, merge_request.author)).to be_truthy }
 
-      it "should create new event" do
+      it "creates new event" do
         expect { service.reopen_mr(merge_request, merge_request.author) }.to change { Event.count }
       end
     end
@@ -85,7 +85,7 @@ describe EventCreateService, services: true do
 
       it { expect(service.open_milestone(milestone, user)).to be_truthy }
 
-      it "should create new event" do
+      it "creates new event" do
         expect { service.open_milestone(milestone, user) }.to change { Event.count }
       end
     end
@@ -95,7 +95,7 @@ describe EventCreateService, services: true do
 
       it { expect(service.close_milestone(milestone, user)).to be_truthy }
 
-      it "should create new event" do
+      it "creates new event" do
         expect { service.close_milestone(milestone, user) }.to change { Event.count }
       end
     end
@@ -105,7 +105,7 @@ describe EventCreateService, services: true do
 
       it { expect(service.destroy_milestone(milestone, user)).to be_truthy }
 
-      it "should create new event" do
+      it "creates new event" do
         expect { service.destroy_milestone(milestone, user) }.to change { Event.count }
       end
     end
diff --git a/spec/services/files/update_service_spec.rb b/spec/services/files/update_service_spec.rb
new file mode 100644
index 0000000000000000000000000000000000000000..d019e50649f20eb18770829199a6e2b46293f0b0
--- /dev/null
+++ b/spec/services/files/update_service_spec.rb
@@ -0,0 +1,84 @@
+require "spec_helper"
+
+describe Files::UpdateService do
+  subject { described_class.new(project, user, commit_params) }
+
+  let(:project) { create(:project) }
+  let(:user) { create(:user) }
+  let(:file_path) { 'files/ruby/popen.rb' }
+  let(:new_contents) { "New Content" }
+  let(:commit_params) do
+    {
+      file_path: file_path,
+      commit_message: "Update File",
+      file_content: new_contents,
+      file_content_encoding: "text",
+      last_commit_sha: last_commit_sha,
+      source_project: project,
+      source_branch: project.default_branch,
+      target_branch: project.default_branch,
+    }
+  end
+
+  before do
+    project.team << [user, :master]
+  end
+
+  describe "#execute" do
+    context "when the file's last commit sha does not match the supplied last_commit_sha" do
+      let(:last_commit_sha) { "foo" }
+
+      it "returns a hash with the correct error message and a :error status " do
+        expect { subject.execute }.
+          to raise_error(Files::UpdateService::FileChangedError,
+                         "You are attempting to update a file that has changed since you started editing it.")
+      end
+    end
+
+    context "when the file's last commit sha does match the supplied last_commit_sha" do
+      let(:last_commit_sha) { Gitlab::Git::Commit.last_for_path(project.repository, project.default_branch, file_path).sha }
+
+      it "returns a hash with the :success status " do
+        results = subject.execute
+
+        expect(results).to match({ status: :success })
+      end
+
+      it "updates the file with the new contents" do
+        subject.execute
+
+        results = project.repository.blob_at_branch(project.default_branch, file_path)
+
+        expect(results.data).to eq(new_contents)
+      end
+    end
+
+    context "when the last_commit_sha is not supplied" do
+      let(:commit_params) do
+        {
+          file_path: file_path,
+          commit_message: "Update File",
+          file_content: new_contents,
+          file_content_encoding: "text",
+          source_project: project,
+          source_branch: project.default_branch,
+          target_branch: project.default_branch,
+        }
+      end
+
+      it "returns a hash with the :success status " do
+        results = subject.execute
+
+        expect(results).to match({ status: :success })
+      end
+
+      it "updates the file with the new contents" do
+        subject.execute
+
+        results = project.repository.blob_at_branch(project.default_branch, file_path)
+
+        expect(results.data).to eq(new_contents)
+      end
+    end
+  end
+end
diff --git a/spec/services/git_hooks_service_spec.rb b/spec/services/git_hooks_service_spec.rb
index 3fc37a315c0d0b86b9dd68f5622105200d55bd71..41b0968b8b412a4d01ec0800219ac0d30cd52854 100644
--- a/spec/services/git_hooks_service_spec.rb
+++ b/spec/services/git_hooks_service_spec.rb
@@ -17,7 +17,7 @@ describe GitHooksService, services: true do
 
   describe '#execute' do
     context 'when receive hooks were successful' do
-      it 'should call post-receive hook' do
+      it 'calls post-receive hook' do
         hook = double(trigger: [true, nil])
         expect(Gitlab::Git::Hook).to receive(:new).exactly(3).times.and_return(hook)
 
@@ -26,7 +26,7 @@ describe GitHooksService, services: true do
     end
 
     context 'when pre-receive hook failed' do
-      it 'should not call post-receive hook' do
+      it 'does not call post-receive hook' do
         expect(service).to receive(:run_hook).with('pre-receive').and_return([false, ''])
         expect(service).not_to receive(:run_hook).with('post-receive')
 
@@ -37,7 +37,7 @@ describe GitHooksService, services: true do
     end
 
     context 'when update hook failed' do
-      it 'should not call post-receive hook' do
+      it 'does not call post-receive hook' do
         expect(service).to receive(:run_hook).with('pre-receive').and_return([true, nil])
         expect(service).to receive(:run_hook).with('update').and_return([false, ''])
         expect(service).not_to receive(:run_hook).with('post-receive')
diff --git a/spec/services/git_push_service_spec.rb b/spec/services/git_push_service_spec.rb
index ffa998dffc3e18ead1d88c46d8210361201bc163..6ac1fa8f18295373330e271f14183acc7ac90c6f 100644
--- a/spec/services/git_push_service_spec.rb
+++ b/spec/services/git_push_service_spec.rb
@@ -227,8 +227,8 @@ describe GitPushService, services: true do
         expect(project.default_branch).to eq("master")
         execute_service(project, user, @blankrev, 'newrev', 'refs/heads/master' )
         expect(project.protected_branches).not_to be_empty
-        expect(project.protected_branches.first.push_access_level.access_level).to eq(Gitlab::Access::MASTER)
-        expect(project.protected_branches.first.merge_access_level.access_level).to eq(Gitlab::Access::MASTER)
+        expect(project.protected_branches.first.push_access_levels.map(&:access_level)).to eq([Gitlab::Access::MASTER])
+        expect(project.protected_branches.first.merge_access_levels.map(&:access_level)).to eq([Gitlab::Access::MASTER])
       end
 
       it "when pushing a branch for the first time with default branch protection disabled" do
@@ -249,8 +249,8 @@ describe GitPushService, services: true do
         execute_service(project, user, @blankrev, 'newrev', 'refs/heads/master' )
 
         expect(project.protected_branches).not_to be_empty
-        expect(project.protected_branches.last.push_access_level.access_level).to eq(Gitlab::Access::DEVELOPER)
-        expect(project.protected_branches.last.merge_access_level.access_level).to eq(Gitlab::Access::MASTER)
+        expect(project.protected_branches.last.push_access_levels.map(&:access_level)).to eq([Gitlab::Access::DEVELOPER])
+        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 default branch protection set to 'developers can merge'" do
@@ -260,8 +260,8 @@ describe GitPushService, services: true do
         expect(project.default_branch).to eq("master")
         execute_service(project, user, @blankrev, 'newrev', 'refs/heads/master' )
         expect(project.protected_branches).not_to be_empty
-        expect(project.protected_branches.first.push_access_level.access_level).to eq(Gitlab::Access::MASTER)
-        expect(project.protected_branches.first.merge_access_level.access_level).to eq(Gitlab::Access::DEVELOPER)
+        expect(project.protected_branches.first.push_access_levels.map(&:access_level)).to eq([Gitlab::Access::MASTER])
+        expect(project.protected_branches.first.merge_access_levels.map(&:access_level)).to eq([Gitlab::Access::DEVELOPER])
       end
 
       it "when pushing new commits to existing branch" do
@@ -420,7 +420,7 @@ describe GitPushService, services: true do
       context "mentioning an issue" do
         let(:message) { "this is some work.\n\nrelated to JIRA-1" }
 
-        it "should initiate one api call to jira server to mention the issue" do
+        it "initiates one api call to jira server to mention the issue" do
           execute_service(project, user, @oldrev, @newrev, @ref )
 
           expect(WebMock).to have_requested(:post, jira_api_comment_url).with(
@@ -432,7 +432,7 @@ describe GitPushService, services: true do
       context "closing an issue" do
         let(:message) { "this is some work.\n\ncloses JIRA-1" }
 
-        it "should initiate one api call to jira server to close the issue" do
+        it "initiates one api call to jira server to close the issue" do
           transition_body = {
             transition: {
               id: '2'
@@ -445,7 +445,7 @@ describe GitPushService, services: true do
           ).once
         end
 
-        it "should initiate one api call to jira server to comment on the issue" do
+        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
diff --git a/spec/services/import_export_clean_up_service_spec.rb b/spec/services/import_export_clean_up_service_spec.rb
new file mode 100644
index 0000000000000000000000000000000000000000..81b1d327696e308cc409b20a03486e4c4ec2fcdb
--- /dev/null
+++ b/spec/services/import_export_clean_up_service_spec.rb
@@ -0,0 +1,64 @@
+require 'spec_helper'
+
+describe ImportExportCleanUpService, services: true do
+  describe '#execute' do
+    let(:service) { described_class.new }
+
+    let(:tmp_import_export_folder) { 'tmp/project_exports' }
+
+    context 'when the import/export directory does not exist' do
+      it 'does not remove any archives' do
+        path = '/invalid/path/'
+        stub_repository_downloads_path(path)
+
+        expect(File).to receive(:directory?).with(path + tmp_import_export_folder).and_return(false).at_least(:once)
+        expect(service).not_to receive(:clean_up_export_files)
+
+        service.execute
+      end
+    end
+
+    context 'when the import/export directory exists' do
+      it 'removes old files' do
+        in_directory_with_files(mtime: 2.days.ago) do |dir, files|
+          service.execute
+
+          files.each { |file| expect(File.exist?(file)).to eq false }
+          expect(File.directory?(dir)).to eq false
+        end
+      end
+
+      it 'does not remove new files' do
+        in_directory_with_files(mtime: 2.hours.ago) do |dir, files|
+          service.execute
+
+          files.each { |file| expect(File.exist?(file)).to eq true }
+          expect(File.directory?(dir)).to eq true
+        end
+      end
+    end
+
+    def in_directory_with_files(mtime:)
+      Dir.mktmpdir do |tmpdir|
+        stub_repository_downloads_path(tmpdir)
+        dir = File.join(tmpdir, tmp_import_export_folder, 'subfolder')
+        FileUtils.mkdir_p(dir)
+
+        files = FileUtils.touch(file_list(dir) + [dir], mtime: mtime.to_time)
+
+        yield(dir, files)
+      end
+    end
+
+    def stub_repository_downloads_path(path)
+      new_shared_settings = Settings.shared.merge('path' => path)
+      allow(Settings).to receive(:shared).and_return(new_shared_settings)
+    end
+
+    def file_list(dir)
+      Array.new(5) do |num|
+        File.join(dir, "random-#{num}.tar.gz")
+      end
+    end
+  end
+end
diff --git a/spec/services/issues/bulk_update_service_spec.rb b/spec/services/issues/bulk_update_service_spec.rb
index 321b54ac39df8b14f983c745a3e48f6bf4d2e80f..ac08aa53b0ba04129b07fc57d6d4fc0729a5317a 100644
--- a/spec/services/issues/bulk_update_service_spec.rb
+++ b/spec/services/issues/bulk_update_service_spec.rb
@@ -217,7 +217,7 @@ describe Issues::BulkUpdateService, services: true do
       let(:labels) { [merge_requests] }
       let(:remove_labels) { [regression] }
 
-      it 'remove the label IDs from all issues passed' do
+      it 'removes the label IDs from all issues passed' do
         expect(issues.map(&:reload).map(&:label_ids).flatten).not_to include(regression.id)
       end
 
diff --git a/spec/services/issues/close_service_spec.rb b/spec/services/issues/close_service_spec.rb
index 67a919ba8ee32016a6e31e0d421cf23e1dd8b792..aff022a573e03a4c9d9428f4ba6ad065e31b71d5 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,26 +11,27 @@ describe Issues::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
       before do
         perform_enqueued_jobs do
-          @issue = Issues::CloseService.new(project, user, {}).execute(issue)
+          @issue = described_class.new(project, user, {}).execute(issue)
         end
       end
 
       it { expect(@issue).to be_valid }
       it { expect(@issue).to be_closed }
 
-      it 'should send email to user2 about assign of new issue' do
+      it 'sends email to user2 about assign of new issue' do
         email = ActionMailer::Base.deliveries.last
         expect(email.to.first).to eq(user2.email)
         expect(email.subject).to include(issue.title)
       end
 
-      it 'should create system note about issue reassign' do
+      it 'creates system note about issue reassign' do
         note = @issue.notes.last
         expect(note.note).to include "Status changed to closed"
       end
@@ -39,10 +41,22 @@ describe Issues::CloseService, services: true do
       end
     end
 
+    context 'current user is not authorized to close issue' do
+      before do
+        perform_enqueued_jobs do
+          @issue = described_class.new(project, guest).execute(issue)
+        end
+      end
+
+      it 'does not close the issue' do
+        expect(@issue).to be_open
+      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)
+        @issue = described_class.new(project, user, {}).execute(issue)
       end
 
       it { expect(@issue).to be_valid }
diff --git a/spec/services/issues/create_service_spec.rb b/spec/services/issues/create_service_spec.rb
index 1ee9f3aae4dcb17aaf754121e33e75c4381df444..fcc3c0a00bd08e946d7409e5f26930e115c1cbd1 100644
--- a/spec/services/issues/create_service_spec.rb
+++ b/spec/services/issues/create_service_spec.rb
@@ -73,5 +73,7 @@ describe Issues::CreateService, services: true do
         end
       end
     end
+
+    it_behaves_like 'new issuable record that supports slash commands'
   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..34a89fcd4e1869d01bcd3bb3a81ed9bd6f97efaa
--- /dev/null
+++ b/spec/services/issues/reopen_service_spec.rb
@@ -0,0 +1,25 @@
+require 'spec_helper'
+
+describe Issues::ReopenService, services: true do
+  let(:guest) { create(:user) }
+  let(:issue) { create(:issue, :closed) }
+  let(:project) { issue.project }
+
+  before do
+    project.team << [guest, :guest]
+  end
+
+  describe '#execute' do
+    context 'current user is not authorized to reopen issue' do
+      before do
+        perform_enqueued_jobs do
+          @issue = described_class.new(project, guest).execute(issue)
+        end
+      end
+
+      it 'does not reopen the issue' do
+        expect(@issue).to be_closed
+      end
+    end
+  end
+end
diff --git a/spec/services/issues/update_service_spec.rb b/spec/services/issues/update_service_spec.rb
index dacbcd8fb46e07c5ae61a41490f2418cce8ab99a..0313f4244639b3a45eed461c879fae52d8f3363b 100644
--- a/spec/services/issues/update_service_spec.rb
+++ b/spec/services/issues/update_service_spec.rb
@@ -53,7 +53,7 @@ describe Issues::UpdateService, services: true do
       it { expect(@issue.labels.count).to eq(1) }
       it { expect(@issue.labels.first.title).to eq(label.name) }
 
-      it 'should send email to user2 about assign of new issue and email to user3 about issue unassignment' do
+      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
@@ -61,14 +61,14 @@ describe Issues::UpdateService, services: true do
         expect(email.subject).to include(issue.title)
       end
 
-      it 'should create system note about issue reassign' do
+      it 'creates system note about issue reassign' do
         note = find_note('Reassigned to')
 
         expect(note).not_to be_nil
         expect(note.note).to include "Reassigned to \@#{user2.username}"
       end
 
-      it 'should create system note about issue label edit' do
+      it 'creates system note about issue label edit' do
         note = find_note('Added ~')
 
         expect(note).not_to be_nil
@@ -267,7 +267,7 @@ describe Issues::UpdateService, services: true do
           expect(note).to be_nil
         end
 
-        it 'should not generate a new note at all' do
+        it 'does not generate a new note at all' do
           expect do
             update_issue({ description: "- [ ] One\n- [ ] Two\n- [ ] Three" })
           end.not_to change { Note.count }
@@ -319,5 +319,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/merge_requests/build_service_spec.rb b/spec/services/merge_requests/build_service_spec.rb
index 782d74ec5ecf3f331b4c8a451877b8f2f83e6962..232508cda23bc4f4e6cc7b1c040c2c3248a61bd7 100644
--- a/spec/services/merge_requests/build_service_spec.rb
+++ b/spec/services/merge_requests/build_service_spec.rb
@@ -61,7 +61,7 @@ describe MergeRequests::BuildService, services: true do
     end
 
     context 'one commit in the diff' do
-      let(:commits) { [commit_1] }
+      let(:commits) { Commit.decorate([commit_1], project) }
 
       it 'allows the merge request to be created' do
         expect(merge_request.can_be_created).to eq(true)
@@ -84,7 +84,7 @@ describe MergeRequests::BuildService, services: true do
       end
 
       context 'commit has no description' do
-        let(:commits) { [commit_2] }
+        let(:commits) { Commit.decorate([commit_2], project) }
 
         it 'uses the title of the commit as the title of the merge request' do
           expect(merge_request.title).to eq(commit_2.safe_message)
@@ -111,7 +111,7 @@ describe MergeRequests::BuildService, services: true do
         end
 
         context 'commit has no description' do
-          let(:commits) { [commit_2] }
+          let(:commits) { Commit.decorate([commit_2], project) }
 
           it 'sets the description to "Closes #$issue-iid"' do
             expect(merge_request.description).to eq("Closes ##{issue.iid}")
@@ -121,7 +121,7 @@ describe MergeRequests::BuildService, services: true do
     end
 
     context 'more than one commit in the diff' do
-      let(:commits) { [commit_1, commit_2] }
+      let(:commits) { Commit.decorate([commit_1, commit_2], project) }
 
       it 'allows the merge request to be created' do
         expect(merge_request.can_be_created).to eq(true)
diff --git a/spec/services/merge_requests/close_service_spec.rb b/spec/services/merge_requests/close_service_spec.rb
index c1db4f3284b18eec69923ac1b535811b18a58a48..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)
@@ -32,13 +34,13 @@ describe MergeRequests::CloseService, services: true do
                                with(@merge_request, 'close')
       end
 
-      it 'should send email to user2 about assign of new merge_request' do
+      it 'sends email to user2 about assign of new merge_request' do
         email = ActionMailer::Base.deliveries.last
         expect(email.to.first).to eq(user2.email)
         expect(email.subject).to include(merge_request.title)
       end
 
-      it 'should create system note about merge_request reassign' do
+      it 'creates system note about merge_request reassign' do
         note = @merge_request.notes.last
         expect(note.note).to include 'Status changed to closed'
       end
@@ -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 d0b55d2d5094bdbf6b908ab8f77f211cfd620841..c1e4f8bd96b019b8bb09c6b9445d2cc390ec9d54 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]
@@ -32,7 +32,7 @@ describe MergeRequests::CreateService, services: true do
       it { expect(@merge_request.assignee).to be_nil }
       it { expect(@merge_request.merge_params['force_remove_source_branch']).to eq('1') }
 
-      it 'should execute hooks with default action' do
+      it 'executes hooks with default action' do
         expect(service).to have_received(:execute_hooks).with(@merge_request)
       end
 
@@ -74,5 +74,14 @@ 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
   end
 end
diff --git a/spec/services/merge_requests/get_urls_service_spec.rb b/spec/services/merge_requests/get_urls_service_spec.rb
new file mode 100644
index 0000000000000000000000000000000000000000..8a4b76367e32edce78979525fb064e2abf4197a1
--- /dev/null
+++ b/spec/services/merge_requests/get_urls_service_spec.rb
@@ -0,0 +1,134 @@
+require "spec_helper"
+
+describe MergeRequests::GetUrlsService do
+  let(:project) { create(:project, :public) }
+  let(:service) { MergeRequests::GetUrlsService.new(project) }
+  let(:source_branch) { "my_branch" }
+  let(:new_merge_request_url) { "http://localhost/#{project.namespace.name}/#{project.path}/merge_requests/new?merge_request%5Bsource_branch%5D=#{source_branch}" }
+  let(:show_merge_request_url) { "http://localhost/#{project.namespace.name}/#{project.path}/merge_requests/#{merge_request.iid}" }
+  let(:new_branch_changes) { "#{Gitlab::Git::BLANK_SHA} 570e7b2abdd848b95f2f578043fc23bd6f6fd24d refs/heads/#{source_branch}" }
+  let(:deleted_branch_changes) { "d14d6c0abdd253381df51a723d58691b2ee1ab08 #{Gitlab::Git::BLANK_SHA} refs/heads/#{source_branch}" }
+  let(:existing_branch_changes) { "d14d6c0abdd253381df51a723d58691b2ee1ab08 570e7b2abdd848b95f2f578043fc23bd6f6fd24d refs/heads/#{source_branch}" }
+  let(:default_branch_changes) { "d14d6c0abdd253381df51a723d58691b2ee1ab08 570e7b2abdd848b95f2f578043fc23bd6f6fd24d refs/heads/master" }
+
+  describe "#execute" do
+    shared_examples 'new_merge_request_link' do
+      it 'returns url to create new merge request' do
+        result = service.execute(changes)
+        expect(result).to match([{
+          branch_name: source_branch,
+          url: new_merge_request_url,
+          new_merge_request: true
+        }])
+      end
+    end
+
+    shared_examples 'show_merge_request_url' do
+      it 'returns url to view merge request' do
+        result = service.execute(changes)
+        expect(result).to match([{
+          branch_name: source_branch,
+          url: show_merge_request_url,
+          new_merge_request: false
+        }])
+      end
+    end
+
+    shared_examples 'no_merge_request_url' do
+      it 'returns no URL' do
+        result = service.execute(changes)
+        expect(result).to be_empty
+      end
+    end
+
+    context 'pushing to default branch' do
+      let(:changes) { default_branch_changes }
+      it_behaves_like 'no_merge_request_url'
+    end
+
+    context 'pushing to project with MRs disabled' do
+      let(:changes) { new_branch_changes }
+
+      before do
+        project.merge_requests_enabled = false
+      end
+
+      it_behaves_like 'no_merge_request_url'
+    end
+
+    context 'pushing one completely new branch' do
+      let(:changes) { new_branch_changes }
+      it_behaves_like 'new_merge_request_link'
+    end
+
+    context 'pushing to existing branch but no merge request' do
+      let(:changes) { existing_branch_changes }
+      it_behaves_like 'new_merge_request_link'
+    end
+
+    context 'pushing to deleted branch' do
+      let(:changes) { deleted_branch_changes }
+      it_behaves_like 'no_merge_request_url'
+    end
+
+    context 'pushing to existing branch and merge request opened' do
+      let!(:merge_request) { create(:merge_request, source_project: project, source_branch: source_branch) }
+      let(:changes) { existing_branch_changes }
+      it_behaves_like 'show_merge_request_url'
+    end
+
+    context 'pushing to existing branch and merge request is reopened' do
+      let!(:merge_request) { create(:merge_request, :reopened, source_project: project, source_branch: source_branch) }
+      let(:changes) { existing_branch_changes }
+      it_behaves_like 'show_merge_request_url'
+    end
+
+    context 'pushing to existing branch from forked project' do
+      let(:user) { create(:user) }
+      let!(:forked_project) { Projects::ForkService.new(project, user).execute }
+      let!(:merge_request) { create(:merge_request, source_project: forked_project, target_project: project, source_branch: source_branch) }
+      let(:changes) { existing_branch_changes }
+      # Source project is now the forked one
+      let(:service) { MergeRequests::GetUrlsService.new(forked_project) }
+
+      before do
+        allow(forked_project).to receive(:empty_repo?).and_return(false)
+      end
+
+      it_behaves_like 'show_merge_request_url'
+    end
+
+    context 'pushing to existing branch and merge request is closed' do
+      let!(:merge_request) { create(:merge_request, :closed, source_project: project, source_branch: source_branch) }
+      let(:changes) { existing_branch_changes }
+      it_behaves_like 'new_merge_request_link'
+    end
+
+    context 'pushing to existing branch and merge request is merged' do
+      let!(:merge_request) { create(:merge_request, :merged, source_project: project, source_branch: source_branch) }
+      let(:changes) { existing_branch_changes }
+      it_behaves_like 'new_merge_request_link'
+    end
+
+    context 'pushing new branch and existing branch (with merge request created) at once' do
+      let!(:merge_request) { create(:merge_request, source_project: project, source_branch: "existing_branch") }
+      let(:new_branch_changes) { "#{Gitlab::Git::BLANK_SHA} 570e7b2abdd848b95f2f578043fc23bd6f6fd24d refs/heads/new_branch" }
+      let(:existing_branch_changes) { "d14d6c0abdd253381df51a723d58691b2ee1ab08 570e7b2abdd848b95f2f578043fc23bd6f6fd24d refs/heads/existing_branch" }
+      let(:changes) { "#{new_branch_changes}\n#{existing_branch_changes}" }
+      let(:new_merge_request_url) { "http://localhost/#{project.namespace.name}/#{project.path}/merge_requests/new?merge_request%5Bsource_branch%5D=new_branch" }
+
+      it 'returns 2 urls for both creating new and showing merge request' do
+        result = service.execute(changes)
+        expect(result).to match([{
+          branch_name: "new_branch",
+          url: new_merge_request_url,
+          new_merge_request: true
+        }, {
+          branch_name: "existing_branch",
+          url: show_merge_request_url,
+          new_merge_request: false
+        }])
+      end
+    end
+  end
+end
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
new file mode 100644
index 0000000000000000000000000000000000000000..c4b874682751c550ab3ce83e6d90b25ce9f15046
--- /dev/null
+++ b/spec/services/merge_requests/merge_request_diff_cache_service_spec.rb
@@ -0,0 +1,17 @@
+require 'spec_helper'
+
+describe MergeRequests::MergeRequestDiffCacheService do
+  let(:subject) { MergeRequests::MergeRequestDiffCacheService.new }
+
+  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]
+
+      expect(Rails.cache).to receive(:read).with(cache_key).and_return({})
+      expect(Rails.cache).to receive(:write).with(cache_key, anything)
+
+      subject.execute(merge_request)
+    end
+  end
+end
diff --git a/spec/services/merge_requests/merge_service_spec.rb b/spec/services/merge_requests/merge_service_spec.rb
index 8ffebcac6986e738dad562eef552decb132bd8b8..159f6817e8d9bbd9ade7d6a845a30dbfc21dcefb 100644
--- a/spec/services/merge_requests/merge_service_spec.rb
+++ b/spec/services/merge_requests/merge_service_spec.rb
@@ -26,13 +26,13 @@ describe MergeRequests::MergeService, services: true do
       it { expect(merge_request).to be_valid }
       it { expect(merge_request).to be_merged }
 
-      it 'should send email to user2 about merge of new merge_request' do
+      it 'sends email to user2 about merge of new merge_request' do
         email = ActionMailer::Base.deliveries.last
         expect(email.to.first).to eq(user2.email)
         expect(email.subject).to include(merge_request.title)
       end
 
-      it 'should create system note about merge_request merge' do
+      it 'creates system note about merge_request merge' do
         note = merge_request.notes.last
         expect(note.note).to include 'Status changed to merged'
       end
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 4da8146e3d6a41dd59a283bd4d5a952f9d4e939c..520e906b21f357e9b8c909a5d7160f743a819a63 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
@@ -110,19 +110,15 @@ describe MergeRequests::MergeWhenBuildSucceedsService do
 
     context 'properly handles multiple stages' do
       let(:ref) { mr_merge_if_green_enabled.source_branch }
-      let(:build) { create(:ci_build, pipeline: pipeline, ref: ref, name: 'build', stage: 'build') }
-      let(:test) { create(:ci_build, pipeline: pipeline, ref: ref, name: 'test', stage: 'test') }
+      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) }
 
       before do
         # This behavior of MergeRequest: we instantiate a new object
         allow_any_instance_of(MergeRequest).to receive(:pipeline).and_wrap_original do
           Ci::Pipeline.find(pipeline.id)
         end
-
-        # We create test after the build
-        allow(pipeline).to receive(:create_next_builds).and_wrap_original do
-          test
-        end
       end
 
       it "doesn't merge if some stages failed" do
diff --git a/spec/services/merge_requests/refresh_service_spec.rb b/spec/services/merge_requests/refresh_service_spec.rb
index 781ee7ffed3ed113fc6a3d64fc4dcb26f72f5475..fff86480c6d7771c6da31841523b7679e4081b76 100644
--- a/spec/services/merge_requests/refresh_service_spec.rb
+++ b/spec/services/merge_requests/refresh_service_spec.rb
@@ -55,7 +55,7 @@ describe MergeRequests::RefreshService, services: true do
         reload_mrs
       end
 
-      it 'should execute hooks with update action' do
+      it 'executes hooks with update action' do
         expect(refresh_service).to have_received(:execute_hooks).
           with(@merge_request, 'update', @oldrev)
       end
@@ -111,7 +111,7 @@ describe MergeRequests::RefreshService, services: true do
         reload_mrs
       end
 
-      it 'should execute hooks with update action' do
+      it 'executes hooks with update action' do
         expect(refresh_service).to have_received(:execute_hooks).
           with(@fork_merge_request, 'update', @oldrev)
       end
diff --git a/spec/services/merge_requests/reopen_service_spec.rb b/spec/services/merge_requests/reopen_service_spec.rb
index 88c9c640514533f76918e209eafeacc165e8db47..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
@@ -27,21 +28,33 @@ describe MergeRequests::ReopenService, services: true do
       it { expect(merge_request).to be_valid }
       it { expect(merge_request).to be_reopened }
 
-      it 'should execute hooks with reopen action' do
+      it 'executes hooks with reopen action' do
         expect(service).to have_received(:execute_hooks).
                                with(merge_request, 'reopen')
       end
 
-      it 'should send email to user2 about reopen of merge_request' do
+      it 'sends email to user2 about reopen of merge_request' do
         email = ActionMailer::Base.deliveries.last
         expect(email.to.first).to eq(user2.email)
         expect(email.subject).to include(merge_request.title)
       end
 
-      it 'should create system note about merge_request reopen' do
+      it 'creates system note about merge_request reopen' do
         note = merge_request.notes.last
         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/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 d4ebe28c276a68ef48c2938d17eceafca430375f..6dfeb581975e13c85586c66bbd1c370b7996924f 100644
--- a/spec/services/merge_requests/update_service_spec.rb
+++ b/spec/services/merge_requests/update_service_spec.rb
@@ -64,12 +64,12 @@ describe MergeRequests::UpdateService, services: true do
       it { expect(@merge_request.target_branch).to eq('target') }
       it { expect(@merge_request.merge_params['force_remove_source_branch']).to eq('1') }
 
-      it 'should execute hooks with update action' do
+      it 'executes hooks with update action' do
         expect(service).to have_received(:execute_hooks).
                                with(@merge_request, 'update')
       end
 
-      it 'should send email to user2 about assign of new merge request and email to user3 about merge request unassignment' do
+      it 'sends email to user2 about assign of new merge request and email to user3 about merge request unassignment' do
         deliveries = ActionMailer::Base.deliveries
         email = deliveries.last
         recipients = deliveries.last(2).map(&:to).flatten
@@ -77,14 +77,14 @@ describe MergeRequests::UpdateService, services: true do
         expect(email.subject).to include(merge_request.title)
       end
 
-      it 'should create system note about merge_request reassign' do
+      it 'creates system note about merge_request reassign' do
         note = find_note('Reassigned to')
 
         expect(note).not_to be_nil
         expect(note.note).to include "Reassigned to \@#{user2.username}"
       end
 
-      it 'should create system note about merge_request label edit' do
+      it 'creates system note about merge_request label edit' do
         note = find_note('Added ~')
 
         expect(note).not_to be_nil
@@ -226,6 +226,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" }) }
 
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..4f231aab161c7c97bf2449a23d8f8b9894fa04d8
--- /dev/null
+++ b/spec/services/notes/slash_commands_service_spec.rb
@@ -0,0 +1,140 @@
+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 '#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 9fc93f325f72919fd3849a400106de5a04bfe15e..f81a58899fd819d5a987169afb0bf6f028b7f153 100644
--- a/spec/services/notification_service_spec.rb
+++ b/spec/services/notification_service_spec.rb
@@ -9,13 +9,35 @@ 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
+      expect(ActionMailer::Base.deliveries).to be_empty
+    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)
+      expect(ActionMailer::Base.deliveries).to be_empty
+    end
+  end
+
   describe 'Keys' do
     describe '#new_key' do
       let!(:key) { create(:personal_key) }
 
       it { expect(notification.new_key(key)).to be_truthy }
 
-      it 'should sent email to key owner' do
+      it 'sends email to key owner' do
         expect{ notification.new_key(key) }.to change{ ActionMailer::Base.deliveries.size }.by(1)
       end
     end
@@ -27,7 +49,7 @@ describe NotificationService, services: true do
 
       it { expect(notification.new_email(email)).to be_truthy }
 
-      it 'should send email to email owner' do
+      it 'sends email to email owner' do
         expect{ notification.new_email(email) }.to change{ ActionMailer::Base.deliveries.size }.by(1)
       end
     end
@@ -399,6 +421,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)
@@ -593,7 +622,7 @@ describe NotificationService, services: true do
         update_custom_notification(:close_issue, @u_custom_global)
       end
 
-      it 'should sent email to issue assignee and issue author' do
+      it 'sends email to issue assignee and issue author' do
         notification.close_issue(issue, @u_disabled)
 
         should_email(issue.assignee)
@@ -646,7 +675,7 @@ describe NotificationService, services: true do
         update_custom_notification(:reopen_issue, @u_custom_global)
       end
 
-      it 'should send email to issue assignee and issue author' do
+      it 'sends email to issue assignee and issue author' do
         notification.reopen_issue(issue, @u_disabled)
 
         should_email(issue.assignee)
@@ -700,6 +729,8 @@ describe NotificationService, services: true do
     before do
       build_team(merge_request.target_project)
       add_users_with_subscription(merge_request.target_project, merge_request)
+      update_custom_notification(:new_merge_request, @u_guest_custom, project)
+      update_custom_notification(:new_merge_request, @u_custom_global)
       ActionMailer::Base.deliveries.clear
     end
 
@@ -763,6 +794,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)
@@ -1004,6 +1042,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
@@ -1029,6 +1113,46 @@ 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
+
   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/autocomplete_service_spec.rb b/spec/services/projects/autocomplete_service_spec.rb
index 0971fec2e9f29bf9a34d11c7bcf27e1ba181b6b7..7916c2d957cc068379fc07c3cdc8805d30a8d777 100644
--- a/spec/services/projects/autocomplete_service_spec.rb
+++ b/spec/services/projects/autocomplete_service_spec.rb
@@ -13,7 +13,7 @@ describe Projects::AutocompleteService, services: true do
       let!(:security_issue_1) { create(:issue, :confidential, project: project, title: 'Security issue 1', author: author) }
       let!(:security_issue_2) { create(:issue, :confidential, title: 'Security issue 2', project: project, assignee: assignee) }
 
-      it 'should not list project confidential issues for guests' do
+      it 'does not list project confidential issues for guests' do
         autocomplete = described_class.new(project, nil)
         issues = autocomplete.issues.map(&:iid)
 
@@ -23,7 +23,7 @@ describe Projects::AutocompleteService, services: true do
         expect(issues.count).to eq 1
       end
 
-      it 'should not list project confidential issues for non project members' do
+      it 'does not list project confidential issues for non project members' do
         autocomplete = described_class.new(project, non_member)
         issues = autocomplete.issues.map(&:iid)
 
@@ -33,7 +33,7 @@ describe Projects::AutocompleteService, services: true do
         expect(issues.count).to eq 1
       end
 
-      it 'should not list project confidential issues for project members with guest role' do
+      it 'does not list project confidential issues for project members with guest role' do
         project.team << [member, :guest]
 
         autocomplete = described_class.new(project, non_member)
@@ -45,7 +45,7 @@ describe Projects::AutocompleteService, services: true do
         expect(issues.count).to eq 1
       end
 
-      it 'should list project confidential issues for author' do
+      it 'lists project confidential issues for author' do
         autocomplete = described_class.new(project, author)
         issues = autocomplete.issues.map(&:iid)
 
@@ -55,7 +55,7 @@ describe Projects::AutocompleteService, services: true do
         expect(issues.count).to eq 2
       end
 
-      it 'should list project confidential issues for assignee' do
+      it 'lists project confidential issues for assignee' do
         autocomplete = described_class.new(project, assignee)
         issues = autocomplete.issues.map(&:iid)
 
@@ -65,7 +65,7 @@ describe Projects::AutocompleteService, services: true do
         expect(issues.count).to eq 2
       end
 
-      it 'should list project confidential issues for project members' do
+      it 'lists project confidential issues for project members' do
         project.team << [member, :developer]
 
         autocomplete = described_class.new(project, member)
@@ -77,7 +77,7 @@ describe Projects::AutocompleteService, services: true do
         expect(issues.count).to eq 3
       end
 
-      it 'should list all project issues for admin' do
+      it 'lists all project issues for admin' do
         autocomplete = described_class.new(project, admin)
         issues = autocomplete.issues.map(&:iid)
 
diff --git a/spec/services/projects/create_service_spec.rb b/spec/services/projects/create_service_spec.rb
index fd1143594672e541dbab0faa258ca548047671b7..bbced59ff023b1c36fbdbbfbd7600e7af0bb1d0e 100644
--- a/spec/services/projects/create_service_spec.rb
+++ b/spec/services/projects/create_service_spec.rb
@@ -109,7 +109,7 @@ describe Projects::CreateService, services: true do
         )
       end
 
-      it 'should not allow a restricted visibility level for non-admins' do
+      it 'does not allow a restricted visibility level for non-admins' do
         project = create_project(@user, @opts)
         expect(project).to respond_to(:errors)
         expect(project.errors.messages).to have_key(:visibility_level)
@@ -118,7 +118,7 @@ describe Projects::CreateService, services: true do
         )
       end
 
-      it 'should allow a restricted visibility level for admins' do
+      it 'allows a restricted visibility level for admins' do
         admin = create(:admin)
         project = create_project(admin, @opts)
 
@@ -128,7 +128,7 @@ describe Projects::CreateService, services: true do
     end
 
     context 'repository creation' do
-      it 'should synchronously create the repository' do
+      it 'synchronously creates the repository' do
         expect_any_instance_of(Project).to receive(:create_repository)
 
         project = create_project(@user, @opts)
diff --git a/spec/services/projects/enable_deploy_key_service_spec.rb b/spec/services/projects/enable_deploy_key_service_spec.rb
new file mode 100644
index 0000000000000000000000000000000000000000..a37510cf159897314d196ad92969370832d81cd1
--- /dev/null
+++ b/spec/services/projects/enable_deploy_key_service_spec.rb
@@ -0,0 +1,27 @@
+require 'spec_helper'
+
+describe Projects::EnableDeployKeyService, services: true do
+  let(:deploy_key)  { create(:deploy_key, public: true) }
+  let(:project)     { create(:empty_project) }
+  let(:user)        { project.creator}
+  let!(:params)     { { key_id: deploy_key.id } }
+
+  it 'enables the key' do
+    expect do
+      service.execute
+    end.to change { project.deploy_keys.count }.from(0).to(1)
+  end
+
+  context 'trying to add an unaccessable key' do
+    let(:another_key) { create(:another_key) }
+    let!(:params)     { { key_id: another_key.id } }
+
+    it 'returns nil if the key cannot be added' do
+      expect(service.execute).to be nil
+    end
+  end
+
+  def service
+    Projects::EnableDeployKeyService.new(project, user, params)
+  end
+end
diff --git a/spec/services/projects/fork_service_spec.rb b/spec/services/projects/fork_service_spec.rb
index 31bb7120d84c6732f9eb761014f1454c759f8807..ef2036c78b1ab8b387d2c61124fff330a8cb2851 100644
--- a/spec/services/projects/fork_service_spec.rb
+++ b/spec/services/projects/fork_service_spec.rb
@@ -26,7 +26,7 @@ describe Projects::ForkService, services: true do
     end
 
     context 'project already exists' do
-      it "should fail due to validation, not transaction failure" 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
@@ -36,7 +36,7 @@ describe Projects::ForkService, services: true do
     end
 
     context 'GitLab CI is enabled' do
-      it "fork and enable CI for fork" do
+      it "forks and enables CI for fork" do
         @from_project.enable_ci
         @to_project = fork_project(@from_project, @to_user)
         expect(@to_project.builds_enabled?).to be_truthy
@@ -97,14 +97,14 @@ describe Projects::ForkService, services: true do
     end
 
     context 'fork project for group when user not owner' do
-      it 'group developer should fail to fork project into the group' do
+      it 'group developer fails to fork project into the group' do
         to_project = fork_project(@project, @developer, @opts)
         expect(to_project.errors[:namespace]).to eq(['is not valid'])
       end
     end
 
     context 'project already exists in group' do
-      it 'should fail due to validation, not transaction failure' do
+      it 'fails due to validation, not transaction failure' do
         existing_project = create(:project, name: @project.name,
                                             namespace: @group)
         to_project = fork_project(@project, @group_owner, @opts)
diff --git a/spec/services/projects/update_service_spec.rb b/spec/services/projects/update_service_spec.rb
index e8b9e6b923840a4a5ac2406201aefd2d6950e3c0..e139be19140cd6bdbefca2e1d0799e29fcd818f6 100644
--- a/spec/services/projects/update_service_spec.rb
+++ b/spec/services/projects/update_service_spec.rb
@@ -9,7 +9,7 @@ describe Projects::UpdateService, services: true do
       @opts = {}
     end
 
-    context 'should be private when updated to private' do
+    context 'is private when updated to private' do
       before do
         @created_private = @project.private?
 
@@ -21,7 +21,7 @@ describe Projects::UpdateService, services: true do
       it { expect(@project.private?).to be_truthy }
     end
 
-    context 'should be internal when updated to internal' do
+    context 'is internal when updated to internal' do
       before do
         @created_private = @project.private?
 
@@ -33,7 +33,7 @@ describe Projects::UpdateService, services: true do
       it { expect(@project.internal?).to be_truthy }
     end
 
-    context 'should be public when updated to public' do
+    context 'is public when updated to public' do
       before do
         @created_private = @project.private?
 
@@ -50,7 +50,7 @@ describe Projects::UpdateService, services: true do
         stub_application_setting(restricted_visibility_levels: [Gitlab::VisibilityLevel::PUBLIC])
       end
 
-      context 'should be private when updated to private' do
+      context 'is private when updated to private' do
         before do
           @created_private = @project.private?
 
@@ -62,7 +62,7 @@ describe Projects::UpdateService, services: true do
         it { expect(@project.private?).to be_truthy }
       end
 
-      context 'should be internal when updated to internal' do
+      context 'is internal when updated to internal' do
         before do
           @created_private = @project.private?
 
@@ -74,7 +74,7 @@ describe Projects::UpdateService, services: true do
         it { expect(@project.internal?).to be_truthy }
       end
 
-      context 'should be private when updated to public' do
+      context 'is private when updated to public' do
         before do
           @created_private = @project.private?
 
@@ -86,7 +86,7 @@ describe Projects::UpdateService, services: true do
         it { expect(@project.private?).to be_truthy }
       end
 
-      context 'should be public when updated to public by admin' do
+      context 'is public when updated to public by admin' do
         before do
           @created_private = @project.private?
 
@@ -114,7 +114,7 @@ describe Projects::UpdateService, services: true do
       @fork_created_internal = forked_project.internal?
     end
 
-    context 'should update forks visibility level when parent set to more restrictive' do
+    context 'updates forks visibility level when parent set to more restrictive' do
       before do
         opts.merge!(visibility_level: Gitlab::VisibilityLevel::PRIVATE)
         update_project(project, user, opts).inspect
@@ -126,7 +126,7 @@ describe Projects::UpdateService, services: true do
       it { expect(project.forks.first.private?).to be_truthy }
     end
 
-    context 'should not update forks visibility level when parent set to less restrictive' do
+    context 'does not update forks visibility level when parent set to less restrictive' do
       before do
         opts.merge!(visibility_level: Gitlab::VisibilityLevel::PUBLIC)
         update_project(project, user, opts).inspect
diff --git a/spec/services/repair_ldap_blocked_user_service_spec.rb b/spec/services/repair_ldap_blocked_user_service_spec.rb
index ce7d1455975409772068666a304866fb68402e84..87192457298c57c540dff77189f8e34432a3fda4 100644
--- a/spec/services/repair_ldap_blocked_user_service_spec.rb
+++ b/spec/services/repair_ldap_blocked_user_service_spec.rb
@@ -6,14 +6,14 @@ describe RepairLdapBlockedUserService, services: true do
   subject(:service) { RepairLdapBlockedUserService.new(user) }
 
   describe '#execute' do
-    it 'change to normal block after destroying last ldap identity' do
+    it 'changes to normal block after destroying last ldap identity' do
       identity.destroy
       service.execute
 
       expect(user.reload).not_to be_ldap_blocked
     end
 
-    it 'change to normal block after changing last ldap identity to another provider' do
+    it 'changes to normal block after changing last ldap identity to another provider' do
       identity.update_attribute(:provider, 'twitter')
       service.execute
 
diff --git a/spec/services/search_service_spec.rb b/spec/services/search_service_spec.rb
index 7b3a9a75d7c4d535f096a73850d84951f2533c24..bd89c4a7c116bc5db8e49e730cc5a881884bb67b 100644
--- a/spec/services/search_service_spec.rb
+++ b/spec/services/search_service_spec.rb
@@ -16,7 +16,7 @@ describe 'Search::GlobalService', services: true do
 
   describe '#execute' do
     context 'unauthenticated' do
-      it 'should return public projects only' do
+      it 'returns public projects only' do
         context = Search::GlobalService.new(nil, search: "searchable")
         results = context.execute
         expect(results.objects('projects')).to match_array [public_project]
@@ -24,19 +24,19 @@ describe 'Search::GlobalService', services: true do
     end
 
     context 'authenticated' do
-      it 'should return public, internal and private projects' do
+      it 'returns public, internal and private projects' do
         context = Search::GlobalService.new(user, search: "searchable")
         results = context.execute
         expect(results.objects('projects')).to match_array [public_project, found_project, internal_project]
       end
 
-      it 'should return only public & internal projects' do
+      it 'returns only public & internal projects' do
         context = Search::GlobalService.new(internal_user, search: "searchable")
         results = context.execute
         expect(results.objects('projects')).to match_array [internal_project, public_project]
       end
 
-      it 'namespace name should be searchable' do
+      it 'namespace name is searchable' do
         context = Search::GlobalService.new(user, search: found_project.namespace.path)
         results = context.execute
         expect(results.objects('projects')).to match_array [found_project]
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..a616275e8834c30342b69ce6cce5cd233890a009
--- /dev/null
+++ b/spec/services/slash_commands/interpret_service_spec.rb
@@ -0,0 +1,384 @@
+require 'spec_helper'
+
+describe SlashCommands::InterpretService, services: true do
+  let(:project) { create(:project) }
+  let(:user) { 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 << [user, :developer]
+  end
+
+  describe '#execute' do
+    let(:service) { described_class.new(project, user) }
+    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: user.id)
+      end
+    end
+
+    shared_examples 'unassign command' do
+      it 'populates assignee_id: nil if content contains /unassign' do
+        issuable.update(assignee_id: user.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 '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 '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, user)
+        _, 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(user)
+        _, 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 '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 @#{user.username}" }
+      let(:issuable) { issue }
+    end
+
+    it_behaves_like 'assign command' do
+      let(:content) { "/assign @#{user.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 '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 '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 'empty command' do
+      let(:content) { '/remove_due_date' }
+      let(:issuable) { merge_request }
+    end
+  end
+end
diff --git a/spec/services/system_note_service_spec.rb b/spec/services/system_note_service_spec.rb
index 43693441450f7b80790ecaf1861b80e01408d537..00427d6db2a6c29dd1d7313df647940daf0e555c 100644
--- a/spec/services/system_note_service_spec.rb
+++ b/spec/services/system_note_service_spec.rb
@@ -471,15 +471,15 @@ describe SystemNoteService, services: true do
     shared_examples 'cross project mentionable' do
       include GitlabMarkdownHelper
 
-      it 'should contain cross reference to new noteable' do
+      it 'contains cross reference to new noteable' do
         expect(subject.note).to include cross_project_reference(new_project, new_noteable)
       end
 
-      it 'should mention referenced noteable' do
+      it 'mentions referenced noteable' do
         expect(subject.note).to include new_noteable.to_reference
       end
 
-      it 'should mention referenced project' do
+      it 'mentions referenced project' do
         expect(subject.note).to include new_project.to_reference
       end
     end
@@ -489,7 +489,7 @@ describe SystemNoteService, services: true do
 
       it_behaves_like 'cross project mentionable'
 
-      it 'should notify about noteable being moved to' do
+      it 'notifies about noteable being moved to' do
         expect(subject.note).to match /Moved to/
       end
     end
@@ -499,7 +499,7 @@ describe SystemNoteService, services: true do
 
       it_behaves_like 'cross project mentionable'
 
-      it 'should notify about noteable being moved from' do
+      it 'notifies about noteable being moved from' do
         expect(subject.note).to match /Moved from/
       end
     end
@@ -507,7 +507,7 @@ describe SystemNoteService, services: true do
     context 'invalid direction' do
       let(:direction) { :invalid }
 
-      it 'should raise error' do
+      it 'raises error' do
         expect { subject }.to raise_error StandardError, /Invalid direction/
       end
     end
diff --git a/spec/services/test_hook_service_spec.rb b/spec/services/test_hook_service_spec.rb
index 4f47e89b4b57d7bb2dc892291d87eb47cd37584b..4f6dd8c6d3f1ce4895991c5f092916af7876404e 100644
--- a/spec/services/test_hook_service_spec.rb
+++ b/spec/services/test_hook_service_spec.rb
@@ -6,7 +6,7 @@ describe TestHookService, services: true do
   let(:hook)    { create :project_hook, project: project }
 
   describe '#execute' do
-    it "should execute successfully" do
+    it "executes successfully" do
       stub_request(:post, hook.url).to_return(status: 200)
       expect(TestHookService.new.execute(hook, user)).to be_truthy
     end
diff --git a/spec/services/todo_service_spec.rb b/spec/services/todo_service_spec.rb
index 34d8ea9090e34be57b61b02b3b5b1269ad93f131..296fd1bd5a4339b3d72511011d38dd39a5df8a6a 100644
--- a/spec/services/todo_service_spec.rb
+++ b/spec/services/todo_service_spec.rb
@@ -194,12 +194,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 +207,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 +300,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
@@ -472,6 +494,42 @@ describe TodoService, services: true do
     expect(john_doe.todos_pending_count).to eq(1)
   end
 
+  describe '#mark_todos_as_done' do
+    let(: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)
+
+      todos = TodosFinder.new(john_doe, {}).execute
+      expect { TodoService.new.mark_todos_as_done(todos, john_doe) }
+       .to change { john_doe.todos.done.count }.from(0).to(1)
+    end
+
+    it 'marks an array of todos as done' do
+      todo = create(:todo, :mentioned, user: john_doe, target: issue, project: project)
+
+      expect { TodoService.new.mark_todos_as_done([todo], john_doe) }
+        .to change { todo.reload.state }.from('pending').to('done')
+    end
+
+    it 'returns the number of updated todos' do # Needed on API
+      todo = create(:todo, :mentioned, user: john_doe, target: issue, project: project)
+
+      expect(TodoService.new.mark_todos_as_done([todo], john_doe)).to eq(1)
+    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)
+      TodoService.new.mark_todos_as_done([todo], john_doe)
+
+      expect_any_instance_of(TodosFinder).not_to receive(:execute)
+
+      expect(john_doe.todos_done_count).to eq(1)
+      expect(john_doe.todos_pending_count).to eq(1)
+    end
+  end
+
   def should_create_todo(attributes = {})
     attributes.reverse_merge!(
       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 4f3aacf55be9dbe4740f5c28e5f5dff58c2ce0b0..c144cd85487f250be31227711bf6be927f853baf 100644
--- a/spec/spec_helper.rb
+++ b/spec/spec_helper.rb
@@ -33,6 +33,7 @@ RSpec.configure do |config|
   config.include EmailHelpers
   config.include TestEnv
   config.include ActiveJob::TestHelper
+  config.include ActiveSupport::Testing::TimeHelpers
   config.include StubGitlabCalls
   config.include StubGitlabData
 
@@ -42,6 +43,13 @@ RSpec.configure do |config|
   config.before(:suite) do
     TestEnv.init
   end
+
+  config.around(:each, :caching) do |example|
+    caching_store = Rails.cache
+    Rails.cache = ActiveSupport::Cache::MemoryStore.new if example.metadata[:caching]
+    example.run
+    Rails.cache = caching_store
+  end
 end
 
 FactoryGirl::SyntaxRunner.class_eval do
diff --git a/spec/support/api/members_shared_examples.rb b/spec/support/api/members_shared_examples.rb
new file mode 100644
index 0000000000000000000000000000000000000000..dab71a35a552c526b6a8b95304ddee1ec5ee2da9
--- /dev/null
+++ b/spec/support/api/members_shared_examples.rb
@@ -0,0 +1,11 @@
+shared_examples 'a 404 response when source is private' do
+  before do
+    source.update_column(:visibility_level, Gitlab::VisibilityLevel::PRIVATE)
+  end
+
+  it 'returns 404' do
+    route
+
+    expect(response).to have_http_status(404)
+  end
+end
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/email_helpers.rb b/spec/support/email_helpers.rb
index a85ab22ce36fc814a2f5603d8d5f528a484f46ca..0bfc4685532f576c184eb0393b26ee94a41b494e 100644
--- a/spec/support/email_helpers.rb
+++ b/spec/support/email_helpers.rb
@@ -3,6 +3,16 @@ module EmailHelpers
     ActionMailer::Base.deliveries.map(&:to).flatten.count(user.email) == 1
   end
 
+  def reset_delivered_emails!
+    ActionMailer::Base.deliveries.clear
+  end
+
+  def should_only_email(*users)
+    users.each {|user| should_email(user) }
+    recipients = ActionMailer::Base.deliveries.flat_map(&:to)
+    expect(recipients.count).to eq(users.count)
+  end
+
   def should_email(user)
     expect(sent_to_user?(user)).to be_truthy
   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/import_export/import_export.yml b/spec/support/import_export/import_export.yml
index 3ceec50640121603ad6a6b89a5907ecfcfa22699..17136dee0006a4546b5aebcdfd276eebe4390217 100644
--- a/spec/support/import_export/import_export.yml
+++ b/spec/support/import_export/import_export.yml
@@ -7,6 +7,8 @@ project_tree:
     - :merge_request_test
   - commit_statuses:
     - :commit
+  - project_members:
+    - :user
 
 included_attributes:
   project:
@@ -14,6 +16,8 @@ included_attributes:
     - :path
   merge_requests:
     - :id
+  user:
+    - :email
 
 excluded_attributes:
   merge_requests:
diff --git a/spec/support/issuable_create_service_slash_commands_shared_examples.rb b/spec/support/issuable_create_service_slash_commands_shared_examples.rb
new file mode 100644
index 0000000000000000000000000000000000000000..5f9645ed44f4c59aa0c98e90f5f060faa42d8498
--- /dev/null
+++ b/spec/support/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/issuable_slash_commands_shared_examples.rb b/spec/support/issuable_slash_commands_shared_examples.rb
new file mode 100644
index 0000000000000000000000000000000000000000..d2a49ea5c5edc787d8e9997dd1144a101d816d18
--- /dev/null
+++ b/spec/support/issuable_slash_commands_shared_examples.rb
@@ -0,0 +1,289 @@
+# 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|
+  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
+
+  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
+        page.within('.js-main-target-form') do
+          fill_in 'note[note]', with: "Awesome!\n/assign @bob\n/label ~bug\n/milestone %\"ASAP\""
+          click_button 'Comment'
+        end
+
+        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
+        page.within('.js-main-target-form') do
+          fill_in 'note[note]', with: "/assign @bob\n/label ~bug\n/milestone %\"ASAP\""
+          click_button 'Comment'
+        end
+
+        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
+          page.within('.js-main-target-form') do
+            fill_in 'note[note]', with: "/close"
+            click_button 'Comment'
+          end
+
+          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
+          page.within('.js-main-target-form') do
+            fill_in 'note[note]', with: "/close"
+            click_button 'Comment'
+          end
+
+          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
+          page.within('.js-main-target-form') do
+            fill_in 'note[note]', with: "/reopen"
+            click_button 'Comment'
+          end
+
+          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
+          page.within('.js-main-target-form') do
+            fill_in 'note[note]', with: "/reopen"
+            click_button 'Comment'
+          end
+
+          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
+          page.within('.js-main-target-form') do
+            fill_in 'note[note]', with: "/title Awesome new title"
+            click_button 'Comment'
+          end
+
+          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
+          page.within('.js-main-target-form') do
+            fill_in 'note[note]', with: "/title Awesome new title"
+            click_button 'Comment'
+          end
+
+          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
+        page.within('.js-main-target-form') do
+          fill_in 'note[note]', with: "/todo"
+          click_button 'Comment'
+        end
+
+        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
+
+        page.within('.js-main-target-form') do
+          fill_in 'note[note]', with: "/done"
+          click_button 'Comment'
+        end
+
+        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
+
+        page.within('.js-main-target-form') do
+          fill_in 'note[note]', with: "/subscribe"
+          click_button 'Comment'
+        end
+
+        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
+
+        page.within('.js-main-target-form') do
+          fill_in 'note[note]', with: "/unsubscribe"
+          click_button 'Comment'
+        end
+
+        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/select2_helper.rb b/spec/support/select2_helper.rb
index 04d25b5e9e93a3ae51bce9d8916263833322f643..35cc51725c65065a75067311f6edb81820df0400 100644
--- a/spec/support/select2_helper.rb
+++ b/spec/support/select2_helper.rb
@@ -11,7 +11,7 @@
 #
 
 module Select2Helper
-  def select2(value, options={})
+  def select2(value, options = {})
     raise ArgumentError, 'options must be a Hash' unless options.kind_of?(Hash)
 
     selector = options.fetch(:from)
diff --git a/spec/support/test_env.rb b/spec/support/test_env.rb
index 4561aa9644d45f99822281e0abe69e56abdf9768..edbbfc3c9e5729674525103443a8a382c5262f9c 100644
--- a/spec/support/test_env.rb
+++ b/spec/support/test_env.rb
@@ -5,24 +5,31 @@ module TestEnv
 
   # When developing the seed repository, comment out the branch you will modify.
   BRANCH_SHA = {
-    'empty-branch'          => '7efb185',
-    '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'
+    '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',
+    'conflict-start'                     => '14fa46b',
+    'conflict-resolvable'                => '1450cd6',
+    'conflict-binary-file'               => '259a6fb',
+    'conflict-contains-conflict-markers' => '5e0964c',
+    'conflict-missing-side'              => 'eb227b3',
+    'conflict-too-large'                 => '39fa04f',
   }
 
   # gitlab-test-fork is a fork of gitlab-fork, but we don't necessarily
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/tasks/gitlab/backup_rake_spec.rb b/spec/tasks/gitlab/backup_rake_spec.rb
index d2c056d8e14c0a28d8fc8f006ac005d0925b7348..548e7780c362f5a6d3ba15536749e7ca76643a4f 100644
--- a/spec/tasks/gitlab/backup_rake_spec.rb
+++ b/spec/tasks/gitlab/backup_rake_spec.rb
@@ -42,7 +42,7 @@ describe 'gitlab:app namespace rake task' do
       before do
         allow(Dir).to receive(:glob).and_return([])
         allow(Dir).to receive(:chdir)
-        allow(File).to receive(:exists?).and_return(true)
+        allow(File).to receive(:exist?).and_return(true)
         allow(Kernel).to receive(:system).and_return(true)
         allow(FileUtils).to receive(:cp_r).and_return(true)
         allow(FileUtils).to receive(:mv).and_return(true)
@@ -53,7 +53,7 @@ describe 'gitlab:app namespace rake task' do
 
       let(:gitlab_version) { Gitlab::VERSION }
 
-      it 'should fail on mismatch' do
+      it 'fails on mismatch' do
         allow(YAML).to receive(:load_file).
           and_return({ gitlab_version: "not #{gitlab_version}" })
 
@@ -61,7 +61,7 @@ describe 'gitlab:app namespace rake task' do
           to raise_error(SystemExit)
       end
 
-      it 'should invoke restoration on match' do
+      it 'invokes restoration on match' do
         allow(YAML).to receive(:load_file).
           and_return({ gitlab_version: gitlab_version })
         expect(Rake::Task['gitlab:db:drop_tables']).to receive(:invoke)
@@ -107,7 +107,7 @@ describe 'gitlab:app namespace rake task' do
       end
 
       context 'archive file permissions' do
-        it 'should set correct permissions on the tar file' do
+        it 'sets correct permissions on the tar file' do
           expect(File.exist?(@backup_tar)).to be_truthy
           expect(File::Stat.new(@backup_tar).mode.to_s(8)).to eq('100600')
         end
@@ -127,7 +127,7 @@ describe 'gitlab:app namespace rake task' do
         end
       end
 
-      it 'should set correct permissions on the tar contents' do
+      it 'sets correct permissions on the tar contents' do
         tar_contents, exit_status = Gitlab::Popen.popen(
           %W{tar -tvf #{@backup_tar} db uploads.tar.gz repositories builds.tar.gz artifacts.tar.gz lfs.tar.gz registry.tar.gz}
         )
@@ -142,7 +142,7 @@ describe 'gitlab:app namespace rake task' do
         expect(tar_contents).not_to match(/^.{4,9}[rwx].* (database.sql.gz|uploads.tar.gz|repositories|builds.tar.gz|artifacts.tar.gz|registry.tar.gz)\/$/)
       end
 
-      it 'should delete temp directories' do
+      it 'deletes temp directories' do
         temp_dirs = Dir.glob(
           File.join(Gitlab.config.backup.path, '{db,repositories,uploads,builds,artifacts,lfs,registry}')
         )
@@ -153,7 +153,7 @@ describe 'gitlab:app namespace rake task' do
       context 'registry disabled' do
         let(:enable_registry) { false }
 
-        it 'should not create registry.tar.gz' do
+        it 'does not create registry.tar.gz' do
           tar_contents, exit_status = Gitlab::Popen.popen(
             %W{tar -tvf #{@backup_tar}}
           )
@@ -191,7 +191,7 @@ describe 'gitlab:app namespace rake task' do
         FileUtils.rm(@backup_tar)
       end
 
-      it 'should include repositories in all repository storages' do
+      it 'includes repositories in all repository storages' do
         tar_contents, exit_status = Gitlab::Popen.popen(
           %W{tar -tvf #{@backup_tar} repositories}
         )
diff --git a/spec/tasks/gitlab/db_rake_spec.rb b/spec/tasks/gitlab/db_rake_spec.rb
index 36d03a224e4f212fcd88b2e40c19763a2f368735..fc52c04e78d71385481390cc4c49a9b93c10f4e4 100644
--- a/spec/tasks/gitlab/db_rake_spec.rb
+++ b/spec/tasks/gitlab/db_rake_spec.rb
@@ -19,7 +19,7 @@ describe 'gitlab:db namespace rake task' do
   end
 
   describe 'configure' do
-    it 'should invoke db:migrate when schema has already been loaded' do
+    it 'invokes db:migrate when schema has already been loaded' do
       allow(ActiveRecord::Base.connection).to receive(:tables).and_return(['default'])
       expect(Rake::Task['db:migrate']).to receive(:invoke)
       expect(Rake::Task['db:schema:load']).not_to receive(:invoke)
@@ -27,7 +27,7 @@ describe 'gitlab:db namespace rake task' do
       expect { run_rake_task('gitlab:db:configure') }.not_to raise_error
     end
 
-    it 'should invoke db:shema:load and db:seed_fu when schema is not loaded' do
+    it 'invokes db:shema:load and db:seed_fu when schema is not loaded' do
       allow(ActiveRecord::Base.connection).to receive(:tables).and_return([])
       expect(Rake::Task['db:schema:load']).to receive(:invoke)
       expect(Rake::Task['db:seed_fu']).to receive(:invoke)
@@ -35,7 +35,7 @@ describe 'gitlab:db namespace rake task' do
       expect { run_rake_task('gitlab:db:configure') }.not_to raise_error
     end
 
-    it 'should not invoke any other rake tasks during an error' do
+    it 'does not invoke any other rake tasks during an error' do
       allow(ActiveRecord::Base).to receive(:connection).and_raise(RuntimeError, 'error')
       expect(Rake::Task['db:migrate']).not_to receive(:invoke)
       expect(Rake::Task['db:schema:load']).not_to receive(:invoke)
@@ -45,7 +45,7 @@ describe 'gitlab:db namespace rake task' do
       allow(ActiveRecord::Base).to receive(:connection).and_call_original
     end
 
-    it 'should not invoke seed after a failed schema_load' do
+    it 'does not invoke seed after a failed schema_load' do
       allow(ActiveRecord::Base.connection).to receive(:tables).and_return([])
       allow(Rake::Task['db:schema:load']).to receive(:invoke).and_raise(RuntimeError, 'error')
       expect(Rake::Task['db:schema:load']).to receive(:invoke)
diff --git a/spec/teaspoon_env.rb b/spec/teaspoon_env.rb
index 69b2b9b6d5bf58ff72d1e4ebe177ec0a6f757cc6..5ea020f313c9afeb57f426ed0d0f90655a8d165b 100644
--- a/spec/teaspoon_env.rb
+++ b/spec/teaspoon_env.rb
@@ -38,7 +38,7 @@ Teaspoon.configure do |config|
 
     # Specify a file matcher as a regular expression and all matching files will be loaded when the suite is run. These
     # files need to be within an asset path. You can add asset paths using the `config.asset_paths`.
-    suite.matcher = "{spec/javascripts,app/assets}/**/*_spec.{js,js.coffee,coffee}"
+    suite.matcher = "{spec/javascripts,app/assets}/**/*_spec.{js,js.es6,es6}"
 
     # Load additional JS files, but requiring them in your spec helper is the preferred way to do this.
     # suite.javascripts = []
@@ -149,7 +149,7 @@ Teaspoon.configure do |config|
   # Specify that you always want a coverage configuration to be used. Otherwise, specify that you want coverage
   # on the CLI.
   # Set this to "true" or the name of your coverage config.
-  # config.use_coverage = nil
+  config.use_coverage = true
 
   # You can have multiple coverage configs by passing a name to config.coverage.
   # e.g. config.coverage :ci do |coverage|
@@ -158,15 +158,15 @@ Teaspoon.configure do |config|
     # Which coverage reports Istanbul should generate. Correlates directly to what Istanbul supports.
     #
     # Available: text-summary, text, html, lcov, lcovonly, cobertura, teamcity
-    # coverage.reports = ["text-summary", "html"]
+    coverage.reports = ["text-summary", "html"]
 
     # The path that the coverage should be written to - when there's an artifact to write to disk.
     # Note: Relative to `config.root`.
-    # coverage.output_path = "coverage"
+    coverage.output_path = "coverage-javascript"
 
     # Assets to be ignored when generating coverage reports. Accepts an array of filenames or regular expressions. The
     # default excludes assets from vendor, gems and support libraries.
-    # coverage.ignore = [%r{/lib/ruby/gems/}, %r{/vendor/assets/}, %r{/support/}, %r{/(.+)_helper.}]
+    coverage.ignore = [%r{vendor/}, %r{spec/}]
 
     # Various thresholds requirements can be defined, and those thresholds will be checked at the end of a run. If any
     # aren't met the run will fail with a message. Thresholds can be defined as a percentage (0-100), or nil.
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 05a76ee4bdb426ba55a44af564e0ef5523e8e42e..ee362e6fcb3225a5359ede8ada383cdc50ef06b1 100644
--- a/spec/views/devise/shared/_signin_box.html.haml_spec.rb
+++ b/spec/views/devise/shared/_signin_box.html.haml_spec.rb
@@ -31,7 +31,7 @@ describe 'devise/shared/_signin_box' do
   def enable_crowd
     allow(view).to receive(:form_based_providers).and_return([:crowd])
     allow(view).to receive(:crowd_enabled?).and_return(true)
-    allow(view).to receive(:user_omniauth_authorize_path).with('crowd').
+    allow(view).to receive(:omniauth_authorize_path).with(:user, :crowd).
       and_return('/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/merge_requests/_heading.html.haml_spec.rb b/spec/views/projects/merge_requests/_heading.html.haml_spec.rb
new file mode 100644
index 0000000000000000000000000000000000000000..733b2dfa7ffe9c30e7441b0714fe945112503175
--- /dev/null
+++ b/spec/views/projects/merge_requests/_heading.html.haml_spec.rb
@@ -0,0 +1,26 @@
+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/tree/show.html.haml_spec.rb b/spec/views/projects/tree/show.html.haml_spec.rb
new file mode 100644
index 0000000000000000000000000000000000000000..0f3fc1ee1ac2b2f831cb76ae0d2d00e9fa504444
--- /dev/null
+++ b/spec/views/projects/tree/show.html.haml_spec.rb
@@ -0,0 +1,37 @@
+require 'spec_helper'
+
+describe 'projects/tree/show' do
+  include Devise::TestHelpers
+
+  let(:project) { create(:project) }
+  let(:repository) { project.repository }
+
+  before do
+    assign(:project, project)
+    assign(:repository, repository)
+
+    allow(view).to receive(:can?).and_return(true)
+    allow(view).to receive(:can_collaborate_with_project?).and_return(true)
+  end
+
+  context 'for branch names ending on .json' do
+    let(:ref) { 'ends-with.json' }
+    let(:commit) { repository.commit(ref) }
+    let(:path) { '' }
+    let(:tree) { repository.tree(commit.id, path) }
+
+    before do
+      assign(:ref, ref)
+      assign(:commit, commit)
+      assign(:id, commit.id)
+      assign(:tree, tree)
+      assign(:path, path)
+    end
+
+    it 'displays correctly' do
+      render
+      expect(rendered).to have_css('.js-project-refs-dropdown .dropdown-toggle-text', text: ref)
+      expect(rendered).to have_css('.readme-holder .file-content', text: ref)
+    end
+  end
+end
diff --git a/spec/workers/build_email_worker_spec.rb b/spec/workers/build_email_worker_spec.rb
index 98deae0a588366b27eb5f4ce16e87c7a0be66c85..788b92c1b84e6e36a71e3416fb21797706713297 100644
--- a/spec/workers/build_email_worker_spec.rb
+++ b/spec/workers/build_email_worker_spec.rb
@@ -5,7 +5,7 @@ describe BuildEmailWorker do
 
   let(:build) { create(:ci_build) }
   let(:user) { create(:user) }
-  let(:data) { Gitlab::BuildDataBuilder.build(build) }
+  let(:data) { Gitlab::DataBuilder::Build.build(build) }
 
   subject { BuildEmailWorker.new }
 
diff --git a/spec/workers/emails_on_push_worker_spec.rb b/spec/workers/emails_on_push_worker_spec.rb
index 796751efe8dcf9a1411df00aea63e7d12371fed1..7ca2c29da1c3e26acd7e96257a85d7deb857e9e9 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::PushDataBuilder.build_sample(project, 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,15 +46,40 @@ 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('Change some files')
+      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')
       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
         expect(email.to).to eq([user.email])
       end
@@ -66,6 +89,7 @@ describe EmailsOnPushWorker do
       before do
         ActionMailer::Base.deliveries.clear
         allow(Notify).to receive(:repository_push_email).and_raise(Net::SMTPFatalError)
+        allow(subject).to receive_message_chain(:logger, :info)
         perform
       end
 
diff --git a/spec/workers/group_destroy_worker_spec.rb b/spec/workers/group_destroy_worker_spec.rb
new file mode 100644
index 0000000000000000000000000000000000000000..4e4eaf9b2f71c63c4f9eb1dfe465edad6469ea50
--- /dev/null
+++ b/spec/workers/group_destroy_worker_spec.rb
@@ -0,0 +1,19 @@
+require 'spec_helper'
+
+describe GroupDestroyWorker do
+  let(:group) { create(:group) }
+  let(:user) { create(:admin) }
+  let!(:project) { create(:project, namespace: group) }
+
+  subject { GroupDestroyWorker.new }
+
+  describe "#perform" do
+    it "deletes the project" do
+      subject.perform(group.id, user.id)
+
+      expect(Group.all).not_to include(group)
+      expect(Project.all).not_to include(project)
+      expect(Dir.exist?(project.path)).to be_falsey
+    end
+  end
+end
diff --git a/spec/workers/post_receive_spec.rb b/spec/workers/post_receive_spec.rb
index 20b1a343c27887d7d7121652dc5e3ed3dfd976b6..1d2cf7acddd2ca6dd3bd9aa6465786a62fa71c65 100644
--- a/spec/workers/post_receive_spec.rb
+++ b/spec/workers/post_receive_spec.rb
@@ -22,7 +22,7 @@ describe PostReceive do
     context "branches" do
       let(:changes) { "123456 789012 refs/heads/tést" }
 
-      it "should call GitTagPushService" do
+      it "calls GitTagPushService" do
         expect_any_instance_of(GitPushService).to receive(:execute).and_return(true)
         expect_any_instance_of(GitTagPushService).not_to receive(:execute)
         PostReceive.new.perform(pwd(project), key_id, base64_changes)
@@ -32,7 +32,7 @@ describe PostReceive do
     context "tags" do
       let(:changes) { "123456 789012 refs/tags/tag" }
 
-      it "should call GitTagPushService" do
+      it "calls GitTagPushService" do
         expect_any_instance_of(GitPushService).not_to receive(:execute)
         expect_any_instance_of(GitTagPushService).to receive(:execute).and_return(true)
         PostReceive.new.perform(pwd(project), key_id, base64_changes)
@@ -42,7 +42,7 @@ describe PostReceive do
     context "merge-requests" do
       let(:changes) { "123456 789012 refs/merge-requests/123" }
 
-      it "should not call any of the services" do
+      it "does not call any of the services" do
         expect_any_instance_of(GitPushService).not_to receive(:execute)
         expect_any_instance_of(GitTagPushService).not_to receive(:execute)
         PostReceive.new.perform(pwd(project), key_id, base64_changes)
@@ -53,7 +53,13 @@ describe PostReceive do
       subject { PostReceive.new.perform(pwd(project), key_id, base64_changes) }
 
       context "creates a Ci::Pipeline for every change" do
-        before { stub_ci_pipeline_to_return_yaml_file }
+        before do
+          allow_any_instance_of(Ci::CreatePipelineService).to receive(:commit) do
+            OpenStruct.new(id: '123456')
+          end
+          allow_any_instance_of(Ci::CreatePipelineService).to receive(:branch?).and_return(true)
+          stub_ci_pipeline_to_return_yaml_file
+        end
 
         it { expect{ subject }.to change{ Ci::Pipeline.count }.by(2) }
       end
diff --git a/spec/workers/project_destroy_worker_spec.rb b/spec/workers/project_destroy_worker_spec.rb
new file mode 100644
index 0000000000000000000000000000000000000000..1b910d9b91e90463d3376adcafc969cabb8c0f25
--- /dev/null
+++ b/spec/workers/project_destroy_worker_spec.rb
@@ -0,0 +1,24 @@
+require 'spec_helper'
+
+describe ProjectDestroyWorker do
+  let(:project) { create(:project) }
+  let(:path) { project.repository.path_to_repo }
+
+  subject { ProjectDestroyWorker.new }
+
+  describe "#perform" do
+    it "deletes the project" do
+      subject.perform(project.id, project.owner, {})
+
+      expect(Project.all).not_to include(project)
+      expect(Dir.exist?(path)).to be_falsey
+    end
+
+    it "deletes the project but skips repo deletion" do
+      subject.perform(project.id, project.owner, { "skip_repo" => true })
+
+      expect(Project.all).not_to include(project)
+      expect(Dir.exist?(path)).to be_truthy
+    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/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..eca7c5012b2b1178f4fd942e95676555344ea208
--- /dev/null
+++ b/vendor/assets/javascripts/Sortable.js
@@ -0,0 +1,1285 @@
+/**!
+ * Sortable
+ * @author	RubaXa   <trash@rubaxa.org>
+ * @license MIT
+ */
+
+
+(function (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") {
+		Sortable = factory();  // export for Meteor.js
+	}
+	else {
+		/* jshint sub:true */
+		window["Sortable"] = factory();
+	}
+})(function () {
+	"use strict";
+
+	var dragEl,
+		parentEl,
+		ghostEl,
+		cloneEl,
+		rootEl,
+		nextEl,
+
+		scrollEl,
+		scrollParentEl,
+
+		lastEl,
+		lastCSS,
+		lastParentCSS,
+
+		oldIndex,
+		newIndex,
+
+		activeGroup,
+		autoScroll = {},
+
+		tapEvt,
+		touchEvt,
+
+		moved,
+
+		/** @const */
+		RSPACE = /\s+/g,
+
+		expando = 'Sortable' + (new Date).getTime(),
+
+		win = window,
+		document = win.document,
+		parseInt = win.parseInt,
+
+		supportDraggable = !!('draggable' in document.createElement('div')),
+		supportCssPointerEvents = (function (el) {
+			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
+				;
+
+				// Delect scrollEl
+				if (scrollParentEl !== rootEl) {
+					scrollEl = options.scroll;
+					scrollParentEl = rootEl;
+
+					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 () {
+							if (el === win) {
+								win.scrollTo(win.pageXOffset + vx * speed, win.pageYOffset + vy * speed);
+							} else {
+								vy && (el.scrollTop += vy * speed);
+								vx && (el.scrollLeft += vx * speed);
+							}
+						}, 24);
+					}
+				}
+			}
+		}, 30),
+
+		_prepareGroup = function (options) {
+			var group = options.group;
+
+			if (!group || typeof group != 'object') {
+				group = options.group = {name: group};
+			}
+
+			['pull', 'put'].forEach(function (key) {
+				if (!(key in group)) {
+					group[key] = true;
+				}
+			});
+
+			options.groups = ' ' + group.name + (group.put.join ? ' ' + group.put.join(' ') : '') + ' ';
+		}
+	;
+
+
+
+	/**
+	 * @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',
+			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
+		};
+
+
+		// 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) === '_') {
+				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 = 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
+			}
+
+			target = _closest(target, options.draggable, el);
+
+			if (!target) {
+				return;
+			}
+
+			if (options.handle && !_closest(originalTarget, options.handle, el)) {
+				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;
+
+				dragStartFn = function () {
+					// Delayed drag has been triggered
+					// we can re-enable the events: touchmove/mousemove
+					_this._disableDelayedDrag();
+
+					// Make the element draggable
+					dragEl.draggable = true;
+
+					// Chosen item
+					_toggleClass(dragEl, _this.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) {
+					document.selection.empty();
+				} else {
+					window.getSelection().removeAllRanges();
+				}
+			} catch (err) {
+			}
+		},
+
+		_dragStarted: function () {
+			if (rootEl && dragEl) {
+				// Apply effect
+				_toggleClass(dragEl, this.options.ghostClass, true);
+
+				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,
+					groupName = ' ' + this.options.group.name + '',
+					i = touchDragOverListeners.length;
+
+				if (parent) {
+					do {
+						if (parent[expando] && parent[expando].options.groups.indexOf(groupName) > -1) {
+							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,
+					touch = evt.touches ? evt.touches[0] : evt,
+					dx = touch.clientX - tapEvt.clientX,
+					dy = touch.clientY - tapEvt.clientY,
+					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);
+
+				_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.pull == 'clone') {
+				cloneEl = dragEl.cloneNode(true);
+				_css(cloneEl, 'display', 'none');
+				rootEl.insertBefore(cloneEl, dragEl);
+				_dispatchEvent(this, rootEl, 'clone', dragEl);
+			}
+
+			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,
+				revert,
+				options = this.options,
+				group = options.group,
+				groupPut = group.put,
+				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
+					: activeGroup.pull && groupPut && (
+						(activeGroup.name === group.name) || // by Name
+						(groupPut.indexOf && ~groupPut.indexOf(activeGroup.name)) // by Array
+					)
+				) &&
+				(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();
+
+				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) !== 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);
+					}
+
+
+					var targetRect = target.getBoundingClientRect(),
+						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),
+						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);
+
+					// 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) {
+							// drag from one list and drop into another
+							_dispatchEvent(null, parentEl, 'sort', dragEl, rootEl, oldIndex, newIndex);
+							_dispatchEvent(this, rootEl, 'sort', dragEl, rootEl, oldIndex, newIndex);
+
+							// Add event
+							_dispatchEvent(null, parentEl, 'add', dragEl, rootEl, oldIndex, newIndex);
+
+							// Remove event
+							_dispatchEvent(this, rootEl, 'remove', 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) {
+						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 =
+
+			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;
+				}
+			}
+			while (el !== ctx && (el = el.parentNode));
+		}
+
+		return null;
+	}
+
+
+	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) {
+		var evt = document.createEvent('Event'),
+			options = (sortable || rootEl[expando]).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) {
+		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);
+		}
+
+		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();
+
+		return ((evt.clientY - (rect.top + rect.height) > 5) || (evt.clientX - (rect.right + rect.width) > 5)) && lastEl; // min delta
+	}
+
+
+	/**
+	 * 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;
+	}
+
+
+	// 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,
+		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.scrollTo.js b/vendor/assets/javascripts/jquery.scrollTo.js
old mode 100755
new mode 100644
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/Elm.gitignore b/vendor/gitignore/Elm.gitignore
index a594364e2c02e00643c10a556d507a7cef4afe4a..8b631e7de00937af125d4f143a50fb67c0c8c24c 100644
--- a/vendor/gitignore/Elm.gitignore
+++ b/vendor/gitignore/Elm.gitignore
@@ -1,4 +1,4 @@
 # elm-package generated files
-elm-stuff/
+elm-stuff
 # elm-repl generated files
 repl-temp-*
diff --git a/vendor/gitignore/Global/VisualStudioCode.gitignore b/vendor/gitignore/Global/VisualStudioCode.gitignore
index faa18382a3c9eeeea55949156e562c33220f7148..d9960081c9820bd3ac26d6d798e18191bafba5f2 100644
--- a/vendor/gitignore/Global/VisualStudioCode.gitignore
+++ b/vendor/gitignore/Global/VisualStudioCode.gitignore
@@ -1,2 +1,4 @@
-.vscode
-
+.vscode/*
+!.vscode/settings.json
+!.vscode/tasks.json
+!.vscode/launch.json
diff --git a/vendor/gitignore/Go.gitignore b/vendor/gitignore/Go.gitignore
index daf913b1b347aae6de6f48d599bc89ef8c8693d6..cd0d5d1e2f4c7647a1bcca8b8927242c3831e6fb 100644
--- a/vendor/gitignore/Go.gitignore
+++ b/vendor/gitignore/Go.gitignore
@@ -22,3 +22,6 @@ _testmain.go
 *.exe
 *.test
 *.prof
+
+# Output of the go coverage tool, specifically when used with LiteIDE
+*.out
diff --git a/vendor/gitignore/Leiningen.gitignore b/vendor/gitignore/Leiningen.gitignore
index 47fed6c20d9b87dfe78dc4817e198584cfa5f947..a9fe6fba80d90b63aee464ecfd89ea59f697c6e1 100644
--- a/vendor/gitignore/Leiningen.gitignore
+++ b/vendor/gitignore/Leiningen.gitignore
@@ -1,6 +1,7 @@
 pom.xml
 pom.xml.asc
-*jar
+*.jar
+*.class
 /lib/
 /classes/
 /target/
diff --git a/vendor/gitignore/Objective-C.gitignore b/vendor/gitignore/Objective-C.gitignore
index 86f21d8e0ff7635d06025bddcae1cee3d7cd378f..20592083931a5924e8891b6e9916bd3b51b062bd 100644
--- a/vendor/gitignore/Objective-C.gitignore
+++ b/vendor/gitignore/Objective-C.gitignore
@@ -52,7 +52,7 @@ Carthage/Build
 fastlane/report.xml
 fastlane/screenshots
 
-#Code Injection
+# Code Injection
 #
 # After new code Injection tools there's a generated folder /iOSInjectionProject
 # https://github.com/johnno1962/injectionforxcode
diff --git a/vendor/gitignore/Scala.gitignore b/vendor/gitignore/Scala.gitignore
index c58d83b318909082a44d3a84222b74607d1268f8..a02d882cb88407d93f96cc647944b17408dcec7f 100644
--- a/vendor/gitignore/Scala.gitignore
+++ b/vendor/gitignore/Scala.gitignore
@@ -15,3 +15,7 @@ project/plugins/project/
 # Scala-IDE specific
 .scala_dependencies
 .worksheet
+
+# ENSIME specific
+.ensime_cache/
+.ensime
diff --git a/vendor/gitignore/SugarCRM.gitignore b/vendor/gitignore/SugarCRM.gitignore
index 842c3ec518bf64ba78621449b076c919b4d5d98c..e9270205fd565f86bb2c667f0b82281b24cfaff1 100644
--- a/vendor/gitignore/SugarCRM.gitignore
+++ b/vendor/gitignore/SugarCRM.gitignore
@@ -7,6 +7,7 @@
 # For development the cache directory can be safely ignored and
 # therefore it is ignored.
 /cache/
+!/cache/index.html
 # Ignore some files and directories from the custom directory.
 /custom/history/
 /custom/modulebuilder/
@@ -22,4 +23,5 @@
 *.log
 # Ignore the new upload directories.
 /upload/
+!/upload/index.html
 /upload_backup/
diff --git a/vendor/gitignore/TeX.gitignore b/vendor/gitignore/TeX.gitignore
index 3cb097c9d5e5d3cb7b63393d7e1ddb8b999b2e28..34f999df3e7e645e84f4dcea7e19ee84cf17cc31 100644
--- a/vendor/gitignore/TeX.gitignore
+++ b/vendor/gitignore/TeX.gitignore
@@ -19,6 +19,9 @@
 # *.eps
 # *.pdf
 
+## Generated if empty string is given at "Please type another file name for output:"
+.pdf
+
 ## Bibliography auxiliary files (bibtex/biblatex/biber):
 *.bbl
 *.bcf
@@ -31,6 +34,7 @@
 ## Build tool auxiliary files:
 *.fdb_latexmk
 *.synctex
+*.synctex(busy)
 *.synctex.gz
 *.synctex.gz(busy)
 *.pdfsync
@@ -84,6 +88,10 @@ acs-*.bib
 # gnuplottex
 *-gnuplottex-*
 
+# gregoriotex
+*.gaux
+*.gtex
+
 # hyperref
 *.brf
 
@@ -128,6 +136,9 @@ _minted*
 *.sagetex.py
 *.sagetex.scmd
 
+# scrwfile
+*.wrt
+
 # sympy
 *.sout
 *.sympy
diff --git a/vendor/gitignore/Terraform.gitignore b/vendor/gitignore/Terraform.gitignore
index 7868d16d216f4aefc9a5ae335451af508629402f..41859c81f1c272fa7acf5f2bf5d67e1cb20cd3df 100644
--- a/vendor/gitignore/Terraform.gitignore
+++ b/vendor/gitignore/Terraform.gitignore
@@ -1,3 +1,6 @@
 # Compiled files
 *.tfstate
 *.tfstate.backup
+
+# Module directory
+.terraform/
diff --git a/vendor/gitignore/Unity.gitignore b/vendor/gitignore/Unity.gitignore
index 5aafcbb7f1d4b3f02e1c63c1ce1693aa2b5a9cc8..1c10388911b354f39ac498923ad7c1ea5925fdbf 100644
--- a/vendor/gitignore/Unity.gitignore
+++ b/vendor/gitignore/Unity.gitignore
@@ -5,8 +5,9 @@
 /[Bb]uilds/
 /Assets/AssetStoreTools*
 
-# Autogenerated VS/MD solution and project files
+# Autogenerated VS/MD/Consulo solution and project files
 ExportedObj/
+.consulo/
 *.csproj
 *.unityproj
 *.sln
diff --git a/vendor/gitlab-ci-yml/C++.gitlab-ci.yml b/vendor/gitlab-ci-yml/C++.gitlab-ci.yml
new file mode 100644
index 0000000000000000000000000000000000000000..c83c49d8c950314bc5a40fa1e5080247f3307656
--- /dev/null
+++ b/vendor/gitlab-ci-yml/C++.gitlab-ci.yml
@@ -0,0 +1,26 @@
+# use the official gcc image, based on debian
+# can use verions as well, like gcc:5.2
+# see https://hub.docker.com/_/gcc/
+image: gcc
+
+build:
+  stage: build
+  # instead of calling g++ directly you can also use some build toolkit like make
+  # install the necessary build tools when needed
+  # before_script: 
+  #   - apt update && apt -y install make autoconf 
+  script: 
+    - g++ helloworld.cpp -o mybinary
+  artifacts:
+    paths:
+      - mybinary
+  # depending on your build setup it's most likely a good idea to cache outputs to reduce the build time
+  # cache:
+  #   paths:
+  #     - "*.o"
+
+# run tests using the binary built before
+test:
+  stage: test
+  script:
+    - ./runmytests.sh
diff --git a/vendor/gitlab-ci-yml/Grails.gitlab-ci.yml b/vendor/gitlab-ci-yml/Grails.gitlab-ci.yml
new file mode 100644
index 0000000000000000000000000000000000000000..7fc698d50cf62833219efe9e1e649cbc7ab73dca
--- /dev/null
+++ b/vendor/gitlab-ci-yml/Grails.gitlab-ci.yml
@@ -0,0 +1,40 @@
+# This template uses the java:8 docker image because there isn't any
+# official Grails image at this moment
+#
+# Grails Framework https://grails.org/ is a powerful Groovy-based web application framework for the JVM
+#
+# This yml works with Grails 3.x only
+# Feel free to change GRAILS_VERSION version with your project version (3.0.1, 3.1.1,...)
+# Feel free to change GRADLE_VERSION version with your gradle project version (2.13, 2.14,...)
+# If you use Angular profile, this yml it's prepared to work with it
+
+image: java:8
+
+variables:
+  GRAILS_VERSION: "3.1.9"
+  GRADLE_VERSION: "2.13"
+  
+# We use SDKMan as tool for managing versions
+before_script:
+   - apt-get update -qq && apt-get install -y -qq unzip
+   - curl -sSL https://get.sdkman.io | bash
+   - echo sdkman_auto_answer=true > /root/.sdkman/etc/config
+   - source /root/.sdkman/bin/sdkman-init.sh
+   - sdk install gradle $GRADLE_VERSION < /dev/null
+   - sdk use gradle $GRADLE_VERSION
+# As it's not a good idea to version gradle.properties feel free to add your
+# environments variable here   
+   - echo grailsVersion=$GRAILS_VERSION > gradle.properties
+   - echo gradleWrapperVersion=2.14 >> gradle.properties
+# refresh dependencies from your project   
+   - ./gradlew --refresh-dependencies
+# Be aware that if you are using Angular profile,
+# Bower cannot be run as root if you don't allow it before.
+# Feel free to remove next line if you are not using Bower
+   - echo {\"allow_root\":true} > /root/.bowerrc
+
+# This build job does the full grails pipeline
+# (compile, test, integrationTest, war, assemble).
+build:
+ script: 
+   - ./gradlew build
\ No newline at end of file
diff --git a/vendor/gitlab-ci-yml/LaTeX.gitlab-ci.yml b/vendor/gitlab-ci-yml/LaTeX.gitlab-ci.yml
new file mode 100644
index 0000000000000000000000000000000000000000..a4aed36889ee1995c14ef30273fedc8d3a680fff
--- /dev/null
+++ b/vendor/gitlab-ci-yml/LaTeX.gitlab-ci.yml
@@ -0,0 +1,11 @@
+# use docker image with latex preinstalled
+# since there is no official latex image, use https://github.com/blang/latex-docker
+# possible alternative: https://github.com/natlownes/docker-latex
+image: blang/latex
+
+build:
+  script:
+    - latexmk -pdf
+  artifacts:
+    paths:
+      - "*.pdf"
diff --git a/vendor/gitlab-ci-yml/Pages/Hexo.gitlab-ci.yml b/vendor/gitlab-ci-yml/Pages/Hexo.gitlab-ci.yml
index b468d79bcad0ad3d04d70bc96d4f24a06c6cdeff..908463c9d1292fe319759467ea32c52bbfe936cb 100644
--- a/vendor/gitlab-ci-yml/Pages/Hexo.gitlab-ci.yml
+++ b/vendor/gitlab-ci-yml/Pages/Hexo.gitlab-ci.yml
@@ -1,25 +1,17 @@
 # Full project: https://gitlab.com/pages/hexo
-image: python:2.7
-
-cache:
-  paths:
-  - vendor/
-
-test:
-  stage: test
-  script:
-    - pip install hyde
-    - hyde gen
-  except:
-    - master
+image: node:4.2.2
 
 pages:
-  stage: deploy
+  cache:
+    paths:
+    - node_modules/
+
   script:
-    - pip install hyde
-    - hyde gen -d public
+  - npm install hexo-cli -g
+  - npm install
+  - hexo deploy
   artifacts:
     paths:
     - public
   only:
-    - master
+  - master
diff --git a/vendor/gitlab-ci-yml/Pages/JBake.gitlab-ci.yml b/vendor/gitlab-ci-yml/Pages/JBake.gitlab-ci.yml
new file mode 100644
index 0000000000000000000000000000000000000000..bc36a4e6966c5d47955cb7cb2bf03dada3327bac
--- /dev/null
+++ b/vendor/gitlab-ci-yml/Pages/JBake.gitlab-ci.yml
@@ -0,0 +1,32 @@
+# This template uses the java:8 docker image because there isn't any
+# official JBake image at this moment
+#
+# JBake https://jbake.org/ is a Java based, open source, static site/blog generator for developers & designers
+#
+# This yml works with jBake 2.4.0
+# Feel free to change JBAKE_VERSION version 
+#
+# HowTo at: https://jorge.aguilera.gitlab.io/howtojbake/
+
+image: java:8
+
+variables:
+    JBAKE_VERSION: 2.4.0
+
+
+# We use SDKMan as tool for managing versions
+before_script:
+   - apt-get update -qq && apt-get install -y -qq unzip
+   - curl -sSL https://get.sdkman.io | bash
+   - echo sdkman_auto_answer=true > /root/.sdkman/etc/config
+   - source /root/.sdkman/bin/sdkman-init.sh
+   - sdk install jbake $JBAKE_VERSION < /dev/null
+   - sdk use jbake $JBAKE_VERSION
+
+# This build job produced the output directory of your site
+pages:
+ script:
+ - jbake . public
+ artifacts:
+   paths:
+   - public
\ No newline at end of file
diff --git a/vendor/gitlab-ci-yml/Ruby.gitlab-ci.yml b/vendor/gitlab-ci-yml/Ruby.gitlab-ci.yml
index 2a761bbd127e3469f89c4066de985f4f2f09c12d..166f146ee05e1a2c33ade906499ac5dd2de8b0be 100644
--- a/vendor/gitlab-ci-yml/Ruby.gitlab-ci.yml
+++ b/vendor/gitlab-ci-yml/Ruby.gitlab-ci.yml
@@ -10,6 +10,9 @@ services:
   - redis:latest
   - postgres:latest
 
+variables:
+  POSTGRES_DB: database_name
+
 # Cache gems in between builds
 cache:
   paths:
@@ -19,6 +22,8 @@ cache:
 # services such as redis or postgres
 before_script:
   - ruby -v                                   # Print out ruby version for debugging
+  # Uncomment next line if your rails app needs a JS runtime:
+  # - apt-get update -q && apt-get install nodejs -yqq
   - gem install bundler  --no-ri --no-rdoc    # Bundler is not installed with the image
   - bundle install -j $(nproc) --path vendor  # Install dependencies into ./vendor/ruby
 
@@ -32,6 +37,9 @@ rspec:
   - rspec spec
 
 rails:
+  variables:
+    DATABASE_URL: "postgresql://postgres:postgres@postgres:5432/$POSTGRES_DB"
   script:
   - bundle exec rake db:migrate
+  - bundle exec rake db:seed
   - bundle exec rake test