diff --git a/app/assets/javascripts/notes.js b/app/assets/javascripts/notes.js
index 47fa0f2eb96fb13e0519fa2da521887a6a91354f..df7a7d2a459f64cb5ab106ac8e08835717d4dec8 100644
--- a/app/assets/javascripts/notes.js
+++ b/app/assets/javascripts/notes.js
@@ -198,7 +198,7 @@ require('./task_list');
       this.refreshing = true;
       return $.ajax({
         url: this.notes_url,
-        data: "last_fetched_at=" + this.last_fetched_at,
+        headers: { "X-Last-Fetched-At": this.last_fetched_at },
         dataType: "json",
         success: (function(_this) {
           return function(data) {
diff --git a/app/assets/stylesheets/framework/layout.scss b/app/assets/stylesheets/framework/layout.scss
index 29d55c446992e3b5a11ac50aecc4f9784b64651d..0a42b17c1f5519b829833d132bb33715d5320c08 100644
--- a/app/assets/stylesheets/framework/layout.scss
+++ b/app/assets/stylesheets/framework/layout.scss
@@ -8,6 +8,19 @@ body {
   &.navless {
     background-color: $white-light !important;
   }
+
+  &.card-content {
+    background-color: $gray-darker;
+
+    .content-wrapper {
+      padding: 0;
+
+      .container-fluid,
+      .container-limited {
+        background-color: $gray-darker;
+      }
+    }
+  }
 }
 
 .container {
diff --git a/app/controllers/profiles/keys_controller.rb b/app/controllers/profiles/keys_controller.rb
index c8663a3c38ee271d4080364e7b54223f1e19825d..e4452f46056bf21781f31040206b53668484e8a8 100644
--- a/app/controllers/profiles/keys_controller.rb
+++ b/app/controllers/profiles/keys_controller.rb
@@ -10,11 +10,6 @@ class Profiles::KeysController < Profiles::ApplicationController
     @key = current_user.keys.find(params[:id])
   end
 
-  # Back-compat: We need to support this URL since git-annex webapp points to it
-  def new
-    redirect_to profile_keys_path
-  end
-
   def create
     @key = current_user.keys.new(key_params)
 
diff --git a/app/controllers/profiles_controller.rb b/app/controllers/profiles_controller.rb
index f0c71725ea8c851a66c38bc209472f9657f8fa39..987b95e89b9e6ead5657ccdddd07a96888d155a7 100644
--- a/app/controllers/profiles_controller.rb
+++ b/app/controllers/profiles_controller.rb
@@ -47,11 +47,14 @@ class ProfilesController < Profiles::ApplicationController
   end
 
   def update_username
-    @user.update_attributes(username: user_params[:username])
-
-    respond_to do |format|
-      format.js
+    if @user.update_attributes(username: user_params[:username])
+      options = { notice: "Username successfully changed" }
+    else
+      message = @user.errors.full_messages.uniq.join('. ')
+      options = { alert: "Username change failed - #{message}" }
     end
+
+    redirect_back_or_default(default: { action: 'show' }, options: options)
   end
 
   private
diff --git a/app/controllers/projects/branches_controller.rb b/app/controllers/projects/branches_controller.rb
index a01c0caa959c8c7865b29b3c75bf4aef197d8955..c40f9b7f75f3ea375b386269e64d921dfd0a5053 100644
--- a/app/controllers/projects/branches_controller.rb
+++ b/app/controllers/projects/branches_controller.rb
@@ -20,7 +20,7 @@ class Projects::BranchesController < Projects::ApplicationController
     respond_to do |format|
       format.html
       format.json do
-        render json: @repository.branch_names
+        render json: @branches.map(&:name)
       end
     end
   end
diff --git a/app/controllers/projects/notes_controller.rb b/app/controllers/projects/notes_controller.rb
index 5cf3a7f593b53f0ee11dcaa30f9743cc42db4de4..d00177e7612fb3b1a371ada907396ce4b79876b9 100644
--- a/app/controllers/projects/notes_controller.rb
+++ b/app/controllers/projects/notes_controller.rb
@@ -211,6 +211,11 @@ class Projects::NotesController < Projects::ApplicationController
   end
 
   def find_current_user_notes
-    @notes = NotesFinder.new(project, current_user, params).execute.inc_author
+    @notes = NotesFinder.new(project, current_user, params.merge(last_fetched_at: last_fetched_at))
+      .execute.inc_author
+  end
+
+  def last_fetched_at
+    request.headers['X-Last-Fetched-At']
   end
 end
diff --git a/app/helpers/mattermost_helper.rb b/app/helpers/mattermost_helper.rb
index 49ac12db832827dc1e4561be3691298aee0b2754..27ff4051c8d2025f77f779b27533bd19d0e807d5 100644
--- a/app/helpers/mattermost_helper.rb
+++ b/app/helpers/mattermost_helper.rb
@@ -1,9 +1,7 @@
 module MattermostHelper
   def mattermost_teams_options(teams)
-    teams_options = teams.map do |id, options|
-      [options['display_name'] || options['name'], id]
+    teams.map do |team|
+      [team['display_name'] || team['name'], team['id']]
     end
-
-    teams_options.compact.unshift(['Select team...', '0'])
   end
 end
diff --git a/app/helpers/triggers_helper.rb b/app/helpers/triggers_helper.rb
index b0135ea2e95696897a99c566ebf0bc5abb22e953..a48d4475e979cde0f4e8035a360ebe4217decd6b 100644
--- a/app/helpers/triggers_helper.rb
+++ b/app/helpers/triggers_helper.rb
@@ -1,9 +1,9 @@
 module TriggersHelper
   def builds_trigger_url(project_id, ref: nil)
     if ref.nil?
-      "#{Settings.gitlab.url}/api/v3/projects/#{project_id}/trigger/builds"
+      "#{Settings.gitlab.url}/api/v4/projects/#{project_id}/trigger/pipeline"
     else
-      "#{Settings.gitlab.url}/api/v3/projects/#{project_id}/ref/#{ref}/trigger/builds"
+      "#{Settings.gitlab.url}/api/v4/projects/#{project_id}/ref/#{ref}/trigger/pipeline"
     end
   end
 
diff --git a/app/models/ci/trigger.rb b/app/models/ci/trigger.rb
index 39a1dd86241ab2a4fdd358751c71985211d198cc..8aa45b2f02e633c8c3bcb4b2af4096eaec4bcd66 100644
--- a/app/models/ci/trigger.rb
+++ b/app/models/ci/trigger.rb
@@ -5,10 +5,11 @@ module Ci
     acts_as_paranoid
 
     belongs_to :project, foreign_key: :gl_project_id
+    belongs_to :owner, class_name: "User"
+
     has_many :trigger_requests, dependent: :destroy
 
-    validates :token, presence: true
-    validates :token, uniqueness: true
+    validates :token, presence: true, uniqueness: true
 
     before_validation :set_default_values
 
@@ -25,7 +26,11 @@ module Ci
     end
 
     def short_token
-      token[0...10]
+      token[0...4]
+    end
+
+    def can_show_token?(user)
+      owner.blank? || owner == user
     end
   end
 end
diff --git a/app/models/note.rb b/app/models/note.rb
index 4c97e4a986c580a2a916792dc6da491987166f80..e22e96aec6fe869a42c4a035b2c53b782071be3c 100644
--- a/app/models/note.rb
+++ b/app/models/note.rb
@@ -85,6 +85,7 @@ class Note < ActiveRecord::Base
   before_validation :nullify_blank_type, :nullify_blank_line_code
   before_validation :set_discussion_id
   after_save :keep_around_commit, unless: :for_personal_snippet?
+  after_save :expire_etag_cache
 
   class << self
     def model_name
@@ -272,4 +273,16 @@ class Note < ActiveRecord::Base
       self.class.build_discussion_id(noteable_type, noteable_id || commit_id)
     end
   end
+
+  def expire_etag_cache
+    return unless for_issue?
+
+    key = Gitlab::Routing.url_helpers.namespace_project_noteable_notes_path(
+      noteable.project.namespace,
+      noteable.project,
+      target_type: noteable_type.underscore,
+      target_id: noteable.id
+    )
+    Gitlab::EtagCaching::Store.new.touch(key)
+  end
 end
diff --git a/app/models/user.rb b/app/models/user.rb
index d3bb04060bfa77cbde63b8fd41b4df31064fc248..dfba51d3b00abeade8839ec76cede07199edaff7 100644
--- a/app/models/user.rb
+++ b/app/models/user.rb
@@ -95,6 +95,7 @@ class User < ActiveRecord::Base
   has_many :todos,                    dependent: :destroy
   has_many :notification_settings,    dependent: :destroy
   has_many :award_emoji,              dependent: :destroy
+  has_many :triggers,                 dependent: :destroy, class_name: 'Ci::Trigger', foreign_key: :owner_id
 
   has_many :assigned_issues,          dependent: :nullify, foreign_key: :assignee_id, class_name: "Issue"
   has_many :assigned_merge_requests,  dependent: :nullify, foreign_key: :assignee_id, class_name: "MergeRequest"
diff --git a/app/services/ci/create_trigger_request_service.rb b/app/services/ci/create_trigger_request_service.rb
index 6af3c1ca5b130d304bbb7b06f44f3877d931034a..dca5aa9f5d70260ae363640a903b2e0f91d9ac92 100644
--- a/app/services/ci/create_trigger_request_service.rb
+++ b/app/services/ci/create_trigger_request_service.rb
@@ -3,7 +3,7 @@ module Ci
     def execute(project, trigger, ref, variables = nil)
       trigger_request = trigger.trigger_requests.create(variables: variables)
 
-      pipeline = Ci::CreatePipelineService.new(project, nil, ref: ref).
+      pipeline = Ci::CreatePipelineService.new(project, trigger.owner, ref: ref).
         execute(ignore_skip_ci: true, trigger_request: trigger_request)
       if pipeline.persisted?
         trigger_request
diff --git a/app/services/projects/update_pages_service.rb b/app/services/projects/update_pages_service.rb
index 2d42c4fc04a74961e3ed88c1a043058975033024..523b9f4191642fb7737239fc9f8a3b3b02bd9655 100644
--- a/app/services/projects/update_pages_service.rb
+++ b/app/services/projects/update_pages_service.rb
@@ -34,6 +34,8 @@ module Projects
       end
     rescue => e
       error(e.message)
+    ensure
+      build.erase_artifacts! unless build.has_expiring_artifacts?
     end
 
     private
diff --git a/app/views/layouts/application.html.haml b/app/views/layouts/application.html.haml
index 19bd9b6d5c99ef7ba47aa938ff83396362be1040..36543edc040f91e562494ecea3a5a441a7279b2d 100644
--- a/app/views/layouts/application.html.haml
+++ b/app/views/layouts/application.html.haml
@@ -1,7 +1,7 @@
 !!! 5
 %html{ lang: "en", class: "#{page_class}" }
   = render "layouts/head"
-  %body{ data: { page: body_data_page, project: "#{@project.path if @project}", group: "#{@group.path if @group}" } }
+  %body{ class: @body_class, data: { page: body_data_page, project: "#{@project.path if @project}", group: "#{@group.path if @group}" } }
     = Gon::Base.render_data
 
     = render "layouts/header/default", title: header_title
diff --git a/app/views/profiles/accounts/show.html.haml b/app/views/profiles/accounts/show.html.haml
index 02fb47ec9819ce31ba9b501fd1dd7064998843f6..8a994f6d600fe220e1aff1bc771e8fa65592f727 100644
--- a/app/views/profiles/accounts/show.html.haml
+++ b/app/views/profiles/accounts/show.html.haml
@@ -93,7 +93,7 @@
       %p
         Changing your username will change path to all personal projects!
     .col-lg-9
-      = form_for @user, url: update_username_profile_path, method: :put, remote: true, html: {class: "update-username"} do |f|
+      = form_for @user, url: update_username_profile_path, method: :put, html: {class: "update-username"} do |f|
         .form-group
           = f.label :username, "Path", class: "label-light"
           .input-group
diff --git a/app/views/profiles/update_username.js.haml b/app/views/profiles/update_username.js.haml
deleted file mode 100644
index 5307e0b48cb8eb5cbbc8c558d2e13d9047688293..0000000000000000000000000000000000000000
--- a/app/views/profiles/update_username.js.haml
+++ /dev/null
@@ -1,7 +0,0 @@
-- if @user.valid?
-  :plain
-    new Flash("Username successfully changed", "notice")
-- else
-  - error = @user.errors.full_messages.first
-  :plain
-    new Flash("Username change failed - #{escape_javascript error.html_safe}", "alert")
diff --git a/app/views/projects/mattermosts/_team_selection.html.haml b/app/views/projects/mattermosts/_team_selection.html.haml
index a80f9aa4c4a87de70a58a7394bf12b1607053574..04bd4e8b6830e8f18c392dc67254f9bcc07c83dc 100644
--- a/app/views/projects/mattermosts/_team_selection.html.haml
+++ b/app/views/projects/mattermosts/_team_selection.html.haml
@@ -2,16 +2,15 @@
   This service will be installed on the Mattermost instance at
   %strong= link_to Gitlab.config.mattermost.host, Gitlab.config.mattermost.host
 %hr
-= form_for(:mattermost, method: :post, url: namespace_project_mattermost_path(@project.namespace, @project)) do |f|
+= form_for(:mattermost, method: :post, url: namespace_project_mattermost_path(@project.namespace, @project), html: { class: 'js-requires-input'} ) do |f|
   %h4 Team
   %p
     = @teams.one? ? 'The team' : 'Select the team'
     where the slash commands will be used in
-  - selected_id = @teams.one? ? @teams.keys.first : 0
-  - options = mattermost_teams_options(@teams)
-  - options = options_for_select(options, selected_id)
-  = f.select(:team_id, options, {}, { class: 'form-control', disabled: @teams.one?, selected: selected_id })
-  = f.hidden_field(:team_id, value: selected_id) if @teams.one?
+  - selected_id = @teams.one? ? @teams.first['id'] : nil
+  - options = options_for_select(mattermost_teams_options(@teams), selected_id)
+  = f.select(:team_id, options, { include_blank: 'Select team...'}, { class: 'form-control', disabled: @teams.one?, selected: selected_id, required: true })
+  = f.hidden_field(:team_id, value: selected_id, required: true) if @teams.one?
   .help-block
     - if @teams.one?
       This is the only available team.
@@ -25,7 +24,7 @@
   %hr
   %h4 Command trigger word
   %p Choose the word that will trigger commands
-  = f.text_field(:trigger, value: @project.path, class: 'form-control')
+  = f.text_field(:trigger, value: @project.path, class: 'form-control', required: true)
   .help-block
     %p
       Trigger word must be unique, and can't begin with a slash or contain any spaces.
diff --git a/app/views/projects/mattermosts/new.html.haml b/app/views/projects/mattermosts/new.html.haml
index 96b1d2aee61dd72fdaf213af327d565bc29ed59d..15829a3f143548c3bc8f0bc6a11bc12c584a642f 100644
--- a/app/views/projects/mattermosts/new.html.haml
+++ b/app/views/projects/mattermosts/new.html.haml
@@ -1,3 +1,5 @@
+- @body_class = 'card-content'
+
 .service-installation
   .inline.pull-right
     = custom_icon('mattermost_logo', size: 48)
diff --git a/app/views/projects/notes/_notes_with_form.html.haml b/app/views/projects/notes/_notes_with_form.html.haml
index 08c73d94a0997172e79a48a15820ba4892223d51..90a150aa74c7941f8e486a7606dc0ebb8d122673 100644
--- a/app/views/projects/notes/_notes_with_form.html.haml
+++ b/app/views/projects/notes/_notes_with_form.html.haml
@@ -23,4 +23,4 @@
           to post a comment
 
 :javascript
-  var notes = new Notes("#{namespace_project_notes_path(namespace_id: @project.namespace, project_id: @project, target_id: @noteable.id, target_type: @noteable.class.name.underscore)}", #{@notes.map(&:id).to_json}, #{Time.now.to_i}, "#{diff_view}")
+  var notes = new Notes("#{namespace_project_noteable_notes_path(namespace_id: @project.namespace, project_id: @project, target_id: @noteable.id, target_type: @noteable.class.name.underscore)}", #{@notes.map(&:id).to_json}, #{Time.now.to_i}, "#{diff_view}")
diff --git a/changelogs/unreleased/1648-remove-remnants-of-git-annex-from-ce.yml b/changelogs/unreleased/1648-remove-remnants-of-git-annex-from-ce.yml
new file mode 100644
index 0000000000000000000000000000000000000000..f247fe35439ecff99ca2ec343b8ac28fbae978cc
--- /dev/null
+++ b/changelogs/unreleased/1648-remove-remnants-of-git-annex-from-ce.yml
@@ -0,0 +1,4 @@
+---
+title: Remove remnants of git annex support.
+merge_request:
+author:
diff --git a/changelogs/unreleased/28655-current-path-text-is-not-updated-after-setting-the-new-username.yml b/changelogs/unreleased/28655-current-path-text-is-not-updated-after-setting-the-new-username.yml
new file mode 100644
index 0000000000000000000000000000000000000000..bff996172f33d9bc4fe21baa4d82efb1b5f3620a
--- /dev/null
+++ b/changelogs/unreleased/28655-current-path-text-is-not-updated-after-setting-the-new-username.yml
@@ -0,0 +1,4 @@
+---
+title: Update account view to display new username
+merge_request:
+author:
diff --git a/changelogs/unreleased/28898-fix-search-branches-in-cherry-picking.yml b/changelogs/unreleased/28898-fix-search-branches-in-cherry-picking.yml
new file mode 100644
index 0000000000000000000000000000000000000000..48e62f8f70dc83165d2652ab89b6748d64a5d9e6
--- /dev/null
+++ b/changelogs/unreleased/28898-fix-search-branches-in-cherry-picking.yml
@@ -0,0 +1,4 @@
+---
+title: Fix json response in branches controller
+merge_request: 9710
+author: George Andrinopoulos
diff --git a/changelogs/unreleased/delete-artifacts-for-pages.yml b/changelogs/unreleased/delete-artifacts-for-pages.yml
new file mode 100644
index 0000000000000000000000000000000000000000..50b3dd81d600dab4e5bac1ea2d708fbd38fb29fc
--- /dev/null
+++ b/changelogs/unreleased/delete-artifacts-for-pages.yml
@@ -0,0 +1,4 @@
+---
+title: Delete artifacts for pages unless expiry date is specified
+merge_request: 9716
+author:
diff --git a/changelogs/unreleased/etag-notes-polling.yml b/changelogs/unreleased/etag-notes-polling.yml
new file mode 100644
index 0000000000000000000000000000000000000000..53990821d253748be50cd26e27f0ad7c8aecf675
--- /dev/null
+++ b/changelogs/unreleased/etag-notes-polling.yml
@@ -0,0 +1,4 @@
+---
+title: Use ETag to improve performance of issue notes polling
+merge_request: 9036
+author:
diff --git a/changelogs/unreleased/introduce-pipeline-triggers.yml b/changelogs/unreleased/introduce-pipeline-triggers.yml
new file mode 100644
index 0000000000000000000000000000000000000000..ce5a230d48f51d133c79a6ad38c61689e92a85e8
--- /dev/null
+++ b/changelogs/unreleased/introduce-pipeline-triggers.yml
@@ -0,0 +1,4 @@
+---
+title: Introduce Pipeline Triggers that are user-aware
+merge_request:
+author:
diff --git a/config/initializers/metrics.rb b/config/initializers/8_metrics.rb
similarity index 100%
rename from config/initializers/metrics.rb
rename to config/initializers/8_metrics.rb
diff --git a/config/initializers/etag_caching.rb b/config/initializers/etag_caching.rb
new file mode 100644
index 0000000000000000000000000000000000000000..eba888011418c5789c1f1a740c1b4b10d660407e
--- /dev/null
+++ b/config/initializers/etag_caching.rb
@@ -0,0 +1,4 @@
+# This middleware has to come after Gitlab::Metrics::RackMiddleware
+# in the middleware stack, because it tracks events with
+# GitLab Performance Monitoring
+Rails.application.config.middleware.use(Gitlab::EtagCaching::Middleware)
diff --git a/config/routes/profile.rb b/config/routes/profile.rb
index 6b91485da9e6a2d1c25afe2e8736e6daa51797aa..07c341999eac83d90b0ac87ff4d0b20094683595 100644
--- a/config/routes/profile.rb
+++ b/config/routes/profile.rb
@@ -21,7 +21,7 @@ resource :profile, only: [:show, :update] do
       end
     end
     resource :preferences, only: [:show, :update]
-    resources :keys, only: [:index, :show, :new, :create, :destroy]
+    resources :keys, only: [:index, :show, :create, :destroy]
     resources :emails, only: [:index, :create, :destroy]
     resources :chat_names, only: [:index, :new, :create, :destroy] do
       collection do
diff --git a/config/routes/project.rb b/config/routes/project.rb
index 2703bf4ab46003cd0f0a88f4d27600ec1a500554..7dc7963ab881ac8858f5e8545c63935e1128b5b8 100644
--- a/config/routes/project.rb
+++ b/config/routes/project.rb
@@ -267,7 +267,7 @@ constraints(ProjectUrlConstrainer.new) do
 
       resources :group_links, only: [:index, :create, :update, :destroy], constraints: { id: /\d+/ }
 
-      resources :notes, only: [:index, :create, :destroy, :update], concerns: :awardable, constraints: { id: /\d+/ } do
+      resources :notes, only: [:create, :destroy, :update], concerns: :awardable, constraints: { id: /\d+/ } do
         member do
           delete :delete_attachment
           post :resolve
@@ -275,6 +275,8 @@ constraints(ProjectUrlConstrainer.new) do
         end
       end
 
+      get 'noteable/:target_type/:target_id/notes' => 'notes#index', as: 'noteable_notes'
+
       resources :boards, only: [:index, :show] do
         scope module: :boards do
           resources :issues, only: [:index, :update]
diff --git a/db/migrate/20170217151948_add_owner_id_to_triggers.rb b/db/migrate/20170217151948_add_owner_id_to_triggers.rb
new file mode 100644
index 0000000000000000000000000000000000000000..16d7cc5bed69189e381083db38085556a5ff55c0
--- /dev/null
+++ b/db/migrate/20170217151948_add_owner_id_to_triggers.rb
@@ -0,0 +1,9 @@
+class AddOwnerIdToTriggers < ActiveRecord::Migration
+  include Gitlab::Database::MigrationHelpers
+
+  DOWNTIME = false
+
+  def change
+    add_column :ci_triggers, :owner_id, :integer
+  end
+end
diff --git a/db/migrate/20170217151949_add_description_to_triggers.rb b/db/migrate/20170217151949_add_description_to_triggers.rb
new file mode 100644
index 0000000000000000000000000000000000000000..1dca0e374124a005e4dfc81d40016f706b7bd18c
--- /dev/null
+++ b/db/migrate/20170217151949_add_description_to_triggers.rb
@@ -0,0 +1,9 @@
+class AddDescriptionToTriggers < ActiveRecord::Migration
+  include Gitlab::Database::MigrationHelpers
+
+  DOWNTIME = false
+
+  def change
+    add_column :ci_triggers, :description, :string
+  end
+end
diff --git a/db/migrate/20170305203726_add_owner_id_foreign_key.rb b/db/migrate/20170305203726_add_owner_id_foreign_key.rb
new file mode 100644
index 0000000000000000000000000000000000000000..3eece0e2eb5edd18c3793bf4f378b611959ae9e3
--- /dev/null
+++ b/db/migrate/20170305203726_add_owner_id_foreign_key.rb
@@ -0,0 +1,11 @@
+class AddOwnerIdForeignKey < ActiveRecord::Migration
+  include Gitlab::Database::MigrationHelpers
+
+  DOWNTIME = false
+
+  disable_ddl_transaction!
+
+  def change
+    add_concurrent_foreign_key :ci_triggers, :users, column: :owner_id, on_delete: :cascade
+  end
+end
diff --git a/db/schema.rb b/db/schema.rb
index d034f28c3cacbec2c53628797fe278533cd349a6..d1d3f9d6dd57e3a0866811a823ab3689d51b18f9 100644
--- a/db/schema.rb
+++ b/db/schema.rb
@@ -111,7 +111,7 @@ ActiveRecord::Schema.define(version: 20170306090835) do
     t.boolean "plantuml_enabled"
     t.integer "max_pages_size", default: 100, null: false
     t.integer "terminal_max_session_time", default: 0, null: false
-    t.string "default_artifacts_expire_in", default: '0', null: false
+    t.string "default_artifacts_expire_in", default: "0", null: false
   end
 
   create_table "audit_events", force: :cascade do |t|
@@ -377,6 +377,8 @@ ActiveRecord::Schema.define(version: 20170306090835) do
     t.datetime "created_at"
     t.datetime "updated_at"
     t.integer "gl_project_id"
+    t.integer "owner_id"
+    t.string "description"
   end
 
   add_index "ci_triggers", ["gl_project_id"], name: "index_ci_triggers_on_gl_project_id", using: :btree
@@ -581,9 +583,9 @@ ActiveRecord::Schema.define(version: 20170306090835) do
   end
 
   add_index "labels", ["group_id", "project_id", "title"], name: "index_labels_on_group_id_and_project_id_and_title", unique: true, using: :btree
-  add_index "labels", ["type", "project_id"], name: "index_labels_on_type_and_project_id", using: :btree
   add_index "labels", ["project_id"], name: "index_labels_on_project_id", using: :btree
   add_index "labels", ["title"], name: "index_labels_on_title", using: :btree
+  add_index "labels", ["type", "project_id"], name: "index_labels_on_type_and_project_id", using: :btree
 
   create_table "lfs_objects", force: :cascade do |t|
     t.string "oid", null: false
@@ -1333,6 +1335,7 @@ ActiveRecord::Schema.define(version: 20170306090835) do
   add_index "web_hooks", ["project_id"], name: "index_web_hooks_on_project_id", using: :btree
 
   add_foreign_key "boards", "projects"
+  add_foreign_key "ci_triggers", "users", column: "owner_id", name: "fk_e8e10d1964", on_delete: :cascade
   add_foreign_key "issue_metrics", "issues", on_delete: :cascade
   add_foreign_key "label_priorities", "labels", on_delete: :cascade
   add_foreign_key "label_priorities", "projects", on_delete: :cascade
diff --git a/doc/api/README.md b/doc/api/README.md
index 3399e2bb5f68a80a3a115941b175519516425a98..285cd2435ac169c47dadabf863e6e5e1ab341924 100644
--- a/doc/api/README.md
+++ b/doc/api/README.md
@@ -12,7 +12,6 @@ following locations:
 - [Branches](branches.md)
 - [Broadcast Messages](broadcast_messages.md)
 - [Builds](builds.md)
-- [Build Triggers](build_triggers.md)
 - [Build Variables](build_variables.md)
 - [Commits](commits.md)
 - [Deployments](deployments.md)
@@ -33,6 +32,7 @@ following locations:
 - [Notes](notes.md) (comments)
 - [Notification settings](notification_settings.md)
 - [Pipelines](pipelines.md)
+- [Pipeline Triggers](pipeline_triggers.md)
 - [Projects](projects.md) including setting Webhooks
 - [Project Access Requests](access_requests.md)
 - [Project Members](members.md)
diff --git a/doc/api/build_triggers.md b/doc/api/build_triggers.md
index 28befba69d694f4c0426f84f3c2f32247a9d69db..20d924ab35e1f119eec43b06660347ef01e69f06 100644
--- a/doc/api/build_triggers.md
+++ b/doc/api/build_triggers.md
@@ -1,108 +1 @@
-# Build triggers
-
-You can read more about [triggering builds through the API](../ci/triggers/README.md).
-
-## List project triggers
-
-Get a list of project's build triggers.
-
-```
-GET /projects/:id/triggers
-```
-
-| Attribute | Type    | required | Description         |
-|-----------|---------|----------|---------------------|
-| `id`      | integer | yes      | The ID of a project |
-
-```
-curl --header "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" "https://gitlab.example.com/api/v4/projects/1/triggers"
-```
-
-```json
-[
-    {
-        "created_at": "2015-12-23T16:24:34.716Z",
-        "deleted_at": null,
-        "last_used": "2016-01-04T15:41:21.986Z",
-        "token": "fbdb730c2fbdb095a0862dbd8ab88b",
-        "updated_at": "2015-12-23T16:24:34.716Z"
-    },
-    {
-        "created_at": "2015-12-23T16:25:56.760Z",
-        "deleted_at": null,
-        "last_used": null,
-        "token": "7b9148c158980bbd9bcea92c17522d",
-        "updated_at": "2015-12-23T16:25:56.760Z"
-    }
-]
-```
-
-## Get trigger details
-
-Get details of project's build trigger.
-
-```
-GET /projects/:id/triggers/:token
-```
-
-| Attribute | Type    | required | Description              |
-|-----------|---------|----------|--------------------------|
-| `id`      | integer | yes      | The ID of a project      |
-| `token`   | string  | yes      | The `token` of a trigger |
-
-```
-curl --header "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" "https://gitlab.example.com/api/v4/projects/1/triggers/7b9148c158980bbd9bcea92c17522d"
-```
-
-```json
-{
-    "created_at": "2015-12-23T16:25:56.760Z",
-    "deleted_at": null,
-    "last_used": null,
-    "token": "7b9148c158980bbd9bcea92c17522d",
-    "updated_at": "2015-12-23T16:25:56.760Z"
-}
-```
-
-## Create a project trigger
-
-Create a build trigger for a project.
-
-```
-POST /projects/:id/triggers
-```
-
-| Attribute | Type    | required | Description              |
-|-----------|---------|----------|--------------------------|
-| `id`      | integer | yes      | The ID of a project      |
-
-```
-curl --request POST --header "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" "https://gitlab.example.com/api/v4/projects/1/triggers"
-```
-
-```json
-{
-    "created_at": "2016-01-07T09:53:58.235Z",
-    "deleted_at": null,
-    "last_used": null,
-    "token": "6d056f63e50fe6f8c5f8f4aa10edb7",
-    "updated_at": "2016-01-07T09:53:58.235Z"
-}
-```
-
-## Remove a project trigger
-
-Remove a project's build trigger.
-
-```
-DELETE /projects/:id/triggers/:token
-```
-
-| Attribute | Type    | required | Description              |
-|-----------|---------|----------|--------------------------|
-| `id`      | integer | yes      | The ID of a project      |
-| `token`   | string  | yes      | The `token` of a trigger |
-
-```
-curl --request DELETE --header "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" "https://gitlab.example.com/api/v4/projects/1/triggers/7b9148c158980bbd9bcea92c17522d"
-```
+This document was moved to [Pipeline Triggers](pipeline_triggers.md).
diff --git a/doc/api/pipeline_triggers.md b/doc/api/pipeline_triggers.md
new file mode 100644
index 0000000000000000000000000000000000000000..fdb41a1d615ee7783544cd07ee1f2c2bbb20fed6
--- /dev/null
+++ b/doc/api/pipeline_triggers.md
@@ -0,0 +1,170 @@
+# Pipeline triggers
+
+You can read more about [triggering pipelines through the API](../ci/triggers/README.md).
+
+## List project triggers
+
+Get a list of project's build triggers.
+
+```
+GET /projects/:id/triggers
+```
+
+| Attribute | Type    | required | Description         |
+|-----------|---------|----------|---------------------|
+| `id`      | integer | yes      | The ID of a project |
+
+```
+curl --header "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" "https://gitlab.example.com/api/v4/projects/1/triggers"
+```
+
+```json
+[
+    {
+        "id": 10,
+        "description": "my trigger",
+        "created_at": "2016-01-07T09:53:58.235Z",
+        "deleted_at": null,
+        "last_used": null,
+        "token": "6d056f63e50fe6f8c5f8f4aa10edb7",
+        "updated_at": "2016-01-07T09:53:58.235Z",
+        "owner": null
+    }
+]
+```
+
+## Get trigger details
+
+Get details of project's build trigger.
+
+```
+GET /projects/:id/triggers/:trigger_id
+```
+
+| Attribute | Type    | required | Description              |
+|-----------|---------|----------|--------------------------|
+| `id`      | integer | yes      | The ID of a project      |
+| `token`   | string  | yes      | The `token` of a trigger |
+
+```
+curl --header "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" "https://gitlab.example.com/api/v4/projects/1/triggers/5"
+```
+
+```json
+{
+    "id": 10,
+    "description": "my trigger",
+    "created_at": "2016-01-07T09:53:58.235Z",
+    "deleted_at": null,
+    "last_used": null,
+    "token": "6d056f63e50fe6f8c5f8f4aa10edb7",
+    "updated_at": "2016-01-07T09:53:58.235Z",
+    "owner": null
+}
+```
+
+## Create a project trigger
+
+Create a trigger for a project.
+
+```
+POST /projects/:id/triggers
+```
+
+| Attribute     | Type    | required | Description              |
+|---------------|---------|----------|--------------------------|
+| `id`          | integer | yes      | The ID of a project      |
+| `description` | string  | yes      | The trigger name         |
+
+```
+curl --request POST --header "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" --form description="my description" "https://gitlab.example.com/api/v4/projects/1/triggers"
+```
+
+```json
+{
+    "id": 10,
+    "description": "my trigger",
+    "created_at": "2016-01-07T09:53:58.235Z",
+    "deleted_at": null,
+    "last_used": null,
+    "token": "6d056f63e50fe6f8c5f8f4aa10edb7",
+    "updated_at": "2016-01-07T09:53:58.235Z",
+    "owner": null
+}
+```
+
+## Update a project trigger
+
+Update a trigger for a project.
+
+```
+PUT /projects/:id/triggers/:trigger_id
+```
+
+| Attribute     | Type    | required | Description              |
+|---------------|---------|----------|--------------------------|
+| `trigger_id`  | integer | yes      | The trigger id           |
+| `description` | string  | no       | The trigger name         |
+
+```
+curl --request PUT --header "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" --form description="my description" "https://gitlab.example.com/api/v4/projects/1/triggers/10"
+```
+
+```json
+{
+    "id": 10,
+    "description": "my trigger",
+    "created_at": "2016-01-07T09:53:58.235Z",
+    "deleted_at": null,
+    "last_used": null,
+    "token": "6d056f63e50fe6f8c5f8f4aa10edb7",
+    "updated_at": "2016-01-07T09:53:58.235Z",
+    "owner": null
+}
+```
+
+## Take ownership of a project trigger
+
+Update an owner of a project trigger.
+
+```
+POST /projects/:id/triggers/:trigger_id/take_ownership
+```
+
+| Attribute     | Type    | required | Description              |
+|---------------|---------|----------|--------------------------|
+| `trigger_id`  | integer | yes      | The trigger id           |
+
+```
+curl --request POST --header "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" "https://gitlab.example.com/api/v4/projects/1/triggers/10/take_ownership"
+```
+
+```json
+{
+    "id": 10,
+    "description": "my trigger",
+    "created_at": "2016-01-07T09:53:58.235Z",
+    "deleted_at": null,
+    "last_used": null,
+    "token": "6d056f63e50fe6f8c5f8f4aa10edb7",
+    "updated_at": "2016-01-07T09:53:58.235Z",
+    "owner": null
+}
+```
+
+## Remove a project trigger
+
+Remove a project's build trigger.
+
+```
+DELETE /projects/:id/triggers/:trigger_id
+```
+
+| Attribute      | Type    | required | Description              |
+|----------------|---------|----------|--------------------------|
+| `id`           | integer | yes      | The ID of a project      |
+| `trigger_id`   | integer | yes      | The trigger id           |
+
+```
+curl --request DELETE --header "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" "https://gitlab.example.com/api/v4/projects/1/triggers/5"
+```
diff --git a/doc/api/v3_to_v4.md b/doc/api/v3_to_v4.md
index 39dc6d98e7b6bcba6f5c6dc17ab3a2e3b31582aa..67ee2b69c3f500fb870be091111f1cc070f0b2b5 100644
--- a/doc/api/v3_to_v4.md
+++ b/doc/api/v3_to_v4.md
@@ -59,3 +59,6 @@ changes are in V4:
 - Return 202 with JSON body on async removals on V4 API (DELETE `/projects/:id/repository/merged_branches` and DELETE `/projects/:id`) [!9449](https://gitlab.com/gitlab-org/gitlab-ce/merge_requests/9449)
 - `projects/:id/milestones?iid[]=x&iid[]=y` array filter has been renamed to `iids` [!9096](https://gitlab.com/gitlab-org/gitlab-ce/merge_requests/9096)
 - Return basic info about pipeline in `GET /projects/:id/pipelines` [!8875](https://gitlab.com/gitlab-org/gitlab-ce/merge_requests/8875)
+- Rename Build Triggers to be Pipeline Triggers API [!9713](https://gitlab.com/gitlab-org/gitlab-ce/merge_requests/9713)
+  - `POST /projects/:id/trigger/builds` to `POST /projects/:id/trigger/pipeline`
+  - Require description when creating a new trigger `POST /projects/:id/triggers`
diff --git a/doc/ci/triggers/README.md b/doc/ci/triggers/README.md
index 1ad9621c8a0b0e8aa490c4d6bebbf542a2771f21..ccaee33dc920ca414d3fe5576ce47ac5228b233f 100644
--- a/doc/ci/triggers/README.md
+++ b/doc/ci/triggers/README.md
@@ -36,7 +36,7 @@ it will not trigger a job.
 To trigger a job you need to send a `POST` request to GitLab's API endpoint:
 
 ```
-POST /projects/:id/trigger/builds
+POST /projects/:id/trigger/pipeline
 ```
 
 The required parameters are the trigger's `token` and the Git `ref` on which
@@ -71,7 +71,7 @@ To trigger a job from webhook of another project you need to add the following
 webhook url for Push and Tag push events:
 
 ```
-https://gitlab.example.com/api/v4/projects/:id/ref/:ref/trigger/builds?token=TOKEN
+https://gitlab.example.com/api/v4/projects/:id/ref/:ref/trigger/pipeline?token=TOKEN
 ```
 
 > **Note**:
@@ -105,7 +105,7 @@ Using cURL you can trigger a rebuild with minimal effort, for example:
 curl --request POST \
      --form token=TOKEN \
      --form ref=master \
-     https://gitlab.example.com/api/v4/projects/9/trigger/builds
+     https://gitlab.example.com/api/v4/projects/9/trigger/pipeline
 ```
 
 In this case, the project with ID `9` will get rebuilt on `master` branch.
@@ -114,7 +114,7 @@ Alternatively, you can pass the `token` and `ref` arguments in the query string:
 
 ```bash
 curl --request POST \
-    "https://gitlab.example.com/api/v4/projects/9/trigger/builds?token=TOKEN&ref=master"
+    "https://gitlab.example.com/api/v4/projects/9/trigger/pipeline?token=TOKEN&ref=master"
 ```
 
 ### Triggering a job within `.gitlab-ci.yml`
@@ -128,7 +128,7 @@ need to add in project's A `.gitlab-ci.yml`:
 build_docs:
   stage: deploy
   script:
-  - "curl --request POST --form token=TOKEN --form ref=master https://gitlab.example.com/api/v4/projects/9/trigger/builds"
+  - "curl --request POST --form token=TOKEN --form ref=master https://gitlab.example.com/api/v4/projects/9/trigger/pipeline"
   only:
   - tags
 ```
@@ -187,7 +187,7 @@ curl --request POST \
   --form token=TOKEN \
   --form ref=master \
   --form "variables[UPLOAD_TO_S3]=true" \
-  https://gitlab.example.com/api/v4/projects/9/trigger/builds
+  https://gitlab.example.com/api/v4/projects/9/trigger/pipeline
 ```
 
 ### Using webhook to trigger job
@@ -195,7 +195,7 @@ curl --request POST \
 You can add the following webhook to another project in order to trigger a job:
 
 ```
-https://gitlab.example.com/api/v4/projects/9/ref/master/trigger/builds?token=TOKEN&variables[UPLOAD_TO_S3]=true
+https://gitlab.example.com/api/v4/projects/9/ref/master/trigger/pipeline?token=TOKEN&variables[UPLOAD_TO_S3]=true
 ```
 
 ### Using cron to trigger nightly jobs
@@ -205,7 +205,7 @@ in conjunction with cron. The example below triggers a job on the `master`
 branch of project with ID `9` every night at `00:30`:
 
 ```bash
-30 0 * * * curl --request POST --form token=TOKEN --form ref=master https://gitlab.example.com/api/v4/projects/9/trigger/builds
+30 0 * * * curl --request POST --form token=TOKEN --form ref=master https://gitlab.example.com/api/v4/projects/9/trigger/pipeline
 ```
 
 [ci-229]: https://gitlab.com/gitlab-org/gitlab-ci/merge_requests/229
diff --git a/doc/ci/variables/README.md b/doc/ci/variables/README.md
index 04c0af44237ea3a154af7954fa21a0a67bcea245..a9e25187b886c18c856ccb76ec4c5f7f2c2c185b 100644
--- a/doc/ci/variables/README.md
+++ b/doc/ci/variables/README.md
@@ -308,8 +308,8 @@ Running on runner-8a2f473d-project-1796893-concurrent-0 via runner-8a2f473d-mach
 ++ CI_RUNNER_ID=1337
 ++ export CI_RUNNER_DESCRIPTION=shared-runners-manager-1.example.com
 ++ CI_RUNNER_DESCRIPTION=shared-runners-manager-1.example.com
-++ export 'CI_RUNNER_TAGS=shared, docker, linux, ruby, mysql, postgres, mongo, git-annex'
-++ CI_RUNNER_TAGS='shared, docker, linux, ruby, mysql, postgres, mongo, git-annex'
+++ export 'CI_RUNNER_TAGS=shared, docker, linux, ruby, mysql, postgres, mongo'
+++ CI_RUNNER_TAGS='shared, docker, linux, ruby, mysql, postgres, mongo'
 ++ export CI_REGISTRY=registry.example.com
 ++ CI_REGISTRY=registry.example.com
 ++ export CI_DEBUG_TRACE=true
diff --git a/doc/development/instrumentation.md b/doc/development/instrumentation.md
index b8669964c84b74858fd6c285b47507ffa04d0a50..a14c0752366b854d5586984dbcd10835d4edb224 100644
--- a/doc/development/instrumentation.md
+++ b/doc/development/instrumentation.md
@@ -35,7 +35,7 @@ Using this method is in general preferred over directly calling the various
 instrumentation methods.
 
 Method instrumentation should be added in the initializer
-`config/initializers/metrics.rb`.
+`config/initializers/8_metrics.rb`.
 
 ### Examples
 
diff --git a/doc/downgrade_ee_to_ce/README.md b/doc/downgrade_ee_to_ce/README.md
index a6d22e5a04a299d02b840148ca7ce52de8ea9558..fe4b6d737710d47e4ea553b2ad0857259e7cd068 100644
--- a/doc/downgrade_ee_to_ce/README.md
+++ b/doc/downgrade_ee_to_ce/README.md
@@ -15,13 +15,6 @@ Kerberos and Atlassian Crowd are only available on the Enterprise Edition, so
 you should disable these mechanisms before downgrading and you should provide
 alternative authentication methods to your users.
 
-### Git Annex
-
-Git Annex is also only available on the Enterprise Edition. This means that if
-you have repositories that use Git Annex to store large files, these files will
-no longer be easily available via Git. You should consider migrating these
-repositories to use Git LFS before downgrading to the Community Edition.
-
 ### Remove Jenkins CI Service entries from the database
 
 The `JenkinsService` class is only available on the Enterprise Edition codebase,
diff --git a/doc/university/README.md b/doc/university/README.md
index 8d4e7eff1150ee1836f628a46f05c93b65884848..c1661f0b52ba2914f9b049e12bad5e92d2e7ef3f 100644
--- a/doc/university/README.md
+++ b/doc/university/README.md
@@ -165,7 +165,7 @@ The curriculum is composed of GitLab videos, screencasts, presentations, project
 
 #### 3.4. Large Files
 
-1. [Big files in Git (Git LFS, Annex) - Video](https://www.youtube.com/watch?v=DawznUxYDe4)
+1. [Big files in Git (Git LFS) - Video](https://www.youtube.com/watch?v=DawznUxYDe4)
 
 #### 3.5. LDAP and Active Directory
 
diff --git a/doc/university/support/README.md b/doc/university/support/README.md
index ca538ef6dc327fb59c3b582fc629024db7ad86b7..567dadb3b47dcbe41b4aba3382eb351d1492f83e 100644
--- a/doc/university/support/README.md
+++ b/doc/university/support/README.md
@@ -167,7 +167,6 @@ Some tickets need specific knowledge or a deep understanding of a particular com
 
 Move on to understanding some of GitLab's more advanced features. You can make use of GitLab.com to understand the features from an end-user perspective and then use your own instance to understand setup and configuration of the feature from an Administrative perspective
 
-- Set up and try [Git Annex](https://docs.gitlab.com/ee/workflow/git_annex.html)
 - Set up and try [Git LFS](https://docs.gitlab.com/ee/workflow/lfs/manage_large_binaries_with_git_lfs.html)
 - Get to know the [GitLab API](https://docs.gitlab.com/ee/api/README.html), its capabilities and shortcomings
 - Learn how to [migrate from SVN to Git](https://docs.gitlab.com/ee/workflow/importing/migrating_from_svn.html)
diff --git a/doc/user/project/pages/getting_started_part_four.md b/doc/user/project/pages/getting_started_part_four.md
index 6edf99239ea1062934379092280ef2fa988187e4..35af48724f22a2eb9f6e5544c8d8c7e8a43e3f90 100644
--- a/doc/user/project/pages/getting_started_part_four.md
+++ b/doc/user/project/pages/getting_started_part_four.md
@@ -133,6 +133,9 @@ your Jekyll 3.4.0 site with GitLab Pages. This is the minimum
 configuration for our example. On the steps below, we'll refine
 the script by adding extra options to our GitLab CI.
 
+Artifacts will be automatically deleted once GitLab Pages got deployed.
+You can preserve artifacts for limited time by specifying the expiry time.
+
 ### Image
 
 At this point, you probably ask yourself: "okay, but to install Jekyll
diff --git a/doc/workflow/lfs/manage_large_binaries_with_git_lfs.md b/doc/workflow/lfs/manage_large_binaries_with_git_lfs.md
index 9cc45065eb295effd79958a25df204bf7de289d5..6adde4479752f4ce2a9a7318a3fbb3ec9aa43590 100644
--- a/doc/workflow/lfs/manage_large_binaries_with_git_lfs.md
+++ b/doc/workflow/lfs/manage_large_binaries_with_git_lfs.md
@@ -4,13 +4,6 @@ Managing large files such as audio, video and graphics files has always been one
 of the shortcomings of Git. The general recommendation is to not have Git repositories
 larger than 1GB to preserve performance.
 
-GitLab already supports [managing large files with git annex](http://docs.gitlab.com/ee/workflow/git_annex.html)
-(EE only), however in certain environments it is not always convenient to use
-different commands to differentiate between the large files and regular ones.
-
-Git LFS makes this simpler for the end user by removing the requirement to
-learn new commands.
-
 ## How it works
 
 Git LFS client talks with the GitLab server over HTTPS. It uses HTTP Basic Authentication
diff --git a/lib/api/entities.rb b/lib/api/entities.rb
index d2d21f5d03a91a654871f040c11c3ba15aa42b3a..98ef9d4118e0ababe24131728dc58f2254118e78 100644
--- a/lib/api/entities.rb
+++ b/lib/api/entities.rb
@@ -592,10 +592,6 @@ module API
       end
     end
 
-    class TriggerRequest < Grape::Entity
-      expose :id, :variables
-    end
-
     class Runner < Grape::Entity
       expose :id
       expose :description
@@ -643,7 +639,10 @@ module API
     end
 
     class Trigger < Grape::Entity
-      expose :token, :created_at, :updated_at, :deleted_at, :last_used
+      expose :id
+      expose :token, :description
+      expose :created_at, :updated_at, :deleted_at, :last_used
+      expose :owner, using: Entities::UserBasic
     end
 
     class Variable < Grape::Entity
diff --git a/lib/api/triggers.rb b/lib/api/triggers.rb
index b7c9c5f2b7f99b9cc297b6120d024dd99e937601..119e9024712a9634ddf57b07bbdf5e87188e44f2 100644
--- a/lib/api/triggers.rb
+++ b/lib/api/triggers.rb
@@ -6,15 +6,15 @@ module API
       requires :id, type: String, desc: 'The ID of a project'
     end
     resource :projects do
-      desc 'Trigger a GitLab project build' do
-        success Entities::TriggerRequest
+      desc 'Trigger a GitLab project pipeline' do
+        success Entities::Pipeline
       end
       params do
         requires :ref, type: String, desc: 'The commit sha or name of a branch or tag'
         requires :token, type: String, desc: 'The unique token of trigger'
         optional :variables, type: Hash, desc: 'The list of variables to be injected into build'
       end
-      post ":id/(ref/:ref/)trigger/builds" do
+      post ":id/(ref/:ref/)trigger/pipeline" do
         project = find_project(params[:id])
         trigger = Ci::Trigger.find_by_token(params[:token].to_s)
         not_found! unless project && trigger
@@ -29,9 +29,9 @@ module API
         # create request and trigger builds
         trigger_request = Ci::CreateTriggerRequestService.new.execute(project, trigger, params[:ref].to_s, variables)
         if trigger_request
-          present trigger_request, with: Entities::TriggerRequest
+          present trigger_request.pipeline, with: Entities::Pipeline
         else
-          errors = 'No builds created'
+          errors = 'No pipeline created'
           render_api_error!(errors, 400)
         end
       end
@@ -55,13 +55,13 @@ module API
         success Entities::Trigger
       end
       params do
-        requires :token, type: String, desc: 'The unique token of trigger'
+        requires :trigger_id, type: Integer,  desc: 'The trigger ID'
       end
-      get ':id/triggers/:token' do
+      get ':id/triggers/:trigger_id' do
         authenticate!
         authorize! :admin_build, user_project
 
-        trigger = user_project.triggers.find_by(token: params[:token].to_s)
+        trigger = user_project.triggers.find(params.delete(:trigger_id))
         return not_found!('Trigger') unless trigger
 
         present trigger, with: Entities::Trigger
@@ -70,26 +70,76 @@ module API
       desc 'Create a trigger' do
         success Entities::Trigger
       end
+      params do
+        requires :description, type: String,  desc: 'The trigger description'
+      end
       post ':id/triggers' do
         authenticate!
         authorize! :admin_build, user_project
 
-        trigger = user_project.triggers.create
+        trigger = user_project.triggers.create(
+          declared_params(include_missing: false).merge(owner: current_user))
 
-        present trigger, with: Entities::Trigger
+        if trigger.valid?
+          present trigger, with: Entities::Trigger
+        else
+          render_validation_error!(trigger)
+        end
+      end
+
+      desc 'Update a trigger' do
+        success Entities::Trigger
+      end
+      params do
+        requires :trigger_id, type: Integer,  desc: 'The trigger ID'
+        optional :description, type: String,  desc: 'The trigger description'
+      end
+      put ':id/triggers/:trigger_id' do
+        authenticate!
+        authorize! :admin_build, user_project
+
+        trigger = user_project.triggers.find(params.delete(:trigger_id))
+        return not_found!('Trigger') unless trigger
+
+        if trigger.update(declared_params(include_missing: false))
+          present trigger, with: Entities::Trigger
+        else
+          render_validation_error!(trigger)
+        end
+      end
+
+      desc 'Take ownership of trigger' do
+        success Entities::Trigger
+      end
+      params do
+        requires :trigger_id, type: Integer,  desc: 'The trigger ID'
+      end
+      post ':id/triggers/:trigger_id/take_ownership' do
+        authenticate!
+        authorize! :admin_build, user_project
+
+        trigger = user_project.triggers.find(params.delete(:trigger_id))
+        return not_found!('Trigger') unless trigger
+
+        if trigger.update(owner: current_user)
+          status :ok
+          present trigger, with: Entities::Trigger
+        else
+          render_validation_error!(trigger)
+        end
       end
 
       desc 'Delete a trigger' do
         success Entities::Trigger
       end
       params do
-        requires :token, type: String, desc: 'The unique token of trigger'
+        requires :trigger_id, type: Integer,  desc: 'The trigger ID'
       end
-      delete ':id/triggers/:token' do
+      delete ':id/triggers/:trigger_id' do
         authenticate!
         authorize! :admin_build, user_project
 
-        trigger = user_project.triggers.find_by(token: params[:token].to_s)
+        trigger = user_project.triggers.find(params.delete(:trigger_id))
         return not_found!('Trigger') unless trigger
 
         trigger.destroy
diff --git a/lib/api/v3/entities.rb b/lib/api/v3/entities.rb
index 270d99a23484a77d03839a17fe2e4edc11aabe64..69853d33becfcadd98b72808d677c5d03d4cec98 100644
--- a/lib/api/v3/entities.rb
+++ b/lib/api/v3/entities.rb
@@ -186,6 +186,15 @@ module API
       class Environment < ::API::Entities::EnvironmentBasic
         expose :project, using: Entities::Project
       end
+
+      class Trigger < Grape::Entity
+        expose :token, :created_at, :updated_at, :deleted_at, :last_used
+        expose :owner, using: ::API::Entities::UserBasic
+      end
+
+      class TriggerRequest < Grape::Entity
+        expose :id, :variables
+      end
     end
   end
 end
diff --git a/lib/api/v3/triggers.rb b/lib/api/v3/triggers.rb
index 4051d4bca8de97bb16a3b58e8fa21744485850be..1dfdb6a5956c15b6edfc2f847f5b8900dca9b616 100644
--- a/lib/api/v3/triggers.rb
+++ b/lib/api/v3/triggers.rb
@@ -7,8 +7,81 @@ module API
         requires :id, type: String, desc: 'The ID of a project'
       end
       resource :projects do
+        desc 'Trigger a GitLab project build' do
+          success ::API::V3::Entities::TriggerRequest
+        end
+        params do
+          requires :ref, type: String, desc: 'The commit sha or name of a branch or tag'
+          requires :token, type: String, desc: 'The unique token of trigger'
+          optional :variables, type: Hash, desc: 'The list of variables to be injected into build'
+        end
+        post ":id/(ref/:ref/)trigger/builds" do
+          project = find_project(params[:id])
+          trigger = Ci::Trigger.find_by_token(params[:token].to_s)
+          not_found! unless project && trigger
+          unauthorized! unless trigger.project == project
+
+          # validate variables
+          variables = params[:variables].to_h
+          unless variables.all? { |key, value| key.is_a?(String) && value.is_a?(String) }
+            render_api_error!('variables needs to be a map of key-valued strings', 400)
+          end
+
+          # create request and trigger builds
+          trigger_request = Ci::CreateTriggerRequestService.new.execute(project, trigger, params[:ref].to_s, variables)
+          if trigger_request
+            present trigger_request, with: ::API::V3::Entities::TriggerRequest
+          else
+            errors = 'No builds created'
+            render_api_error!(errors, 400)
+          end
+        end
+
+        desc 'Get triggers list' do
+          success ::API::V3::Entities::Trigger
+        end
+        params do
+          use :pagination
+        end
+        get ':id/triggers' do
+          authenticate!
+          authorize! :admin_build, user_project
+
+          triggers = user_project.triggers.includes(:trigger_requests)
+
+          present paginate(triggers), with: ::API::V3::Entities::Trigger
+        end
+
+        desc 'Get specific trigger of a project' do
+          success ::API::V3::Entities::Trigger
+        end
+        params do
+          requires :token, type: String, desc: 'The unique token of trigger'
+        end
+        get ':id/triggers/:token' do
+          authenticate!
+          authorize! :admin_build, user_project
+
+          trigger = user_project.triggers.find_by(token: params[:token].to_s)
+          return not_found!('Trigger') unless trigger
+
+          present trigger, with: ::API::V3::Entities::Trigger
+        end
+
+        desc 'Create a trigger' do
+          success ::API::V3::Entities::Trigger
+        end
+        post ':id/triggers' do
+          authenticate!
+          authorize! :admin_build, user_project
+
+          trigger = user_project.triggers.create
+
+          present trigger, with: ::API::V3::Entities::Trigger
+        end
+
         desc 'Delete a trigger' do
-          success ::API::Entities::Trigger
+          success ::API::V3::Entities::Trigger
         end
         params do
           requires :token, type: String, desc: 'The unique token of trigger'
@@ -22,7 +95,7 @@ module API
 
           trigger.destroy
 
-          present trigger, with: ::API::Entities::Trigger
+          present trigger, with: ::API::V3::Entities::Trigger
         end
       end
     end
diff --git a/lib/backup/repository.rb b/lib/backup/repository.rb
index d16d5ba4960e110a16406ba25fa5ca7c37bf8ba5..3c4ba5d50e6e92dda4b495f751efde0c0ef53254 100644
--- a/lib/backup/repository.rb
+++ b/lib/backup/repository.rb
@@ -180,9 +180,8 @@ module Backup
       return unless Dir.exist?(path)
 
       dir_entries = Dir.entries(path)
-      %w[annex custom_hooks].each do |entry|
-        yield(entry) if dir_entries.include?(entry)
-      end
+
+      yield('custom_hooks') if dir_entries.include?('custom_hooks')
     end
 
     def prepare
diff --git a/lib/gitlab/etag_caching/middleware.rb b/lib/gitlab/etag_caching/middleware.rb
new file mode 100644
index 0000000000000000000000000000000000000000..0f24f9bbfde68f211739ea5d16cc4290abfb4bb5
--- /dev/null
+++ b/lib/gitlab/etag_caching/middleware.rb
@@ -0,0 +1,66 @@
+module Gitlab
+  module EtagCaching
+    class Middleware
+      RESERVED_WORDS = ProjectPathValidator::RESERVED.map { |word| "/#{word}/" }.join('|')
+      ROUTE_REGEXP = Regexp.union(
+        %r(^(?!.*(#{RESERVED_WORDS})).*/noteable/issue/\d+/notes\z)
+      )
+
+      def initialize(app)
+        @app = app
+      end
+
+      def call(env)
+        return @app.call(env) unless enabled_for_current_route?(env)
+        Gitlab::Metrics.add_event(:etag_caching_middleware_used)
+
+        etag, cached_value_present = get_etag(env)
+        if_none_match = env['HTTP_IF_NONE_MATCH']
+
+        if if_none_match == etag
+          Gitlab::Metrics.add_event(:etag_caching_cache_hit)
+          [304, { 'ETag' => etag }, ['']]
+        else
+          track_cache_miss(if_none_match, cached_value_present)
+
+          status, headers, body = @app.call(env)
+          headers['ETag'] = etag
+          [status, headers, body]
+        end
+      end
+
+      private
+
+      def enabled_for_current_route?(env)
+        ROUTE_REGEXP.match(env['PATH_INFO'])
+      end
+
+      def get_etag(env)
+        cache_key = env['PATH_INFO']
+        store = Store.new
+        current_value = store.get(cache_key)
+        cached_value_present = current_value.present?
+
+        unless cached_value_present
+          current_value = store.touch(cache_key, only_if_missing: true)
+        end
+
+        [weak_etag_format(current_value), cached_value_present]
+      end
+
+      def weak_etag_format(value)
+        %Q{W/"#{value}"}
+      end
+
+      def track_cache_miss(if_none_match, cached_value_present)
+        if if_none_match.blank?
+          Gitlab::Metrics.add_event(:etag_caching_header_missing)
+        elsif !cached_value_present
+          Gitlab::Metrics.add_event(:etag_caching_key_not_found)
+        else
+          Gitlab::Metrics.add_event(:etag_caching_resource_changed)
+        end
+      end
+    end
+  end
+end
diff --git a/lib/gitlab/etag_caching/store.rb b/lib/gitlab/etag_caching/store.rb
new file mode 100644
index 0000000000000000000000000000000000000000..9532e432f787cede6cfd3bf0a7544c987989db8a
--- /dev/null
+++ b/lib/gitlab/etag_caching/store.rb
@@ -0,0 +1,32 @@
+module Gitlab
+  module EtagCaching
+    class Store
+      EXPIRY_TIME = 10.minutes
+      REDIS_NAMESPACE = 'etag:'.freeze
+
+      def get(key)
+        Gitlab::Redis.with { |redis| redis.get(redis_key(key)) }
+      end
+
+      def touch(key, only_if_missing: false)
+        etag = generate_etag
+
+        Gitlab::Redis.with do |redis|
+          redis.set(redis_key(key), etag, ex: EXPIRY_TIME, nx: only_if_missing)
+        end
+
+        etag
+      end
+
+      private
+
+      def generate_etag
+        SecureRandom.hex
+      end
+
+      def redis_key(key)
+        "#{REDIS_NAMESPACE}#{key}"
+      end
+    end
+  end
+end
diff --git a/lib/mattermost/team.rb b/lib/mattermost/team.rb
index 09dfd082b3a5e4fb211219a13a7445de1105310f..afc152aa02eed14d0bf6d9dcbf2c75b6122c9e61 100644
--- a/lib/mattermost/team.rb
+++ b/lib/mattermost/team.rb
@@ -1,7 +1,7 @@
 module Mattermost
   class Team < Client
     def all
-      session_get('/api/v3/teams/all')
+      session_get('/api/v3/teams/all').values
     end
   end
 end
diff --git a/spec/controllers/profiles/keys_controller_spec.rb b/spec/controllers/profiles/keys_controller_spec.rb
index f7219690722e7422cb7eeb41cf65a16c2f92bfb9..61e4fae46fbfbc32eee58a8d2b93baafbf6d21e7 100644
--- a/spec/controllers/profiles/keys_controller_spec.rb
+++ b/spec/controllers/profiles/keys_controller_spec.rb
@@ -3,16 +3,6 @@ require 'spec_helper'
 describe Profiles::KeysController do
   let(:user) { create(:user) }
 
-  describe '#new' do
-    before { sign_in(user) }
-
-    it 'redirects to #index' do
-      get :new
-
-      expect(response).to redirect_to(profile_keys_path)
-    end
-  end
-
   describe "#get_keys" do
     describe "non existant user" do
       it "does not generally work" do
diff --git a/spec/controllers/projects/branches_controller_spec.rb b/spec/controllers/projects/branches_controller_spec.rb
index e70737376af66b3db6781cb36b2e390b6d66a3b0..298a7ff179c80146dd859ef3da6da52e3dc99988 100644
--- a/spec/controllers/projects/branches_controller_spec.rb
+++ b/spec/controllers/projects/branches_controller_spec.rb
@@ -244,4 +244,27 @@ describe Projects::BranchesController do
       end
     end
   end
+
+  describe "GET index" do
+    render_views
+
+    before do
+      sign_in(user)
+    end
+
+    context 'when rendering a JSON format' do
+      it 'filters branches by name' do
+        get :index,
+            namespace_id: project.namespace,
+            project_id: project,
+            format: :json,
+            search: 'master'
+
+        parsed_response = JSON.parse(response.body)
+
+        expect(parsed_response.length).to eq 1
+        expect(parsed_response.first).to eq 'master'
+      end
+    end
+  end
 end
diff --git a/spec/controllers/projects/graphs_controller_spec.rb b/spec/controllers/projects/graphs_controller_spec.rb
index 049bae1899d8fc4df9501d92676e0510e8384bcc..e0de62e4454706c5859834a2984bec698d1a948c 100644
--- a/spec/controllers/projects/graphs_controller_spec.rb
+++ b/spec/controllers/projects/graphs_controller_spec.rb
@@ -30,18 +30,18 @@ describe Projects::GraphsController do
       double(languages: {
                'Ruby'         => 1000,
                'CoffeeScript' => 350,
-               'PowerShell'   => 15
+               'NSIS'         => 15
              })
     end
 
     let(:expected_values) do
-      ps_color = "##{Digest::SHA256.hexdigest('PowerShell')[0...6]}"
+      nsis_color = "##{Digest::SHA256.hexdigest('NSIS')[0...6]}"
       [
         # colors from Linguist:
-        { label: "Ruby",         color: "#701516", highlight: "#701516" },
-        { label: "CoffeeScript", color: "#244776", highlight: "#244776" },
+        { label: "Ruby",         color: "#701516",  highlight: "#701516" },
+        { label: "CoffeeScript", color: "#244776",  highlight: "#244776" },
         # colors from SHA256 fallback:
-        { label: "PowerShell",   color: ps_color,  highlight: ps_color  }
+        { label: "NSIS",         color: nsis_color, highlight: nsis_color }
       ]
     end
 
diff --git a/spec/controllers/projects/notes_controller_spec.rb b/spec/controllers/projects/notes_controller_spec.rb
index dc597202050fe6d157bca87c97ca1f390232bd18..d80780b1d90eb00252d708980760352208f74f7f 100644
--- a/spec/controllers/projects/notes_controller_spec.rb
+++ b/spec/controllers/projects/notes_controller_spec.rb
@@ -200,4 +200,31 @@ describe Projects::NotesController do
       end
     end
   end
+
+  describe 'GET index' do
+    let(:last_fetched_at) { '1487756246' }
+    let(:request_params) do
+      {
+        namespace_id: project.namespace,
+        project_id: project,
+        target_type: 'issue',
+        target_id: issue.id
+      }
+    end
+
+    before do
+      sign_in(user)
+      project.team << [user, :developer]
+    end
+
+    it 'passes last_fetched_at from headers to NotesFinder' do
+      request.headers['X-Last-Fetched-At'] = last_fetched_at
+
+      expect(NotesFinder).to receive(:new)
+        .with(anything, anything, hash_including(last_fetched_at: last_fetched_at))
+        .and_call_original
+
+      get :index, request_params
+    end
+  end
 end
diff --git a/spec/features/profile_spec.rb b/spec/features/profile_spec.rb
index 406d7cf791cd69cf0ba1f0726dd88471c794e6c6..e63feb14b7ea185255ea2607554c4dac012051d2 100644
--- a/spec/features/profile_spec.rb
+++ b/spec/features/profile_spec.rb
@@ -61,4 +61,18 @@ describe 'Profile account page', feature: true do
       expect(find('#incoming-email-token').value).not_to eq(previous_token)
     end
   end
+
+  describe 'when I change my username' do
+    before do
+      visit profile_account_path
+    end
+
+    it 'changes my username' do
+      fill_in 'user_username', with: 'new-username'
+
+      click_button('Update username')
+
+      expect(page).to have_content('new-username')
+    end
+  end
 end
diff --git a/spec/features/projects/services/mattermost_slash_command_spec.rb b/spec/features/projects/services/mattermost_slash_command_spec.rb
index f5adb53a2dc0be16fe3e85b2b4249bda437c6549..24d22a092d43e9684f6dc51b5590ad2666805f9a 100644
--- a/spec/features/projects/services/mattermost_slash_command_spec.rb
+++ b/spec/features/projects/services/mattermost_slash_command_spec.rb
@@ -1,6 +1,6 @@
 require 'spec_helper'
 
-feature 'Setup Mattermost slash commands', feature: true do
+feature 'Setup Mattermost slash commands', :feature, :js do
   let(:user) { create(:user) }
   let(:project) { create(:empty_project) }
   let(:service) { project.create_mattermost_slash_commands_service }
@@ -62,11 +62,11 @@ feature 'Setup Mattermost slash commands', feature: true do
 
       click_link 'Add to Mattermost'
 
-      team_name = teams.first[1]['display_name']
-      select_element = find('select#mattermost_team_id')
+      team_name = teams.first['display_name']
+      select_element = find('#mattermost_team_id')
       selected_option = select_element.find('option[selected]')
 
-      expect(select_element['disabled']).to eq('disabled')
+      expect(select_element['disabled']).to be(true)
       expect(selected_option).to have_content(team_name.to_s)
     end
 
@@ -75,7 +75,7 @@ feature 'Setup Mattermost slash commands', feature: true do
 
       click_link 'Add to Mattermost'
 
-      expect(find('input#mattermost_team_id', visible: false).value).to eq(teams.first[0].to_s)
+      expect(find('input#mattermost_team_id', visible: false).value).to eq(teams.first['id'])
     end
 
     it 'shows an explanation user is a member of multiple teams' do
@@ -92,12 +92,9 @@ feature 'Setup Mattermost slash commands', feature: true do
 
       click_link 'Add to Mattermost'
 
-      select_element = find('select#mattermost_team_id')
-      selected_option = select_element.find('option[selected]')
+      select_element = find('#mattermost_team_id')
 
-      expect(select_element['disabled']).to be(nil)
-      expect(selected_option).to have_content('Select team...')
-      # The 'Select team...' placeholder is item `0`.
+      expect(select_element['disabled']).to be(false)
       expect(select_element.all('option').count).to eq(3)
     end
 
@@ -110,20 +107,37 @@ feature 'Setup Mattermost slash commands', feature: true do
       expect(page).to have_content('test mattermost error message')
     end
 
+    it 'enables the submit button if the required fields are provided', :js do
+      stub_teams(count: 1)
+
+      click_link 'Add to Mattermost'
+
+      expect(find('input[type="submit"]')['disabled']).not_to be(true)
+    end
+
+    it 'disables the submit button if the required fields are not provided', :js do
+      stub_teams(count: 1)
+
+      click_link 'Add to Mattermost'
+
+      fill_in('mattermost_trigger', with: '')
+
+      expect(find('input[type="submit"]')['disabled']).to be(true)
+    end
+
     def stub_teams(count: 0)
       teams = create_teams(count)
 
-      allow_any_instance_of(MattermostSlashCommandsService).to receive(:list_teams) { teams }
+      allow_any_instance_of(MattermostSlashCommandsService).to receive(:list_teams) { [teams, nil] }
 
       teams
     end
 
     def create_teams(count = 0)
-      teams = {}
+      teams = []
 
       count.times do |i|
-        i += 1
-        teams[i] = { id: i, display_name: i }
+        teams.push({ "id" => "x#{i}", "display_name" => "x#{i}-name" })
       end
 
       teams
diff --git a/spec/initializers/metrics_spec.rb b/spec/initializers/8_metrics_spec.rb
similarity index 87%
rename from spec/initializers/metrics_spec.rb
rename to spec/initializers/8_metrics_spec.rb
index bb59516237007bd5ab2a746cfb813c7be29786fc..570754621f397b75e64d06332ed27858cbef9934 100644
--- a/spec/initializers/metrics_spec.rb
+++ b/spec/initializers/8_metrics_spec.rb
@@ -1,5 +1,5 @@
 require 'spec_helper'
-require_relative '../../config/initializers/metrics'
+require_relative '../../config/initializers/8_metrics'
 
 describe 'instrument_classes', lib: true do
   let(:config) { double(:config) }
diff --git a/spec/lib/gitlab/etag_caching/middleware_spec.rb b/spec/lib/gitlab/etag_caching/middleware_spec.rb
new file mode 100644
index 0000000000000000000000000000000000000000..8b5bfc4dbb00d6783aa5e5d8fdbe2437e0ecc5ea
--- /dev/null
+++ b/spec/lib/gitlab/etag_caching/middleware_spec.rb
@@ -0,0 +1,163 @@
+require 'spec_helper'
+
+describe Gitlab::EtagCaching::Middleware do
+  let(:app) { double(:app) }
+  let(:middleware) { described_class.new(app) }
+  let(:app_status_code) { 200 }
+  let(:if_none_match) { nil }
+  let(:enabled_path) { '/gitlab-org/gitlab-ce/noteable/issue/1/notes' }
+
+  context 'when ETag caching is not enabled for current route' do
+    let(:path) { '/gitlab-org/gitlab-ce/tree/master/noteable/issue/1/notes' }
+
+    before do
+      mock_app_response
+    end
+
+    it 'does not add ETag header' do
+      _, headers, _ = middleware.call(build_env(path, if_none_match))
+
+      expect(headers['ETag']).to be_nil
+    end
+
+    it 'passes status code from app' do
+      status, _, _ = middleware.call(build_env(path, if_none_match))
+
+      expect(status).to eq app_status_code
+    end
+  end
+
+  context 'when there is no ETag in store for given resource' do
+    let(:path) { enabled_path }
+
+    before do
+      mock_app_response
+      mock_value_in_store(nil)
+    end
+
+    it 'generates ETag' do
+      expect_any_instance_of(Gitlab::EtagCaching::Store)
+        .to receive(:touch).and_return('123')
+
+      middleware.call(build_env(path, if_none_match))
+    end
+
+    context 'when If-None-Match header was specified' do
+      let(:if_none_match) { 'W/"abc"' }
+
+      it 'tracks "etag_caching_key_not_found" event' do
+        expect(Gitlab::Metrics).to receive(:add_event)
+          .with(:etag_caching_middleware_used)
+        expect(Gitlab::Metrics).to receive(:add_event)
+          .with(:etag_caching_key_not_found)
+
+        middleware.call(build_env(path, if_none_match))
+      end
+    end
+  end
+
+  context 'when there is ETag in store for given resource' do
+    let(:path) { enabled_path }
+
+    before do
+      mock_app_response
+      mock_value_in_store('123')
+    end
+
+    it 'returns this value as header' do
+      _, headers, _ = middleware.call(build_env(path, if_none_match))
+
+      expect(headers['ETag']).to eq 'W/"123"'
+    end
+  end
+
+  context 'when If-None-Match header matches ETag in store' do
+    let(:path) { enabled_path }
+    let(:if_none_match) { 'W/"123"' }
+
+    before do
+      mock_value_in_store('123')
+    end
+
+    it 'does not call app' do
+      expect(app).not_to receive(:call)
+
+      middleware.call(build_env(path, if_none_match))
+    end
+
+    it 'returns status code 304' do
+      status, _, _ = middleware.call(build_env(path, if_none_match))
+
+      expect(status).to eq 304
+    end
+
+    it 'tracks "etag_caching_cache_hit" event' do
+      expect(Gitlab::Metrics).to receive(:add_event)
+        .with(:etag_caching_middleware_used)
+      expect(Gitlab::Metrics).to receive(:add_event)
+        .with(:etag_caching_cache_hit)
+
+      middleware.call(build_env(path, if_none_match))
+    end
+  end
+
+  context 'when If-None-Match header does not match ETag in store' do
+    let(:path) { enabled_path }
+    let(:if_none_match) { 'W/"abc"' }
+
+    before do
+      mock_value_in_store('123')
+    end
+
+    it 'calls app' do
+      expect(app).to receive(:call).and_return([app_status_code, {}, ['body']])
+
+      middleware.call(build_env(path, if_none_match))
+    end
+
+    it 'tracks "etag_caching_resource_changed" event' do
+      mock_app_response
+
+      expect(Gitlab::Metrics).to receive(:add_event)
+        .with(:etag_caching_middleware_used)
+      expect(Gitlab::Metrics).to receive(:add_event)
+        .with(:etag_caching_resource_changed)
+
+      middleware.call(build_env(path, if_none_match))
+    end
+  end
+
+  context 'when If-None-Match header is not specified' do
+    let(:path) { enabled_path }
+
+    before do
+      mock_value_in_store('123')
+      mock_app_response
+    end
+
+    it 'tracks "etag_caching_header_missing" event' do
+      expect(Gitlab::Metrics).to receive(:add_event)
+        .with(:etag_caching_middleware_used)
+      expect(Gitlab::Metrics).to receive(:add_event)
+        .with(:etag_caching_header_missing)
+
+      middleware.call(build_env(path, if_none_match))
+    end
+  end
+
+  def mock_app_response
+    allow(app).to receive(:call).and_return([app_status_code, {}, ['body']])
+  end
+
+  def mock_value_in_store(value)
+    allow_any_instance_of(Gitlab::EtagCaching::Store)
+      .to receive(:get).and_return(value)
+  end
+
+  def build_env(path, if_none_match)
+    {
+      'PATH_INFO' => path,
+      'HTTP_IF_NONE_MATCH' => if_none_match
+    }
+  end
+end
diff --git a/spec/lib/gitlab/import_export/all_models.yml b/spec/lib/gitlab/import_export/all_models.yml
index eef283c2460985f0a06462cb85c109925d1b2e26..f20b6be51e13860441e33bdf3288dd641acb67fe 100644
--- a/spec/lib/gitlab/import_export/all_models.yml
+++ b/spec/lib/gitlab/import_export/all_models.yml
@@ -97,6 +97,7 @@ variables:
 triggers:
 - project
 - trigger_requests
+- owner
 deploy_keys:
 - user
 - deploy_keys_projects
diff --git a/spec/lib/gitlab/import_export/safe_model_attributes.yml b/spec/lib/gitlab/import_export/safe_model_attributes.yml
index 6534902b52d1e020679b0f2b3a3a926ad849f43c..3bd1f335a8913af2bbf9d77092799c2168cc5642 100644
--- a/spec/lib/gitlab/import_export/safe_model_attributes.yml
+++ b/spec/lib/gitlab/import_export/safe_model_attributes.yml
@@ -240,6 +240,8 @@ Ci::Trigger:
 - created_at
 - updated_at
 - gl_project_id
+- owner_id
+- description
 DeployKey:
 - id
 - user_id
diff --git a/spec/lib/mattermost/team_spec.rb b/spec/lib/mattermost/team_spec.rb
index 6a9c7cebffffbf02cd3ccc634711ca774d205f78..ac493fdb20f7aa224bbb14f9cf5574f4afdebc56 100644
--- a/spec/lib/mattermost/team_spec.rb
+++ b/spec/lib/mattermost/team_spec.rb
@@ -13,7 +13,7 @@ describe Mattermost::Team do
 
     context 'for valid request' do
       let(:response) do
-        [{
+        { "xiyro8huptfhdndadpz8r3wnbo" => {
           "id" => "xiyro8huptfhdndadpz8r3wnbo",
           "create_at" => 1482174222155,
           "update_at" => 1482174222155,
@@ -26,7 +26,7 @@ describe Mattermost::Team do
           "allowed_domains" => "",
           "invite_id" => "o4utakb9jtb7imctdfzbf9r5ro",
           "allow_open_invite" => false
-        }]
+        } }
       end
 
       before do
@@ -39,7 +39,7 @@ describe Mattermost::Team do
       end
 
       it 'returns a token' do
-        is_expected.to eq(response)
+        is_expected.to eq(response.values)
       end
     end
 
diff --git a/spec/models/ci/trigger_spec.rb b/spec/models/ci/trigger_spec.rb
index 3ca9231f58e3ea88cafbc7ee858f8ef5d91fdb39..074cf1a0bd7280b56229f8998ab63183e165ab3a 100644
--- a/spec/models/ci/trigger_spec.rb
+++ b/spec/models/ci/trigger_spec.rb
@@ -1,16 +1,24 @@
 require 'spec_helper'
 
 describe Ci::Trigger, models: true do
-  let(:project) { FactoryGirl.create :empty_project }
+  let(:project) { create :empty_project }
+
+  describe 'associations' do
+    it { is_expected.to belong_to(:project) }
+    it { is_expected.to belong_to(:owner) }
+    it { is_expected.to have_many(:trigger_requests) }
+  end
 
   describe 'before_validation' do
     it 'sets an random token if none provided' do
-      trigger = FactoryGirl.create :ci_trigger_without_token, project: project
+      trigger = create(:ci_trigger_without_token, project: project)
+
       expect(trigger.token).not_to be_nil
     end
 
     it 'does not set an random token if one provided' do
-      trigger = FactoryGirl.create :ci_trigger, project: project
+      trigger = create(:ci_trigger, project: project)
+
       expect(trigger.token).to eq('token')
     end
   end
diff --git a/spec/models/note_spec.rb b/spec/models/note_spec.rb
index 1cde9e049518f2dc1e614d4f872a62c096b6eb23..33536487c4174f1bf6f2635217832f52e6500246 100644
--- a/spec/models/note_spec.rb
+++ b/spec/models/note_spec.rb
@@ -387,4 +387,16 @@ describe Note, models: true do
       end
     end
   end
+
+  describe 'expiring ETag cache' do
+    let(:note) { build(:note_on_issue) }
+
+    it "expires cache for note's issue when note is saved" do
+      expect_any_instance_of(Gitlab::EtagCaching::Store)
+        .to receive(:touch)
+        .with("/#{note.project.namespace.to_param}/#{note.project.to_param}/noteable/issue/#{note.noteable.id}/notes")
+
+      note.save!
+    end
+  end
 end
diff --git a/spec/models/project_services/mattermost_slash_commands_service_spec.rb b/spec/models/project_services/mattermost_slash_commands_service_spec.rb
index 598ff232c82ac291e4526e9ac14f92a089b5cc74..f9531be5d251e8e32e9bd9af2ddce15415b2ac35 100644
--- a/spec/models/project_services/mattermost_slash_commands_service_spec.rb
+++ b/spec/models/project_services/mattermost_slash_commands_service_spec.rb
@@ -92,7 +92,7 @@ describe MattermostSlashCommandsService, :models do
             to_return(
               status: 200,
               headers: { 'Content-Type' => 'application/json' },
-              body: ['list'].to_json
+              body: { 'list' => true }.to_json
             )
         end
 
diff --git a/spec/models/user_spec.rb b/spec/models/user_spec.rb
index e86b4a761d967725cecde1eb4720dce061786aa4..b99cde64675c3a929da3848f74da8a2b9278110a 100644
--- a/spec/models/user_spec.rb
+++ b/spec/models/user_spec.rb
@@ -32,6 +32,7 @@ describe User, models: true do
     it { is_expected.to have_many(:spam_logs).dependent(:destroy) }
     it { is_expected.to have_many(:todos).dependent(:destroy) }
     it { is_expected.to have_many(:award_emoji).dependent(:destroy) }
+    it { is_expected.to have_many(:triggers).dependent(:destroy) }
     it { is_expected.to have_many(:builds).dependent(:nullify) }
     it { is_expected.to have_many(:pipelines).dependent(:nullify) }
     it { is_expected.to have_many(:chat_names).dependent(:destroy) }
diff --git a/spec/requests/api/triggers_spec.rb b/spec/requests/api/triggers_spec.rb
index 153e2791cbe21e32d9b57a7a510950cbf655d2a8..424c02932ab1ab5eacc74db80bfea062da4d656b 100644
--- a/spec/requests/api/triggers_spec.rb
+++ b/spec/requests/api/triggers_spec.rb
@@ -14,7 +14,7 @@ describe API::Triggers do
   let!(:trigger2) { create(:ci_trigger, project: project, token: trigger_token_2) }
   let!(:trigger_request) { create(:ci_trigger_request, trigger: trigger, created_at: '2015-01-01 12:13:14') }
 
-  describe 'POST /projects/:project_id/trigger' do
+  describe 'POST /projects/:project_id/trigger/pipeline' do
     let!(:project2) { create(:project) }
     let(:options) do
       {
@@ -28,17 +28,20 @@ describe API::Triggers do
 
     context 'Handles errors' do
       it 'returns bad request if token is missing' do
-        post api("/projects/#{project.id}/trigger/builds"), ref: 'master'
+        post api("/projects/#{project.id}/trigger/pipeline"), ref: 'master'
+
         expect(response).to have_http_status(400)
       end
 
       it 'returns not found if project is not found' do
-        post api('/projects/0/trigger/builds'), options.merge(ref: 'master')
+        post api('/projects/0/trigger/pipeline'), options.merge(ref: 'master')
+
         expect(response).to have_http_status(404)
       end
 
       it 'returns unauthorized if token is for different project' do
-        post api("/projects/#{project2.id}/trigger/builds"), options.merge(ref: 'master')
+        post api("/projects/#{project2.id}/trigger/pipeline"), options.merge(ref: 'master')
+
         expect(response).to have_http_status(401)
       end
     end
@@ -46,9 +49,11 @@ describe API::Triggers do
     context 'Have a commit' do
       let(:pipeline) { project.pipelines.last }
 
-      it 'creates builds' do
-        post api("/projects/#{project.id}/trigger/builds"), options.merge(ref: 'master')
+      it 'creates pipeline' do
+        post api("/projects/#{project.id}/trigger/pipeline"), options.merge(ref: 'master')
+
         expect(response).to have_http_status(201)
+        expect(json_response).to include('id' => pipeline.id)
         pipeline.builds.reload
         expect(pipeline.builds.pending.size).to eq(2)
         expect(pipeline.builds.size).to eq(5)
@@ -56,15 +61,17 @@ describe API::Triggers do
 
       it 'creates builds on webhook from other gitlab repository and branch' do
         expect do
-          post api("/projects/#{project.id}/ref/master/trigger/builds?token=#{trigger_token}"), { ref: 'refs/heads/other-branch' }
+          post api("/projects/#{project.id}/ref/master/trigger/pipeline?token=#{trigger_token}"), { ref: 'refs/heads/other-branch' }
         end.to change(project.builds, :count).by(5)
+
         expect(response).to have_http_status(201)
       end
 
-      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')
+      it 'returns bad request with no pipeline created if there\'s no commit for that ref' do
+        post api("/projects/#{project.id}/trigger/pipeline"), options.merge(ref: 'other-branch')
+
         expect(response).to have_http_status(400)
-        expect(json_response['message']).to eq('No builds created')
+        expect(json_response['message']).to eq('No pipeline created')
       end
 
       context 'Validates variables' do
@@ -73,22 +80,24 @@ describe API::Triggers do
         end
 
         it 'validates variables to be a hash' do
-          post api("/projects/#{project.id}/trigger/builds"), options.merge(variables: 'value', ref: 'master')
+          post api("/projects/#{project.id}/trigger/pipeline"), options.merge(variables: 'value', ref: 'master')
+
           expect(response).to have_http_status(400)
           expect(json_response['error']).to eq('variables is invalid')
         end
 
         it 'validates variables needs to be a map of key-valued strings' do
-          post api("/projects/#{project.id}/trigger/builds"), options.merge(variables: { key: %w(1 2) }, ref: 'master')
+          post api("/projects/#{project.id}/trigger/pipeline"), options.merge(variables: { key: %w(1 2) }, ref: 'master')
+
           expect(response).to have_http_status(400)
           expect(json_response['message']).to eq('variables needs to be a map of key-valued strings')
         end
 
         it 'creates trigger request with variables' do
-          post api("/projects/#{project.id}/trigger/builds"), options.merge(variables: variables, ref: 'master')
+          post api("/projects/#{project.id}/trigger/pipeline"), options.merge(variables: variables, ref: 'master')
+
           expect(response).to have_http_status(201)
-          pipeline.builds.reload
-          expect(pipeline.builds.first.trigger_request.variables).to eq(variables)
+          expect(pipeline.builds.reload.first.trigger_request.variables).to eq(variables)
         end
       end
     end
@@ -123,17 +132,17 @@ describe API::Triggers do
     end
   end
 
-  describe 'GET /projects/:id/triggers/:token' do
+  describe 'GET /projects/:id/triggers/:trigger_id' do
     context 'authenticated user with valid permissions' do
       it 'returns trigger details' do
-        get api("/projects/#{project.id}/triggers/#{trigger.token}", user)
+        get api("/projects/#{project.id}/triggers/#{trigger.id}", user)
 
         expect(response).to have_http_status(200)
         expect(json_response).to be_a(Hash)
       end
 
       it 'responds with 404 Not Found if requesting non-existing trigger' do
-        get api("/projects/#{project.id}/triggers/abcdef012345", user)
+        get api("/projects/#{project.id}/triggers/-5", user)
 
         expect(response).to have_http_status(404)
       end
@@ -141,7 +150,7 @@ describe API::Triggers do
 
     context 'authenticated user with invalid permissions' do
       it 'does not return triggers list' do
-        get api("/projects/#{project.id}/triggers/#{trigger.token}", user2)
+        get api("/projects/#{project.id}/triggers/#{trigger.id}", user2)
 
         expect(response).to have_http_status(403)
       end
@@ -149,7 +158,7 @@ describe API::Triggers do
 
     context 'unauthenticated user' do
       it 'does not return triggers list' do
-        get api("/projects/#{project.id}/triggers/#{trigger.token}")
+        get api("/projects/#{project.id}/triggers/#{trigger.id}")
 
         expect(response).to have_http_status(401)
       end
@@ -158,19 +167,31 @@ describe API::Triggers do
 
   describe 'POST /projects/:id/triggers' do
     context 'authenticated user with valid permissions' do
-      it 'creates trigger' do
-        expect do
+      context 'with required parameters' do
+        it 'creates trigger' do
+          expect do
+            post api("/projects/#{project.id}/triggers", user),
+              description: 'trigger'
+          end.to change{project.triggers.count}.by(1)
+
+          expect(response).to have_http_status(201)
+          expect(json_response).to include('description' => 'trigger')
+        end
+      end
+
+      context 'without required parameters' do
+        it 'does not create trigger' do
           post api("/projects/#{project.id}/triggers", user)
-        end.to change{project.triggers.count}.by(1)
 
-        expect(response).to have_http_status(201)
-        expect(json_response).to be_a(Hash)
+          expect(response).to have_http_status(:bad_request)
+        end
       end
     end
 
     context 'authenticated user with invalid permissions' do
       it 'does not create trigger' do
-        post api("/projects/#{project.id}/triggers", user2)
+        post api("/projects/#{project.id}/triggers", user2),
+          description: 'trigger'
 
         expect(response).to have_http_status(403)
       end
@@ -178,25 +199,87 @@ describe API::Triggers do
 
     context 'unauthenticated user' do
       it 'does not create trigger' do
-        post api("/projects/#{project.id}/triggers")
+        post api("/projects/#{project.id}/triggers"),
+          description: 'trigger'
+
+        expect(response).to have_http_status(401)
+      end
+    end
+  end
+
+  describe 'PUT /projects/:id/triggers/:trigger_id' do
+    context 'authenticated user with valid permissions' do
+      let(:new_description) { 'new description' }
+
+      it 'updates description' do
+        put api("/projects/#{project.id}/triggers/#{trigger.id}", user),
+          description: new_description
+
+        expect(response).to have_http_status(200)
+        expect(json_response).to include('description' => new_description)
+        expect(trigger.reload.description).to eq(new_description)
+      end
+    end
+
+    context 'authenticated user with invalid permissions' do
+      it 'does not update trigger' do
+        put api("/projects/#{project.id}/triggers/#{trigger.id}", user2)
+
+        expect(response).to have_http_status(403)
+      end
+    end
+
+    context 'unauthenticated user' do
+      it 'does not update trigger' do
+        put api("/projects/#{project.id}/triggers/#{trigger.id}")
+
+        expect(response).to have_http_status(401)
+      end
+    end
+  end
+
+  describe 'POST /projects/:id/triggers/:trigger_id/take_ownership' do
+    context 'authenticated user with valid permissions' do
+      it 'updates owner' do
+        expect(trigger.owner).to be_nil
+
+        post api("/projects/#{project.id}/triggers/#{trigger.id}/take_ownership", user)
+
+        expect(response).to have_http_status(200)
+        expect(json_response).to include('owner')
+        expect(trigger.reload.owner).to eq(user)
+      end
+    end
+
+    context 'authenticated user with invalid permissions' do
+      it 'does not update owner' do
+        post api("/projects/#{project.id}/triggers/#{trigger.id}/take_ownership", user2)
+
+        expect(response).to have_http_status(403)
+      end
+    end
+
+    context 'unauthenticated user' do
+      it 'does not update owner' do
+        post api("/projects/#{project.id}/triggers/#{trigger.id}/take_ownership")
 
         expect(response).to have_http_status(401)
       end
     end
   end
 
-  describe 'DELETE /projects/:id/triggers/:token' do
+  describe 'DELETE /projects/:id/triggers/:trigger_id' do
     context 'authenticated user with valid permissions' do
       it 'deletes trigger' do
         expect do
-          delete api("/projects/#{project.id}/triggers/#{trigger.token}", user)
+          delete api("/projects/#{project.id}/triggers/#{trigger.id}", user)
 
           expect(response).to have_http_status(204)
         end.to change{project.triggers.count}.by(-1)
       end
 
       it 'responds with 404 Not Found if requesting non-existing trigger' do
-        delete api("/projects/#{project.id}/triggers/abcdef012345", user)
+        delete api("/projects/#{project.id}/triggers/-5", user)
 
         expect(response).to have_http_status(404)
       end
@@ -204,7 +287,7 @@ describe API::Triggers do
 
     context 'authenticated user with invalid permissions' do
       it 'does not delete trigger' do
-        delete api("/projects/#{project.id}/triggers/#{trigger.token}", user2)
+        delete api("/projects/#{project.id}/triggers/#{trigger.id}", user2)
 
         expect(response).to have_http_status(403)
       end
@@ -212,7 +295,7 @@ describe API::Triggers do
 
     context 'unauthenticated user' do
       it 'does not delete trigger' do
-        delete api("/projects/#{project.id}/triggers/#{trigger.token}")
+        delete api("/projects/#{project.id}/triggers/#{trigger.id}")
 
         expect(response).to have_http_status(401)
       end
diff --git a/spec/requests/api/v3/triggers_spec.rb b/spec/requests/api/v3/triggers_spec.rb
index 721ce4a361b028a13d849156eb546c92c427a6ed..4819269d69f9c8cfe69ac992e7b70685efff413a 100644
--- a/spec/requests/api/v3/triggers_spec.rb
+++ b/spec/requests/api/v3/triggers_spec.rb
@@ -11,6 +11,177 @@ describe API::V3::Triggers do
   let!(:developer) { create(:project_member, :developer, user: user2, project: project) }
   let!(:trigger) { create(:ci_trigger, project: project, token: trigger_token) }
 
+  describe 'POST /projects/:project_id/trigger' do
+    let!(:project2) { create(:project) }
+    let(:options) do
+      {
+        token: trigger_token
+      }
+    end
+
+    before do
+      stub_ci_pipeline_to_return_yaml_file
+    end
+
+    context 'Handles errors' do
+      it 'returns bad request if token is missing' do
+        post v3_api("/projects/#{project.id}/trigger/builds"), ref: 'master'
+        expect(response).to have_http_status(400)
+      end
+
+      it 'returns not found if project is not found' do
+        post v3_api('/projects/0/trigger/builds'), options.merge(ref: 'master')
+        expect(response).to have_http_status(404)
+      end
+
+      it 'returns unauthorized if token is for different project' do
+        post v3_api("/projects/#{project2.id}/trigger/builds"), options.merge(ref: 'master')
+        expect(response).to have_http_status(401)
+      end
+    end
+
+    context 'Have a commit' do
+      let(:pipeline) { project.pipelines.last }
+
+      it 'creates builds' do
+        post v3_api("/projects/#{project.id}/trigger/builds"), options.merge(ref: 'master')
+        expect(response).to have_http_status(201)
+        pipeline.builds.reload
+        expect(pipeline.builds.pending.size).to eq(2)
+        expect(pipeline.builds.size).to eq(5)
+      end
+
+      it 'creates builds on webhook from other gitlab repository and branch' do
+        expect do
+          post v3_api("/projects/#{project.id}/ref/master/trigger/builds?token=#{trigger_token}"), { ref: 'refs/heads/other-branch' }
+        end.to change(project.builds, :count).by(5)
+        expect(response).to have_http_status(201)
+      end
+
+      it 'returns bad request with no builds created if there\'s no commit for that ref' do
+        post v3_api("/projects/#{project.id}/trigger/builds"), options.merge(ref: 'other-branch')
+        expect(response).to have_http_status(400)
+        expect(json_response['message']).to eq('No builds created')
+      end
+
+      context 'Validates variables' do
+        let(:variables) do
+          { 'TRIGGER_KEY' => 'TRIGGER_VALUE' }
+        end
+
+        it 'validates variables to be a hash' do
+          post v3_api("/projects/#{project.id}/trigger/builds"), options.merge(variables: 'value', ref: 'master')
+          expect(response).to have_http_status(400)
+          expect(json_response['error']).to eq('variables is invalid')
+        end
+
+        it 'validates variables needs to be a map of key-valued strings' do
+          post v3_api("/projects/#{project.id}/trigger/builds"), options.merge(variables: { key: %w(1 2) }, ref: 'master')
+          expect(response).to have_http_status(400)
+          expect(json_response['message']).to eq('variables needs to be a map of key-valued strings')
+        end
+
+        it 'creates trigger request with variables' do
+          post v3_api("/projects/#{project.id}/trigger/builds"), options.merge(variables: variables, ref: 'master')
+          expect(response).to have_http_status(201)
+          pipeline.builds.reload
+          expect(pipeline.builds.first.trigger_request.variables).to eq(variables)
+        end
+      end
+    end
+  end
+
+  describe 'GET /projects/:id/triggers' do
+    context 'authenticated user with valid permissions' do
+      it 'returns list of triggers' do
+        get v3_api("/projects/#{project.id}/triggers", user)
+
+        expect(response).to have_http_status(200)
+        expect(response).to include_pagination_headers
+        expect(json_response).to be_a(Array)
+        expect(json_response[0]).to have_key('token')
+      end
+    end
+
+    context 'authenticated user with invalid permissions' do
+      it 'does not return triggers list' do
+        get v3_api("/projects/#{project.id}/triggers", user2)
+
+        expect(response).to have_http_status(403)
+      end
+    end
+
+    context 'unauthenticated user' do
+      it 'does not return triggers list' do
+        get v3_api("/projects/#{project.id}/triggers")
+
+        expect(response).to have_http_status(401)
+      end
+    end
+  end
+
+  describe 'GET /projects/:id/triggers/:token' do
+    context 'authenticated user with valid permissions' do
+      it 'returns trigger details' do
+        get v3_api("/projects/#{project.id}/triggers/#{trigger.token}", user)
+
+        expect(response).to have_http_status(200)
+        expect(json_response).to be_a(Hash)
+      end
+
+      it 'responds with 404 Not Found if requesting non-existing trigger' do
+        get v3_api("/projects/#{project.id}/triggers/abcdef012345", user)
+
+        expect(response).to have_http_status(404)
+      end
+    end
+
+    context 'authenticated user with invalid permissions' do
+      it 'does not return triggers list' do
+        get v3_api("/projects/#{project.id}/triggers/#{trigger.token}", user2)
+
+        expect(response).to have_http_status(403)
+      end
+    end
+
+    context 'unauthenticated user' do
+      it 'does not return triggers list' do
+        get v3_api("/projects/#{project.id}/triggers/#{trigger.token}")
+
+        expect(response).to have_http_status(401)
+      end
+    end
+  end
+
+  describe 'POST /projects/:id/triggers' do
+    context 'authenticated user with valid permissions' do
+      it 'creates trigger' do
+        expect do
+          post v3_api("/projects/#{project.id}/triggers", user)
+        end.to change{project.triggers.count}.by(1)
+
+        expect(response).to have_http_status(201)
+        expect(json_response).to be_a(Hash)
+      end
+    end
+
+    context 'authenticated user with invalid permissions' do
+      it 'does not create trigger' do
+        post v3_api("/projects/#{project.id}/triggers", user2)
+
+        expect(response).to have_http_status(403)
+      end
+    end
+
+    context 'unauthenticated user' do
+      it 'does not create trigger' do
+        post v3_api("/projects/#{project.id}/triggers")
+
+        expect(response).to have_http_status(401)
+      end
+    end
+  end
+
   describe 'DELETE /projects/:id/triggers/:token' do
     context 'authenticated user with valid permissions' do
       it 'deletes trigger' do
diff --git a/spec/routing/project_routing_spec.rb b/spec/routing/project_routing_spec.rb
index a5bc62ef6c2b4a6bc554d8e8b051610dac8f914d..d31f1bdfb7c634e68d176e0640b32a80662ba4a1 100644
--- a/spec/routing/project_routing_spec.rb
+++ b/spec/routing/project_routing_spec.rb
@@ -431,12 +431,22 @@ describe 'project routing' do
     end
   end
 
-  #         project_notes GET    /:project_id/notes(.:format)         notes#index
-  #                       POST   /:project_id/notes(.:format)         notes#create
-  #          project_note DELETE /:project_id/notes/:id(.:format)     notes#destroy
+  # project_noteable_notes GET    /:project_id/noteable/:target_type/:target_id/notes notes#index
+  #                        POST   /:project_id/notes(.:format)                        notes#create
+  #           project_note DELETE /:project_id/notes/:id(.:format)                    notes#destroy
   describe Projects::NotesController, 'routing' do
+    it 'to #index' do
+      expect(get('/gitlab/gitlabhq/noteable/issue/1/notes')).to route_to(
+        'projects/notes#index',
+        namespace_id: 'gitlab',
+        project_id: 'gitlabhq',
+        target_type: 'issue',
+        target_id: '1'
+      )
+    end
+
     it_behaves_like 'RESTful project resources' do
-      let(:actions)    { [:index, :create, :destroy] }
+      let(:actions)    { [:create, :destroy] }
       let(:controller) { 'notes' }
     end
   end
diff --git a/spec/services/ci/create_trigger_request_service_spec.rb b/spec/services/ci/create_trigger_request_service_spec.rb
index d8c443d29d5646d8ee788f8aad5f6f55262d89d5..5e68343784d8b08bbb3ff99c658a8903c4236cf8 100644
--- a/spec/services/ci/create_trigger_request_service_spec.rb
+++ b/spec/services/ci/create_trigger_request_service_spec.rb
@@ -13,8 +13,22 @@ describe Ci::CreateTriggerRequestService, services: true do
     context 'valid params' do
       subject { service.execute(project, trigger, 'master') }
 
-      it { expect(subject).to be_kind_of(Ci::TriggerRequest) }
-      it { expect(subject.builds.first).to be_kind_of(Ci::Build) }
+      context 'without owner' do
+        it { expect(subject).to be_kind_of(Ci::TriggerRequest) }
+        it { expect(subject.pipeline).to be_kind_of(Ci::Pipeline) }
+        it { expect(subject.builds.first).to be_kind_of(Ci::Build) }
+      end
+
+      context 'with owner' do
+        let(:owner) { create(:user) }
+        let(:trigger) { create(:ci_trigger, project: project, owner: owner) }
+
+        it { expect(subject).to be_kind_of(Ci::TriggerRequest) }
+        it { expect(subject.pipeline).to be_kind_of(Ci::Pipeline) }
+        it { expect(subject.pipeline.user).to eq(owner) }
+        it { expect(subject.builds.first).to be_kind_of(Ci::Build) }
+        it { expect(subject.builds.first.user).to eq(owner) }
+      end
     end
 
     context 'no commit for ref' do
diff --git a/spec/services/projects/update_pages_service_spec.rb b/spec/services/projects/update_pages_service_spec.rb
index 411b22a0fb83d5d198b71c05e3014fd6efdf64fa..f75fdd9e03f2defde3f06b5e816a3a76bd750e6e 100644
--- a/spec/services/projects/update_pages_service_spec.rb
+++ b/spec/services/projects/update_pages_service_spec.rb
@@ -26,6 +26,28 @@ describe Projects::UpdatePagesService do
         build.update_attributes(artifacts_metadata: metadata)
       end
 
+      describe 'pages artifacts' do
+        context 'with expiry date' do
+          before do
+            build.artifacts_expire_in = "2 days"
+          end
+
+          it "doesn't delete artifacts" do
+            expect(execute).to eq(:success)
+
+            expect(build.reload.artifacts_file?).to eq(true)
+          end
+        end
+
+        context 'without expiry date' do
+          it "does delete artifacts" do
+            expect(execute).to eq(:success)
+
+            expect(build.reload.artifacts_file?).to eq(false)
+          end
+        end
+      end
+
       it 'succeeds' do
         expect(project.pages_deployed?).to be_falsey
         expect(execute).to eq(:success)
diff --git a/spec/tasks/gitlab/backup_rake_spec.rb b/spec/tasks/gitlab/backup_rake_spec.rb
index df8a47893f9034935a6ba159ec8852c4cbc4619a..dfbfbd05f43ac1cadf56581cdd4641ea3e44bee9 100644
--- a/spec/tasks/gitlab/backup_rake_spec.rb
+++ b/spec/tasks/gitlab/backup_rake_spec.rb
@@ -108,7 +108,7 @@ describe 'gitlab:app namespace rake task' do
       $stdout = orig_stdout
     end
 
-    describe 'backup creation and deletion using annex and custom_hooks' do
+    describe 'backup creation and deletion using custom_hooks' do
       let(:project) { create(:project) }
       let(:user_backup_path) { "repositories/#{project.path_with_namespace}" }
 
@@ -132,25 +132,6 @@ describe 'gitlab:app namespace rake task' do
         Dir.chdir(@origin_cd)
       end
 
-      context 'project uses git-annex and successfully creates backup' do
-        let(:filename) { "annex" }
-
-        it 'creates annex.tar and project bundle' do
-          tar_contents, exit_status = Gitlab::Popen.popen(%W{tar -tvf #{@backup_tar}})
-
-          expect(exit_status).to eq(0)
-          expect(tar_contents).to match(user_backup_path)
-          expect(tar_contents).to match("#{user_backup_path}/annex.tar")
-          expect(tar_contents).to match("#{user_backup_path}.bundle")
-        end
-
-        it 'restores files correctly' do
-          restore_backup
-
-          expect(Dir.entries(File.join(project.repository.path, "annex"))).to include("dummy.txt")
-        end
-      end
-
       context 'project uses custom_hooks and successfully creates backup' do
         let(:filename) { "custom_hooks" }