diff --git a/GITLAB_WORKHORSE_VERSION b/GITLAB_WORKHORSE_VERSION
index 9084fa2f716a7117829f3f32a5f4cef400e02903..524cb55242b53f6a64cc646ea05db6acc7696d2d 100644
--- a/GITLAB_WORKHORSE_VERSION
+++ b/GITLAB_WORKHORSE_VERSION
@@ -1 +1 @@
-1.1.0
+1.1.1
diff --git a/app/assets/javascripts/gl_dropdown.js b/app/assets/javascripts/gl_dropdown.js
index 969778dded7d99357e1e7c43c7500c47b160301e..9a91018a8e4647bc9ad2fa1c04c3112ff1b67bb1 100644
--- a/app/assets/javascripts/gl_dropdown.js
+++ b/app/assets/javascripts/gl_dropdown.js
@@ -650,6 +650,11 @@
       } else if(value) {
         field = this.dropdown.parent().find("input[name='" + fieldName + "'][value='" + value.toString().replace(/'/g, '\\\'') + "']");
       }
+
+      if (this.options.isSelectable && !this.options.isSelectable(selectedObject, el)) {
+        return;
+      }
+
       if (el.hasClass(ACTIVE_CLASS)) {
         el.removeClass(ACTIVE_CLASS);
         if (field && field.length) {
diff --git a/app/assets/javascripts/members.js.es6 b/app/assets/javascripts/members.js.es6
index 895bc10784ff5ff18439ae17279a5efc1560090d..e3f367a11eb94637229e1ac77c12f1e2272a1930 100644
--- a/app/assets/javascripts/members.js.es6
+++ b/app/assets/javascripts/members.js.es6
@@ -1,38 +1,81 @@
-/* eslint-disable */
-((w) => {
-  w.gl = w.gl || {};
+/* eslint-disable class-methods-use-this */
+(() => {
+  window.gl = window.gl || {};
 
   class Members {
     constructor() {
       this.addListeners();
+      this.initGLDropdown();
     }
 
     addListeners() {
       $('.project_member, .group_member').off('ajax:success').on('ajax:success', this.removeRow);
-      $('.js-member-update-control').off('change').on('change', this.formSubmit);
-      $('.js-edit-member-form').off('ajax:success').on('ajax:success', this.formSuccess);
+      $('.js-member-update-control').off('change').on('change', this.formSubmit.bind(this));
+      $('.js-edit-member-form').off('ajax:success').on('ajax:success', this.formSuccess.bind(this));
       gl.utils.disableButtonIfEmptyField('#user_ids', 'input[name=commit]', 'change');
     }
 
+    initGLDropdown() {
+      $('.js-member-permissions-dropdown').each((i, btn) => {
+        const $btn = $(btn);
+
+        $btn.glDropdown({
+          selectable: true,
+          isSelectable(selected, $el) {
+            return !$el.hasClass('is-active');
+          },
+          fieldName: $btn.data('field-name'),
+          id(selected, $el) {
+            return $el.data('id');
+          },
+          toggleLabel(selected, $el) {
+            return $el.text();
+          },
+          clicked: (selected, $link) => {
+            this.formSubmit(null, $link);
+          },
+        });
+      });
+    }
+
     removeRow(e) {
       const $target = $(e.target);
 
       if ($target.hasClass('btn-remove')) {
         $target.closest('.member')
-          .fadeOut(function () {
+          .fadeOut(function fadeOutMemberRow() {
             $(this).remove();
           });
       }
     }
 
-    formSubmit() {
-      $(this).closest('form').trigger("submit.rails").end().disable();
+    formSubmit(e, $el = null) {
+      const $this = e ? $(e.currentTarget) : $el;
+      const { $toggle, $dateInput } = this.getMemberListItems($this);
+
+      $this.closest('form').trigger('submit.rails');
+
+      $toggle.disable();
+      $dateInput.disable();
     }
 
-    formSuccess() {
-      $(this).find('.js-member-update-control').enable();
+    formSuccess(e) {
+      const { $toggle, $dateInput } = this.getMemberListItems($(e.currentTarget).closest('.member'));
+
+      $toggle.enable();
+      $dateInput.enable();
+    }
+
+    getMemberListItems($el) {
+      const $memberListItem = $el.is('.member') ? $el : $(`#${$el.data('el-id')}`);
+
+      return {
+        $memberListItem,
+        $toggle: $memberListItem.find('.dropdown-menu-toggle'),
+        $dateInput: $memberListItem.find('.js-access-expiration-date'),
+      };
     }
   }
 
   gl.Members = Members;
-})(window);
+})();
diff --git a/app/assets/javascripts/merge_request_widget.js.es6 b/app/assets/javascripts/merge_request_widget.js.es6
index a55fe9df0b3004c01eeedab7df9a7563b0f0b41b..d9495e503888ac4af03e7a0c5a724c3110bc8b6c 100644
--- a/app/assets/javascripts/merge_request_widget.js.es6
+++ b/app/assets/javascripts/merge_request_widget.js.es6
@@ -101,7 +101,7 @@
               urlSuffix = deleteSourceBranch ? '?deleted_source_branch=true' : '';
               return window.location.href = window.location.pathname + urlSuffix;
             } else if (data.merge_error) {
-              return this.$widgetBody.html("<h4>" + data.merge_error + "</h4>");
+              return _this.$widgetBody.html("<h4>" + data.merge_error + "</h4>");
             } else {
               callback = function() {
                 return merge_request_widget.mergeInProgress(deleteSourceBranch);
diff --git a/app/assets/stylesheets/framework/buttons.scss b/app/assets/stylesheets/framework/buttons.scss
index 8da3da2ad08b11c08d1e11437330771551f800ec..1c7b2f4df7cc01882f52a1548ae9a5ab74cd9916 100644
--- a/app/assets/stylesheets/framework/buttons.scss
+++ b/app/assets/stylesheets/framework/buttons.scss
@@ -1,7 +1,7 @@
 @mixin btn-default {
   border-radius: 3px;
   font-size: $gl-font-size;
-  font-weight: 500;
+  font-weight: 400;
   padding: $gl-vert-padding $gl-btn-padding;
 
   &:focus,
diff --git a/app/assets/stylesheets/framework/dropdowns.scss b/app/assets/stylesheets/framework/dropdowns.scss
index 33de652c06fce4e69022a328c4b804601f736f3a..d5914b900e217ee5086876cc4a1f5ddf94801fdf 100644
--- a/app/assets/stylesheets/framework/dropdowns.scss
+++ b/app/assets/stylesheets/framework/dropdowns.scss
@@ -42,6 +42,11 @@
   border-radius: $border-radius-base;
   white-space: nowrap;
 
+  &[disabled] {
+    background-color: $input-bg-disabled;
+    cursor: not-allowed;
+  }
+
   &.no-outline {
     outline: 0;
   }
diff --git a/app/assets/stylesheets/pages/members.scss b/app/assets/stylesheets/pages/members.scss
index 756efa9c7fa027433b0643a602b32f02e10c0166..5f3bbb40ba08a6e8c9dc516632d6843cdf88c1da 100644
--- a/app/assets/stylesheets/pages/members.scss
+++ b/app/assets/stylesheets/pages/members.scss
@@ -54,6 +54,10 @@
   @media (min-width: $screen-sm-min) {
     width: 50%;
   }
+
+  .dropdown-menu-toggle {
+    width: 100%;
+  }
 }
 
 .member-access-text {
diff --git a/app/assets/stylesheets/pages/notifications.scss b/app/assets/stylesheets/pages/notifications.scss
index 94fbbef3c778868d380f6d84c68ea6a4a67b3ca1..7d61390a439915cebad2313c3a223864d77632ed 100644
--- a/app/assets/stylesheets/pages/notifications.scss
+++ b/app/assets/stylesheets/pages/notifications.scss
@@ -1,5 +1,9 @@
 .notification-list-item {
   line-height: 34px;
+
+  .dropdown-menu {
+    @extend .dropdown-menu-align-right;
+  }
 }
 
 .notification {
diff --git a/app/assets/stylesheets/pages/projects.scss b/app/assets/stylesheets/pages/projects.scss
index 72b6685d9401818186354ee00bff27c5f595fa21..6e0f6b1cd8130277e2abb4303387d6f93e1f35dd 100644
--- a/app/assets/stylesheets/pages/projects.scss
+++ b/app/assets/stylesheets/pages/projects.scss
@@ -188,6 +188,10 @@
     margin-left: 10px;
   }
 
+  .notification-dropdown .dropdown-menu {
+    @extend .dropdown-menu-align-right;
+  }
+
   .download-button {
     @media (max-width: $screen-md-max) {
       margin-left: 0;
diff --git a/app/controllers/projects/project_members_controller.rb b/app/controllers/projects/project_members_controller.rb
index 3fb8bba3cd079b4fa25386ab46aab7b09fb0d503..53308948f623ce6bdfc605ed08fc37212f94f6ea 100644
--- a/app/controllers/projects/project_members_controller.rb
+++ b/app/controllers/projects/project_members_controller.rb
@@ -35,13 +35,12 @@ class Projects::ProjectMembersController < Projects::ApplicationController
       @group_links = @project.project_group_links.where(group_id: @project.invited_groups.search(params[:search]).select(:id))
     end
 
-    member_ids = @project_members.pluck(:id)
+    wheres = ["id IN (#{@project_members.select(:id).to_sql})"]
+    wheres << "id IN (#{group_members.select(:id).to_sql})" if group_members
 
-    if group_members
-      member_ids += group_members.pluck(:id)
-    end
-
-    @project_members = Member.where(id: member_ids).order(access_level: :desc).page(params[:page])
+    @project_members = Member.
+      where(wheres.join(' OR ')).
+      order(access_level: :desc).page(params[:page])
 
     @requesters = AccessRequestsFinder.new(@project).execute(current_user)
 
diff --git a/app/controllers/sessions_controller.rb b/app/controllers/sessions_controller.rb
index 38e7c6f4a4859e4bd9fe062e1b0c7cd53eec6a35..8c698695202681347dcc3e332a3df2c1929a7f4d 100644
--- a/app/controllers/sessions_controller.rb
+++ b/app/controllers/sessions_controller.rb
@@ -37,6 +37,12 @@ class SessionsController < Devise::SessionsController
     end
   end
 
+  def destroy
+    super
+    # hide the signed_out notice
+    flash[:notice] = nil
+  end
+
   private
 
   # Handle an "initial setup" state, where there's only one user, it's an admin,
diff --git a/app/finders/snippets_finder.rb b/app/finders/snippets_finder.rb
index 00ff161103932ede759303894fecd0cbda80d564..0586a923a74b4be0fc2fe7d1836a783712dc8df6 100644
--- a/app/finders/snippets_finder.rb
+++ b/app/finders/snippets_finder.rb
@@ -1,12 +1,15 @@
 class SnippetsFinder
   def execute(current_user, params = {})
     filter = params[:filter]
+    user = params.fetch(:user, current_user)
 
     case filter
     when :all then
       snippets(current_user).fresh
+    when :public then
+      Snippet.are_public.fresh
     when :by_user then
-      by_user(current_user, params[:user], params[:scope])
+      by_user(current_user, user, params[:scope])
     when :by_project
       by_project(current_user, params[:project])
     end
diff --git a/app/helpers/gitlab_routing_helper.rb b/app/helpers/gitlab_routing_helper.rb
index af9087d8326aa4b9b580a0f05e30cae8c6eaac7e..99db73c9ee04b2778e922140cf3fbb9266afc47b 100644
--- a/app/helpers/gitlab_routing_helper.rb
+++ b/app/helpers/gitlab_routing_helper.rb
@@ -159,6 +159,11 @@ module GitlabRoutingHelper
     resend_invite_namespace_project_project_member_path(project_member.source.namespace, project_member.source, project_member)
   end
 
+  # Snippets
+  def personal_snippet_url(snippet, *args)
+    snippet_url(snippet)
+  end
+
   # Groups
 
   ## Members
diff --git a/app/policies/personal_snippet_policy.rb b/app/policies/personal_snippet_policy.rb
index 46c5aa1a5be4849b8498d856873707af84b6b8b7..d3913986cd81484b6298cb0c9da9cc78c27a0f37 100644
--- a/app/policies/personal_snippet_policy.rb
+++ b/app/policies/personal_snippet_policy.rb
@@ -6,9 +6,14 @@ class PersonalSnippetPolicy < BasePolicy
     if @subject.author == @user
       can! :read_personal_snippet
       can! :update_personal_snippet
+      can! :destroy_personal_snippet
       can! :admin_personal_snippet
     end
 
+    unless @user.external?
+      can! :create_personal_snippet
+    end
+
     if @subject.internal? && !@user.external?
       can! :read_personal_snippet
     end
diff --git a/app/views/groups/group_members/update.js.haml b/app/views/groups/group_members/update.js.haml
index de8f53b6b52ba27088c628c6970303ace0a6fbc7..9d05bff6c4e0cbf67844075f5d9cb043b9add53d 100644
--- a/app/views/groups/group_members/update.js.haml
+++ b/app/views/groups/group_members/update.js.haml
@@ -1,3 +1,4 @@
 :plain
   var $listItem = $('#{escape_javascript(render('shared/members/member', member: @group_member))}');
   $("##{dom_id(@group_member)} .list-item-name").replaceWith($listItem.find('.list-item-name'));
+  gl.utils.localTimeAgo($('.js-timeago'), $("##{dom_id(@group_member)}"));
diff --git a/app/views/help/show.html.haml b/app/views/help/show.html.haml
index be257b51b9ee2414ae5c303bcae85bfd12f34d8d..f6ebd76af9d8c76ea7f60909283e47b914983430 100644
--- a/app/views/help/show.html.haml
+++ b/app/views/help/show.html.haml
@@ -1,3 +1,3 @@
 - page_title @path.split("/").reverse.map(&:humanize)
 .documentation.wiki
-  = markdown @markdown.gsub('$your_email', current_user.try(:email) || "email@example.com")
+  = markdown @markdown
diff --git a/app/views/layouts/nav/_group_settings.html.haml b/app/views/layouts/nav/_group_settings.html.haml
index c0328fe884247f14bbb3548203533db09e823000..1579d8f16625b4a0aaa89e1f969c5c7545f762e3 100644
--- a/app/views/layouts/nav/_group_settings.html.haml
+++ b/app/views/layouts/nav/_group_settings.html.haml
@@ -1,10 +1,8 @@
 - if current_user
   - can_admin_group = can?(current_user, :admin_group, @group)
   - can_edit = can?(current_user, :admin_group, @group)
-  - member = @group.members.find_by(user_id: current_user.id)
-  - can_leave = member && can?(current_user, :destroy_group_member, member)
 
-  - if can_admin_group || can_edit || can_leave
+  - if can_admin_group || can_edit
     .controls
       .dropdown.group-settings-dropdown
         %a.dropdown-new.btn.btn-default#group-settings-button{href: '#', 'data-toggle' => 'dropdown'}
@@ -14,13 +12,7 @@
           - if can_admin_group
             = nav_link(path: 'groups#projects') do
               = link_to 'Projects', projects_group_path(@group), title: 'Projects'
-          - if (can_edit || can_leave) && can_admin_group
+          - if can_edit && can_admin_group
             %li.divider
-          - if can_edit
             %li
               = link_to 'Edit Group', edit_group_path(@group)
-          - if can_leave
-            %li
-              = link_to polymorphic_path([:leave, @group, :members]),
-                data: { confirm: leave_confirmation_message(@group) }, method: :delete, title: 'Leave group' do
-                Leave Group
diff --git a/app/views/layouts/nav/_project.html.haml b/app/views/layouts/nav/_project.html.haml
index 7bd11f5727ac9370a3c94e1fccea417f5f73ca00..904d11c2cf4d40b542c0f8fbd7e7e2843de907a8 100644
--- a/app/views/layouts/nav/_project.html.haml
+++ b/app/views/layouts/nav/_project.html.haml
@@ -6,23 +6,14 @@
         = icon('caret-down')
       %ul.dropdown-menu.dropdown-menu-align-right
         - can_edit = can?(current_user, :admin_project, @project)
-        -# We don't use @project.team.find_member because it searches for group members too...
-        - member = @project.members.find_by(user_id: current_user.id)
-        - can_leave = member && can?(current_user, :destroy_project_member, member)
 
         = render 'layouts/nav/project_settings', can_edit: can_edit
 
-        - if can_edit || can_leave
+        - if can_edit
           %li.divider
-          - if can_edit
-            %li
-              = link_to edit_project_path(@project) do
-                Edit Project
-          - if can_leave
-            %li
-              = link_to polymorphic_path([:leave, @project, :members]),
-                data: { confirm: leave_confirmation_message(@project) }, method: :delete, title: 'Leave project' do
-                Leave Project
+          %li
+            = link_to edit_project_path(@project) do
+              Edit Project
 
 .scrolling-tabs-container{ class: nav_control_class }
   .fade-left
diff --git a/app/views/profiles/notifications/show.html.haml b/app/views/profiles/notifications/show.html.haml
index 844fce59704582e4a3382de5c99d4b167d7de154..d79a1a9f3682db4462eccbcb826da607165b96c0 100644
--- a/app/views/profiles/notifications/show.html.haml
+++ b/app/views/profiles/notifications/show.html.haml
@@ -30,7 +30,7 @@
       %br
       .clearfix
       .form-group.pull-left.global-notification-setting
-        = render 'shared/notifications/button', notification_setting: @global_notification_setting, left_align: true
+        = render 'shared/notifications/button', notification_setting: @global_notification_setting
 
       .clearfix
 
diff --git a/app/views/projects/edit.html.haml b/app/views/projects/edit.html.haml
index 01cd8fa093894606a2b2d4877d473d53a16098f4..38e7fc4279ca35bb80384ab6af330f978d000140 100644
--- a/app/views/projects/edit.html.haml
+++ b/app/views/projects/edit.html.haml
@@ -146,7 +146,7 @@
           such as compressing file revisions and removing unreachable objects.
     .col-lg-9
       = link_to 'Housekeeping', housekeeping_namespace_project_path(@project.namespace, @project),
-          method: :post, class: "btn btn-save"
+          method: :post, class: "btn btn-default"
   %hr
   .row.prepend-top-default
     .col-lg-3
diff --git a/app/views/projects/group_links/update.js.haml b/app/views/projects/group_links/update.js.haml
index af9a5b190600c0bf5f4fb93c0260d4456003d5ea..55520fda4943b6e6de1e8dd0af931c0448fa4c85 100644
--- a/app/views/projects/group_links/update.js.haml
+++ b/app/views/projects/group_links/update.js.haml
@@ -1,3 +1,4 @@
 :plain
   var $listItem = $('#{escape_javascript(render('shared/members/group', group_link: @group_link))}');
   $("#group_member_#{@group_link.id} .list-item-name").replaceWith($listItem.find('.list-item-name'));
+  gl.utils.localTimeAgo($('.js-timeago'), $("#group_member_#{@group_link.id}"));
diff --git a/app/views/projects/project_members/update.js.haml b/app/views/projects/project_members/update.js.haml
index 91927181efbeba7fd4145f1a203189c5859e34a6..d15f4310ff5ed228fa3825bc442b35812b0b29b5 100644
--- a/app/views/projects/project_members/update.js.haml
+++ b/app/views/projects/project_members/update.js.haml
@@ -1,3 +1,4 @@
 :plain
   var $listItem = $('#{escape_javascript(render('shared/members/member', member: @project_member))}');
   $("##{dom_id(@project_member)} .list-item-name").replaceWith($listItem.find('.list-item-name'));
+  gl.utils.localTimeAgo($('.js-timeago'), $("##{dom_id(@project_member)}"));
diff --git a/app/views/shared/members/_access_request_buttons.html.haml b/app/views/shared/members/_access_request_buttons.html.haml
index eff914398bb0251a5e84de11e81a25c557bf6d6d..e166dfab710f82ff8104aee110bdf52ab6af682d 100644
--- a/app/views/shared/members/_access_request_buttons.html.haml
+++ b/app/views/shared/members/_access_request_buttons.html.haml
@@ -1,10 +1,16 @@
-- if can?(current_user, :request_access, source)
-  - if requester = source.requesters.find_by(user_id: current_user.id)
-    = link_to 'Withdraw Access Request', polymorphic_path([:leave, source, :members]),
-              method: :delete,
-              data: { confirm: remove_member_message(requester) },
-              class: 'btn'
-  - else
-    = link_to 'Request Access', polymorphic_path([:request_access, source, :members]),
-              method: :post,
-              class: 'btn'
+- model_name = source.model_name.to_s.downcase
+
+- if can?(current_user, :"destroy_#{model_name}_member", source.members.find_by(user_id: current_user.id))
+  = link_to "Leave #{model_name}", polymorphic_path([:leave, source, :members]),
+            method: :delete,
+            data: { confirm: leave_confirmation_message(source) },
+            class: 'btn'
+- elsif requester = source.requesters.find_by(user_id: current_user.id)
+  = link_to 'Withdraw Access Request', polymorphic_path([:leave, source, :members]),
+            method: :delete,
+            data: { confirm: remove_member_message(requester) },
+            class: 'btn'
+- elsif source.request_access_enabled && can?(current_user, :request_access, source)
+  = link_to 'Request Access', polymorphic_path([:request_access, source, :members]),
+            method: :post,
+            class: 'btn'
diff --git a/app/views/shared/members/_group.html.haml b/app/views/shared/members/_group.html.haml
index 1c0346bbc78f9269a1625ca540237e7f0937f438..8928de9097b1465fb4f7e920cbe538c17203a999 100644
--- a/app/views/shared/members/_group.html.haml
+++ b/app/views/shared/members/_group.html.haml
@@ -1,7 +1,8 @@
 - group_link = local_assigns[:group_link]
 - group = group_link.group
 - can_admin_member = can?(current_user, :admin_project_member, @project)
-%li.member.group_member{ id: "group_member_#{group_link.id}" }
+- dom_id = "group_member_#{group_link.id}"
+%li.member.group_member{ id: dom_id }
   %span{ class: "list-item-name" }
     = image_tag group_icon(group), class: "avatar s40", alt: ''
     %strong
@@ -14,7 +15,23 @@
           Expires in #{distance_of_time_in_words_to_now(group_link.expires_at)}
   .controls.member-controls
     = form_tag namespace_project_group_link_path(@project.namespace, @project, group_link), method: :put, remote: true, class: 'form-horizontal js-edit-member-form' do
-      = select_tag 'group_link[group_access]', options_for_select(ProjectGroupLink.access_options, group_link.group_access), class: 'form-control member-form-control append-right-5 js-member-update-control', id: "member_access_level_#{group.id}", disabled: !can_admin_member
+      = hidden_field_tag "group_link[group_access]", group_link.group_access
+      .member-form-control.dropdown.append-right-5
+        %button.dropdown-menu-toggle.js-member-permissions-dropdown{ type: "button",
+          disabled: !can_admin_member,
+          data: { toggle: "dropdown", field_name: "group_link[group_access]" } }
+          %span.dropdown-toggle-text
+            = group_link.human_access
+          = icon("chevron-down")
+        .dropdown-menu.dropdown-select.dropdown-menu-align-right.dropdown-menu-selectable
+          = dropdown_title("Change permissions")
+          .dropdown-content
+            %ul
+              - Gitlab::Access.options.each do |role, role_id|
+                %li
+                  = link_to role, "javascript:void(0)",
+                    class: ("is-active" if group_link.group_access == role_id),
+                    data: { id: role_id, el_id: dom_id }
       .prepend-left-5.clearable-input.member-form-control
         = text_field_tag 'group_link[expires_at]', group_link.expires_at, class: 'form-control js-access-expiration-date js-member-update-control', placeholder: 'Expiration date', id: "member_expires_at_#{group.id}", disabled: !can_admin_member
         %i.clear-icon.js-clear-input
diff --git a/app/views/shared/members/_member.html.haml b/app/views/shared/members/_member.html.haml
index e67f7d5a352e65b8b9c58b039e3246aae5093d23..659d4c905fc9822bec6913659ae2899668bc806f 100644
--- a/app/views/shared/members/_member.html.haml
+++ b/app/views/shared/members/_member.html.haml
@@ -48,9 +48,25 @@
       - if show_controls && (member.respond_to?(:group) && @group) || (member.respond_to?(:project) && @project)
         - if user != current_user
           = form_for member, remote: true, html: { class: 'form-horizontal js-edit-member-form' } do |f|
-            = f.select :access_level, options_for_select(member.class.access_level_roles, member.access_level), {}, class: 'form-control member-form-control append-right-5 js-member-update-control', id: "member_access_level_#{member.id}", disabled: !can_admin_member
+            = f.hidden_field :access_level
+            .member-form-control.dropdown.append-right-5
+              %button.dropdown-menu-toggle.js-member-permissions-dropdown{ type: "button",
+                disabled: !can_admin_member,
+                data: { toggle: "dropdown", field_name: "#{f.object_name}[access_level]" } }
+                %span.dropdown-toggle-text
+                  = member.human_access
+                = icon("chevron-down")
+              .dropdown-menu.dropdown-select.dropdown-menu-align-right.dropdown-menu-selectable
+                = dropdown_title("Change permissions")
+                .dropdown-content
+                  %ul
+                    - Gitlab::Access.options.each do |role, role_id|
+                      %li
+                        = link_to role, "javascript:void(0)",
+                          class: ("is-active" if member.access_level == role_id),
+                          data: { id: role_id, el_id: dom_id(member) }
             .prepend-left-5.clearable-input.member-form-control
-              = f.text_field :expires_at, class: 'form-control js-access-expiration-date js-member-update-control', placeholder: 'Expiration date', id: "member_expires_at_#{member.id}", disabled: !can_admin_member
+              = f.text_field :expires_at, class: 'form-control js-access-expiration-date js-member-update-control', placeholder: 'Expiration date', id: "member_expires_at_#{member.id}", disabled: !can_admin_member, data: { el_id: dom_id(member) }
               %i.clear-icon.js-clear-input
         - else
           %span.member-access-text= member.human_access
diff --git a/app/views/shared/notifications/_button.html.haml b/app/views/shared/notifications/_button.html.haml
index 1f7df0bcd194a22cb3f265884c2991341464a64a..fbad0d05de308a9402f4df27e9b4150eb13eaa72 100644
--- a/app/views/shared/notifications/_button.html.haml
+++ b/app/views/shared/notifications/_button.html.haml
@@ -1,4 +1,3 @@
-- left_align = local_assigns[:left_align]
 - if notification_setting
   .dropdown.notification-dropdown
     = form_for notification_setting, remote: true, html: { class: "inline notification-form" } do |f|
@@ -19,7 +18,7 @@
               = notification_title(notification_setting.level)
               = icon("caret-down")
 
-          = render "shared/notifications/notification_dropdown", notification_setting: notification_setting, left_align: left_align
+          = render "shared/notifications/notification_dropdown", notification_setting: notification_setting
 
           = content_for :scripts_body do
             = render "shared/notifications/custom_notifications", notification_setting: notification_setting
diff --git a/app/views/shared/notifications/_notification_dropdown.html.haml b/app/views/shared/notifications/_notification_dropdown.html.haml
index d3258ee64cb42dacef7106f882e6b773a86509dc..85ad74f9a390b2ba9de3145f19bd325ae1895310 100644
--- a/app/views/shared/notifications/_notification_dropdown.html.haml
+++ b/app/views/shared/notifications/_notification_dropdown.html.haml
@@ -1,5 +1,4 @@
-- left_align = local_assigns[:left_align]
-%ul.dropdown-menu.dropdown-menu-no-wrap.dropdown-menu-selectable.dropdown-menu-large{ role: "menu", class: [notifications_menu_identifier("dropdown", notification_setting), ("dropdown-menu-align-right" unless left_align)] }
+%ul.dropdown-menu.dropdown-menu-no-wrap.dropdown-menu-selectable.dropdown-menu-large{ role: "menu", class: [notifications_menu_identifier("dropdown", notification_setting)] }
   - NotificationSetting.levels.each_key do |level|
     - next if level == "custom"
     - next if level == "global" && notification_setting.source.nil?
diff --git a/changelogs/unreleased/23305-leave-project-and-leave-group-should-be-buttons.yml b/changelogs/unreleased/23305-leave-project-and-leave-group-should-be-buttons.yml
new file mode 100644
index 0000000000000000000000000000000000000000..99dbe4a32a03fe0f6aaf3daec20ede86a3de45da
--- /dev/null
+++ b/changelogs/unreleased/23305-leave-project-and-leave-group-should-be-buttons.yml
@@ -0,0 +1,5 @@
+---
+title: Moved Leave Project and Leave Group buttons to access_request_buttons from
+  the settings dropdown
+merge_request: 7600
+author: 
diff --git a/changelogs/unreleased/25294-remove-signed-out-msg.yml b/changelogs/unreleased/25294-remove-signed-out-msg.yml
new file mode 100644
index 0000000000000000000000000000000000000000..567294fe5f7940e3e62d2701a4be561fa8920507
--- /dev/null
+++ b/changelogs/unreleased/25294-remove-signed-out-msg.yml
@@ -0,0 +1,4 @@
+---
+title: 'fix: removed signed_out notification'
+merge_request: 7958
+author: jnoortheen
diff --git a/changelogs/unreleased/25324-change-housekeeping-btn-to-default.yml b/changelogs/unreleased/25324-change-housekeeping-btn-to-default.yml
new file mode 100644
index 0000000000000000000000000000000000000000..0770f9752a030520e9695f49595bfbfb2b92bf7c
--- /dev/null
+++ b/changelogs/unreleased/25324-change-housekeeping-btn-to-default.yml
@@ -0,0 +1,4 @@
+---
+title: Changed Housekeeping button on project settings page to default styling
+merge_request: 
+author: Ryan Harris
diff --git a/changelogs/unreleased/features-api-snippets.yml b/changelogs/unreleased/features-api-snippets.yml
new file mode 100644
index 0000000000000000000000000000000000000000..80c7bb753594eeb5bbb62d1ec690bfd6d0eafb5d
--- /dev/null
+++ b/changelogs/unreleased/features-api-snippets.yml
@@ -0,0 +1,4 @@
+---
+title: 'API: Endpoint to expose personal snippets as /snippets'
+merge_request: 6373
+author: Bernard Guyzmo Pratz
diff --git a/changelogs/unreleased/issue_24020.yml b/changelogs/unreleased/issue_24020.yml
new file mode 100644
index 0000000000000000000000000000000000000000..87310b7529609357e3925404905c057e86443b75
--- /dev/null
+++ b/changelogs/unreleased/issue_24020.yml
@@ -0,0 +1,4 @@
+---
+title: "fix display hook error message"
+merge_request: 7775
+author: basyura
diff --git a/changelogs/unreleased/issue_25030.yml b/changelogs/unreleased/issue_25030.yml
new file mode 100644
index 0000000000000000000000000000000000000000..e18b8d6a79b9f8146622c043ec28d2494fc55d48
--- /dev/null
+++ b/changelogs/unreleased/issue_25030.yml
@@ -0,0 +1,4 @@
+---
+title: Allow branch names with dots on API endpoint
+merge_request: 
+author: 
diff --git a/changelogs/unreleased/members-dropdowns.yml b/changelogs/unreleased/members-dropdowns.yml
new file mode 100644
index 0000000000000000000000000000000000000000..b15403d6d623636c9c7974b298066954561ada2a
--- /dev/null
+++ b/changelogs/unreleased/members-dropdowns.yml
@@ -0,0 +1,4 @@
+---
+title: Updated members dropdowns
+merge_request: 
+author: 
diff --git a/changelogs/unreleased/update-button-font-weight.yml b/changelogs/unreleased/update-button-font-weight.yml
new file mode 100644
index 0000000000000000000000000000000000000000..ddb3c1c8da4f7c42f1e35743be85a3b8e2a8be13
--- /dev/null
+++ b/changelogs/unreleased/update-button-font-weight.yml
@@ -0,0 +1,4 @@
+---
+title: Updates the font weight of button styles because of the change to system fonts
+merge_request:
+author:
diff --git a/doc/README.md b/doc/README.md
index 66c8c26e4f0611291781f7bc55e386d58cdabdde..eba1e9845b10fe3fd986ddabd49c45a56d9aad3f 100644
--- a/doc/README.md
+++ b/doc/README.md
@@ -1,4 +1,4 @@
-# Documentation
+# GitLab Community Edition documentation
 
 ## User documentation
 
diff --git a/doc/api/merge_requests.md b/doc/api/merge_requests.md
index 81df55ab4aba348dfa4fb2b7b9bdc2433a3f8b9f..662cc9da7335e06cc5516de164359d861e35ac27 100644
--- a/doc/api/merge_requests.md
+++ b/doc/api/merge_requests.md
@@ -429,7 +429,7 @@ DELETE /projects/:id/merge_requests/:merge_request_id
 | `merge_request_id` | integer | yes | The ID of a project's merge request |
 
 ```bash
-curl --request DELETE --header "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" https://gitlab.example.com/api/v3/projects/4/merge_request/85
+curl --request DELETE --header "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" https://gitlab.example.com/api/v3/projects/4/merge_requests/85
 ```
 
 ## Accept MR
diff --git a/doc/api/snippets.md b/doc/api/snippets.md
new file mode 100644
index 0000000000000000000000000000000000000000..5a5dc162ffe29f5feaa974c104db204492c30eee
--- /dev/null
+++ b/doc/api/snippets.md
@@ -0,0 +1,232 @@
+# Snippets
+
+> [Introduced][ce-6373] in GitLab 8.15.
+
+### Snippet visibility level
+
+Snippets in GitLab can be either private, internal, or public.
+You can set it with the `visibility_level` field in the snippet.
+
+Constants for snippet visibility levels are:
+
+| Visibility | Visibility level | Description |
+| ---------- | ---------------- | ----------- |
+| Private    | `0`  | The snippet is visible only to the snippet creator |
+| Internal   | `10` | The snippet is visible for any logged in user |
+| Public     | `20` | The snippet can be accessed without any authentication |
+
+## List snippets
+
+Get a list of current user's snippets.
+
+```
+GET /snippets
+```
+
+## Single snippet
+
+Get a single snippet.
+
+```
+GET /snippets/:id
+```
+
+Parameters:
+
+| Attribute          | Type    | Required | Description                   |
+| ---------          | ----    | -------- | -----------                   |
+| `id`               | Integer | yes      | The ID of a snippet           |
+
+``` bash
+curl --header "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" https://gitlab.example.com/api/v3/snippets/1
+```
+
+Example response:
+
+``` json
+{
+  "id": 1,
+  "title": "test",
+  "file_name": "add.rb",
+  "author": {
+    "id": 1,
+    "username": "john_smith",
+    "email": "john@example.com",
+    "name": "John Smith",
+    "state": "active",
+    "created_at": "2012-05-23T08:00:58Z"
+  },
+  "expires_at": null,
+  "updated_at": "2012-06-28T10:52:04Z",
+  "created_at": "2012-06-28T10:52:04Z",
+  "web_url": "http://example.com/snippets/1",
+}
+```
+
+## Create new snippet
+
+Creates a new snippet. The user must have permission to create new snippets.
+
+```
+POST /snippets
+```
+
+Parameters:
+
+| Attribute          | Type    | Required | Description                |
+| ---------          | ----    | -------- | -----------                |
+| `title`            | String  | yes      | The title of a snippet     |
+| `file_name`        | String  | yes      | The name of a snippet file |
+| `content`          | String  | yes      | The content of a snippet   |
+| `visibility_level` | Integer | yes      | The snippet's visibility   |
+
+
+``` bash
+curl --request POST --data '{"title": "This is a snippet", "content": "Hello world", "file_name": "test.txt", "visibility_level": 10 }' --header "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" https://gitlab.example.com/api/v3/snippets
+```
+
+Example response:
+
+``` json
+{
+  "id": 1,
+  "title": "This is a snippet",
+  "file_name": "test.txt",
+  "author": {
+    "id": 1,
+    "username": "john_smith",
+    "email": "john@example.com",
+    "name": "John Smith",
+    "state": "active",
+    "created_at": "2012-05-23T08:00:58Z"
+  },
+  "expires_at": null,
+  "updated_at": "2012-06-28T10:52:04Z",
+  "created_at": "2012-06-28T10:52:04Z",
+  "web_url": "http://example.com/snippets/1",
+}
+```
+
+## Update snippet
+
+Updates an existing snippet. The user must have permission to change an existing snippet.
+
+```
+PUT /snippets/:id
+```
+
+Parameters:
+
+| Attribute          | Type    | Required | Description                |
+| ---------          | ----    | -------- | -----------                |
+| `id`               | Integer | yes      | The ID of a snippet        |
+| `title`            | String  | no       | The title of a snippet     |
+| `file_name`        | String  | no       | The name of a snippet file |
+| `content`          | String  | no       | The content of a snippet   |
+| `visibility_level` | Integer | no       | The snippet's visibility   |
+
+
+``` bash
+curl --request PUT --header "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" --data '{"title": "foo", "content": "bar"}' https://gitlab.example.com/api/v3/snippets/1
+```
+
+Example response:
+
+``` json
+{
+  "id": 1,
+  "title": "test",
+  "file_name": "add.rb",
+  "author": {
+    "id": 1,
+    "username": "john_smith",
+    "email": "john@example.com",
+    "name": "John Smith",
+    "state": "active",
+    "created_at": "2012-05-23T08:00:58Z"
+  },
+  "expires_at": null,
+  "updated_at": "2012-06-28T10:52:04Z",
+  "created_at": "2012-06-28T10:52:04Z",
+  "web_url": "http://example.com/snippets/1",
+}
+```
+
+## Delete snippet
+
+Deletes an existing snippet. 
+
+```
+DELETE /snippets/:id
+```
+
+Parameters:
+
+| Attribute          | Type    | Required | Description                   |
+| ---------          | ----    | -------- | -----------                   |
+| `id`               | Integer | yes      | The ID of a snippet           |
+
+
+```
+curl --request DELETE --header "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" "https://gitlab.example.com/api/v3/snippets/1"
+```
+
+upon successful delete a `204 No content` HTTP code shall be expected, with no data,
+but if the snippet is non-existent, a `404 Not Found` will be returned.
+
+## Explore all public snippets
+
+```
+GET /snippets/public
+```
+
+| Attribute  | Type    | Required | Description                           |
+| ---------  | ----    | -------- | -----------                           |
+| `per_page` | Integer | no       | number of snippets to return per page |
+| `page`     | Integer | no       | the page to retrieve                  |
+
+``` bash
+curl --header "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" https://gitlab.example.com/api/v3/snippets/public?per_page=2&page=1
+```
+
+Example response:
+
+``` json
+[
+    {
+        "author": {
+            "avatar_url": "http://www.gravatar.com/avatar/edaf55a9e363ea263e3b981d09e0f7f7?s=80&d=identicon",
+            "id": 12,
+            "name": "Libby Rolfson",
+            "state": "active",
+            "username": "elton_wehner",
+            "web_url": "http://localhost:3000/elton_wehner"
+        },
+        "created_at": "2016-11-25T16:53:34.504Z",
+        "file_name": "oconnerrice.rb",
+        "id": 49,
+        "raw_url": "http://localhost:3000/snippets/49/raw",
+        "title": "Ratione cupiditate et laborum temporibus.",
+        "updated_at": "2016-11-25T16:53:34.504Z",
+        "web_url": "http://localhost:3000/snippets/49"
+    },
+    {
+        "author": {
+            "avatar_url": "http://www.gravatar.com/avatar/36583b28626de71061e6e5a77972c3bd?s=80&d=identicon",
+            "id": 16,
+            "name": "Llewellyn Flatley",
+            "state": "active",
+            "username": "adaline",
+            "web_url": "http://localhost:3000/adaline"
+        },
+        "created_at": "2016-11-25T16:53:34.479Z",
+        "file_name": "muellershields.rb",
+        "id": 48,
+        "raw_url": "http://localhost:3000/snippets/48/raw",
+        "title": "Minus similique nesciunt vel fugiat qui ullam sunt.",
+        "updated_at": "2016-11-25T16:53:34.479Z",
+        "web_url": "http://localhost:3000/snippets/48"
+    }
+]
+```
+
diff --git a/doc/ci/review_apps/index.md b/doc/ci/review_apps/index.md
index a66165dc973b2c6e50bc8d196568e0d01bf1cd23..c679ea4e2982c5e8e5d6afaea8ded3eed6791ffe 100644
--- a/doc/ci/review_apps/index.md
+++ b/doc/ci/review_apps/index.md
@@ -33,7 +33,7 @@ built and deployed under a dynamic environment and can be previewed with an
 also dynamically URL.
 
 The details of the Review Apps implementation depend widely on your real
-technology stack and on your deployment process. The simplest case it to
+technology stack and on your deployment process. The simplest case is to
 deploy a simple static HTML website, but it will not be that straightforward
 when your app is using a database for example. To make a branch be deployed
 on a temporary instance and booting up this instance with all required software
diff --git a/doc/install/installation.md b/doc/install/installation.md
index 9c6a9656557f9b426fdbd2a77e68f30449366c97..2740b2982b9c62ed8b2e5cec8c2685a3366c5c1f 100644
--- a/doc/install/installation.md
+++ b/doc/install/installation.md
@@ -271,9 +271,9 @@ sudo usermod -aG redis git
 ### Clone the Source
 
     # Clone GitLab repository
-    sudo -u git -H git clone https://gitlab.com/gitlab-org/gitlab-ce.git -b 8-14-stable gitlab
+    sudo -u git -H git clone https://gitlab.com/gitlab-org/gitlab-ce.git -b 8-15-stable gitlab
 
-**Note:** You can change `8-14-stable` to `master` if you want the *bleeding edge* version, but never install master on a production server!
+**Note:** You can change `8-15-stable` to `master` if you want the *bleeding edge* version, but never install master on a production server!
 
 ### Configure It
 
diff --git a/doc/update/8.14-to-8.15.md b/doc/update/8.14-to-8.15.md
index 3f58493fa636fa992929cb9e1288ca2e017f8596..5556dae2551e321dd6c891bb5dddcd003c5ee14e 100644
--- a/doc/update/8.14-to-8.15.md
+++ b/doc/update/8.14-to-8.15.md
@@ -72,7 +72,7 @@ sudo -u git -H git checkout 8-15-stable-ee
 ```bash
 cd /home/git/gitlab-shell
 sudo -u git -H git fetch --all --tags
-sudo -u git -H git checkout v4.0.0
+sudo -u git -H git checkout v4.0.3
 ```
 
 ### 6. Update gitlab-workhorse
diff --git a/features/steps/group/members.rb b/features/steps/group/members.rb
index cefc55d07abef198a32cf024f6302b61557eb3f4..adaf375453cfb5ad79b46246b71fd55c8efcbb15 100644
--- a/features/steps/group/members.rb
+++ b/features/steps/group/members.rb
@@ -117,7 +117,12 @@ class Spinach::Features::GroupMembers < Spinach::FeatureSteps
     member = mary_jane_member
 
     page.within "#group_member_#{member.id}" do
-      select 'Developer', from: "member_access_level_#{member.id}"
+      click_button member.human_access
+
+      page.within '.dropdown-menu' do
+        click_link 'Developer'
+      end
+
       wait_for_ajax
     end
   end
diff --git a/features/steps/project/team_management.rb b/features/steps/project/team_management.rb
index b21d0849ad1fc2a8954bafb18d6584850b607a72..22d971fadfb594aca7f3a0f790bc81ea53e884fb 100644
--- a/features/steps/project/team_management.rb
+++ b/features/steps/project/team_management.rb
@@ -65,7 +65,11 @@ class Spinach::Features::ProjectTeamManagement < Spinach::FeatureSteps
     user = User.find_by(name: 'Dmitriy')
     project_member = project.project_members.find_by(user_id: user.id)
     page.within "#project_member_#{project_member.id}" do
-      select "Reporter", from: "member_access_level_#{project_member.id}"
+      click_button project_member.human_access
+
+      page.within '.dropdown-menu' do
+        click_link 'Reporter'
+      end
     end
   end
 
@@ -144,7 +148,7 @@ class Spinach::Features::ProjectTeamManagement < Spinach::FeatureSteps
   step 'I should see "Opensource" group user listing' do
     page.within '.project-members-groups' do
       expect(page).to have_content('OpenSource')
-      expect(find('select').value).to eq('40')
+      expect(first('.group_member')).to have_content('Master')
     end
   end
 end
diff --git a/lib/api/api.rb b/lib/api/api.rb
index 67109ceeef98f93638281c66f10344b37edd4f7c..cec2702e44d91fe71f39dee9c3177914c75895c9 100644
--- a/lib/api/api.rb
+++ b/lib/api/api.rb
@@ -64,6 +64,7 @@ module API
     mount ::API::Session
     mount ::API::Settings
     mount ::API::SidekiqMetrics
+    mount ::API::Snippets
     mount ::API::Subscriptions
     mount ::API::SystemHooks
     mount ::API::Tags
diff --git a/lib/api/branches.rb b/lib/api/branches.rb
index 73aed624ea72af930c99d331981d28d5ce76e4ac..0950c3d2e88480c19d92b794c3138b267f4bdd88 100644
--- a/lib/api/branches.rb
+++ b/lib/api/branches.rb
@@ -23,9 +23,9 @@ module API
         success Entities::RepoBranch
       end
       params do
-        requires :branch, type: String, regexp: /.+/, desc: 'The name of the branch'
+        requires :branch, type: String, desc: 'The name of the branch'
       end
-      get ':id/repository/branches/:branch' do
+      get ':id/repository/branches/:branch', requirements: { branch: /.+/ } do
         branch = user_project.repository.find_branch(params[:branch])
         not_found!("Branch") unless branch
 
@@ -39,11 +39,11 @@ module API
         success Entities::RepoBranch
       end
       params do
-        requires :branch, type: String, regexp: /.+/, desc: 'The name of the branch'
+        requires :branch, type: String, desc: 'The name of the branch'
         optional :developers_can_push, type: Boolean, desc: 'Flag if developers can push to that branch'
         optional :developers_can_merge, type: Boolean, desc: 'Flag if developers can merge to that branch'
       end
-      put ':id/repository/branches/:branch/protect' do
+      put ':id/repository/branches/:branch/protect', requirements: { branch: /.+/ } do
         authorize_admin_project
 
         branch = user_project.repository.find_branch(params[:branch])
@@ -76,9 +76,9 @@ module API
         success Entities::RepoBranch
       end
       params do
-        requires :branch, type: String, regexp: /.+/, desc: 'The name of the branch'
+        requires :branch, type: String, desc: 'The name of the branch'
       end
-      put ':id/repository/branches/:branch/unprotect' do
+      put ':id/repository/branches/:branch/unprotect', requirements: { branch: /.+/ } do
         authorize_admin_project
 
         branch = user_project.repository.find_branch(params[:branch])
@@ -112,9 +112,9 @@ module API
 
       desc 'Delete a branch'
       params do
-        requires :branch, type: String, regexp: /.+/, desc: 'The name of the branch'
+        requires :branch, type: String, desc: 'The name of the branch'
       end
-      delete ":id/repository/branches/:branch" do
+      delete ":id/repository/branches/:branch", requirements: { branch: /.+/ } do
         authorize_push_project
 
         result = DeleteBranchService.new(user_project, current_user).
diff --git a/lib/api/entities.rb b/lib/api/entities.rb
index 006d5f9f44e2c46dd4c346dbbdbed02808953599..01c0f5072ba40dc1b29fc4b5996989e5a9ecdc03 100644
--- a/lib/api/entities.rb
+++ b/lib/api/entities.rb
@@ -201,6 +201,19 @@ module API
       end
     end
 
+    class PersonalSnippet < Grape::Entity
+      expose :id, :title, :file_name
+      expose :author, using: Entities::UserBasic
+      expose :updated_at, :created_at
+
+      expose :web_url do |snippet|
+        Gitlab::UrlBuilder.build(snippet)
+      end
+      expose :raw_url do |snippet|
+        Gitlab::UrlBuilder.build(snippet) + "/raw"
+      end
+    end
+
     class ProjectEntity < Grape::Entity
       expose :id, :iid
       expose(:project_id) { |entity| entity.project.id }
diff --git a/lib/api/groups.rb b/lib/api/groups.rb
index fbf7513302b28eaab5863de145de6dfa5e220c00..105d3ee342e753c7ea35feb7e1b44c1ee2347ef6 100644
--- a/lib/api/groups.rb
+++ b/lib/api/groups.rb
@@ -1,7 +1,7 @@
 module API
   class Groups < Grape::API
     include PaginationParams
-    
+
     before { authenticate! }
 
     helpers do
@@ -117,11 +117,20 @@ module API
         success Entities::Project
       end
       params do
+        optional :archived, type: Boolean, default: false, desc: 'Limit by archived status'
+        optional :visibility, type: String, values: %w[public internal private],
+                              desc: 'Limit by visibility'
+        optional :search, type: String, desc: 'Return list of authorized projects matching the search criteria'
+        optional :order_by, type: String, values: %w[id name path created_at updated_at last_activity_at],
+                            default: 'created_at', desc: 'Return projects ordered by field'
+        optional :sort, type: String, values: %w[asc desc], default: 'desc',
+                        desc: 'Return projects sorted in ascending and descending order'
         use :pagination
       end
       get ":id/projects" do
         group = find_group!(params[:id])
         projects = GroupProjectsFinder.new(group).execute(current_user)
+        projects = filter_projects(projects)
         present paginate(projects), with: Entities::Project, user: current_user
       end
 
diff --git a/lib/api/snippets.rb b/lib/api/snippets.rb
new file mode 100644
index 0000000000000000000000000000000000000000..e096e6368061f5c8677f6e1543c1e327f7b553a7
--- /dev/null
+++ b/lib/api/snippets.rb
@@ -0,0 +1,137 @@
+module API
+  # Snippets API
+  class Snippets < Grape::API
+    include PaginationParams
+
+    before { authenticate! }
+
+    resource :snippets do
+      helpers do
+        def snippets_for_current_user
+          SnippetsFinder.new.execute(current_user, filter: :by_user, user: current_user)
+        end
+
+        def public_snippets
+          SnippetsFinder.new.execute(current_user, filter: :public)
+        end
+      end
+
+      desc 'Get a snippets list for authenticated user' do
+        detail 'This feature was introduced in GitLab 8.15.'
+        success Entities::PersonalSnippet
+      end
+      params do
+        use :pagination
+      end
+      get do
+        present paginate(snippets_for_current_user), with: Entities::PersonalSnippet
+      end
+
+      desc 'List all public snippets current_user has access to' do
+        detail 'This feature was introduced in GitLab 8.15.'
+        success Entities::PersonalSnippet
+      end
+      params do
+        use :pagination
+      end
+      get 'public' do
+        present paginate(public_snippets), with: Entities::PersonalSnippet
+      end
+
+      desc 'Get a single snippet' do
+        detail 'This feature was introduced in GitLab 8.15.'
+        success Entities::PersonalSnippet
+      end
+      params do
+        requires :id, type: Integer, desc: 'The ID of a snippet'
+      end
+      get ':id' do
+        snippet = snippets_for_current_user.find(params[:id])
+        present snippet, with: Entities::PersonalSnippet
+      end
+
+      desc 'Create new snippet' do
+        detail 'This feature was introduced in GitLab 8.15.'
+        success Entities::PersonalSnippet
+      end
+      params do
+        requires :title, type: String, desc: 'The title of a snippet'
+        requires :file_name, type: String, desc: 'The name of a snippet file'
+        requires :content, type: String, desc: 'The content of a snippet'
+        optional :visibility_level, type: Integer,
+                                    values: Gitlab::VisibilityLevel.values,
+                                    default: Gitlab::VisibilityLevel::INTERNAL,
+                                    desc: 'The visibility level of the snippet'
+      end
+      post do
+        attrs = declared_params(include_missing: false)
+        snippet = CreateSnippetService.new(nil, current_user, attrs).execute
+
+        if snippet.persisted?
+          present snippet, with: Entities::PersonalSnippet
+        else
+          render_validation_error!(snippet)
+        end
+      end
+
+      desc 'Update an existing snippet' do
+        detail 'This feature was introduced in GitLab 8.15.'
+        success Entities::PersonalSnippet
+      end
+      params do
+        requires :id, type: Integer, desc: 'The ID of a snippet'
+        optional :title, type: String, desc: 'The title of a snippet'
+        optional :file_name, type: String, desc: 'The name of a snippet file'
+        optional :content, type: String, desc: 'The content of a snippet'
+        optional :visibility_level, type: Integer,
+                                    values: Gitlab::VisibilityLevel.values,
+                                    desc: 'The visibility level of the snippet'
+        at_least_one_of :title, :file_name, :content, :visibility_level
+      end
+      put ':id' do
+        snippet = snippets_for_current_user.find_by(id: params.delete(:id))
+        return not_found!('Snippet') unless snippet
+        authorize! :update_personal_snippet, snippet
+
+        attrs = declared_params(include_missing: false)
+
+        UpdateSnippetService.new(nil, current_user, snippet, attrs).execute
+        if snippet.persisted?
+          present snippet, with: Entities::PersonalSnippet
+        else
+          render_validation_error!(snippet)
+        end
+      end
+
+      desc 'Remove snippet' do
+        detail 'This feature was introduced in GitLab 8.15.'
+        success Entities::PersonalSnippet
+      end
+      params do
+        requires :id, type: Integer, desc: 'The ID of a snippet'
+      end
+      delete ':id' do
+        snippet = snippets_for_current_user.find_by(id: params.delete(:id))
+        return not_found!('Snippet') unless snippet
+        authorize! :destroy_personal_snippet, snippet
+        snippet.destroy
+        no_content!
+      end
+
+      desc 'Get a raw snippet' do
+        detail 'This feature was introduced in GitLab 8.15.'
+      end
+      params do
+        requires :id, type: Integer, desc: 'The ID of a snippet'
+      end
+      get ":id/raw" do
+        snippet = snippets_for_current_user.find_by(id: params.delete(:id))
+        return not_found!('Snippet') unless snippet
+
+        env['api.format'] = :txt
+        content_type 'text/plain'
+        present snippet.content
+      end
+    end
+  end
+end
diff --git a/lib/gitlab/url_builder.rb b/lib/gitlab/url_builder.rb
index 99d0c28e7493911e9c8789b43599a75938b34ca0..ccb456bcc94aa40bb7b2d7b8109174cb6448d9c9 100644
--- a/lib/gitlab/url_builder.rb
+++ b/lib/gitlab/url_builder.rb
@@ -24,6 +24,8 @@ module Gitlab
         wiki_page_url
       when ProjectSnippet
         project_snippet_url(object)
+      when Snippet
+        personal_snippet_url(object)
       else
         raise NotImplementedError.new("No URL builder defined for #{object.class}")
       end
diff --git a/spec/features/groups/members/last_owner_cannot_leave_group_spec.rb b/spec/features/groups/members/last_owner_cannot_leave_group_spec.rb
index 33bf6d3752f465c3ea020a0ac99820c7334a5734..be60b0489c7a9643fdec5336ee6a7a504e1f97da 100644
--- a/spec/features/groups/members/last_owner_cannot_leave_group_spec.rb
+++ b/spec/features/groups/members/last_owner_cannot_leave_group_spec.rb
@@ -10,7 +10,7 @@ feature 'Groups > Members > Last owner cannot leave group', feature: true do
     visit group_path(group)
   end
 
-  scenario 'user does not see a "Leave Group" link' do
-    expect(page).not_to have_content 'Leave Group'
+  scenario 'user does not see a "Leave group" link' do
+    expect(page).not_to have_content 'Leave group'
   end
 end
diff --git a/spec/features/groups/members/member_leaves_group_spec.rb b/spec/features/groups/members/member_leaves_group_spec.rb
index 3185ff924b943af13893c7173881e49400a8f5f5..ac4d94658ae85d480e1b6d1f289c360ecd6093ed 100644
--- a/spec/features/groups/members/member_leaves_group_spec.rb
+++ b/spec/features/groups/members/member_leaves_group_spec.rb
@@ -13,7 +13,7 @@ feature 'Groups > Members > Member leaves group', feature: true do
   end
 
   scenario 'user leaves group' do
-    click_link 'Leave Group'
+    click_link 'Leave group'
 
     expect(current_path).to eq(dashboard_groups_path)
     expect(group.users.exists?(user.id)).to be_falsey
diff --git a/spec/features/groups/members/user_requests_access_spec.rb b/spec/features/groups/members/user_requests_access_spec.rb
index d8c9c48799634dc578b00a5f3f0185a96bb509b9..e4b5ea91bd3ab27faa15c159b65c16ee82e1aacb 100644
--- a/spec/features/groups/members/user_requests_access_spec.rb
+++ b/spec/features/groups/members/user_requests_access_spec.rb
@@ -29,7 +29,7 @@ feature 'Groups > Members > User requests access', feature: true do
     expect(page).to have_content 'Your request for access has been queued for review.'
 
     expect(page).to have_content 'Withdraw Access Request'
-    expect(page).not_to have_content 'Leave Group'
+    expect(page).not_to have_content 'Leave group'
   end
 
   scenario 'user does not see private projects' do
diff --git a/spec/features/help_pages_spec.rb b/spec/features/help_pages_spec.rb
index 4319d6db0d25301fdfe0e6e7fe2711c2abac60ae..40a1fced8d837cacc8bd42e91940e3cb732e52c6 100644
--- a/spec/features/help_pages_spec.rb
+++ b/spec/features/help_pages_spec.rb
@@ -1,16 +1,6 @@
 require 'spec_helper'
 
 describe 'Help Pages', feature: true do
-  describe 'Show SSH page' do
-    before do
-      login_as :user
-    end
-    it 'replaces the variable $your_email with the email of the user' do
-      visit help_page_path('ssh/README')
-      expect(page).to have_content("ssh-keygen -t rsa -C \"#{@user.email}\"")
-    end
-  end
-
   describe 'Get the main help page' do
     shared_examples_for 'help page' do |prefix: ''|
       it 'prefixes links correctly' do
diff --git a/spec/features/projects/members/group_links_spec.rb b/spec/features/projects/members/group_links_spec.rb
index cc2f695211ce3e44e220f3351dd0c8fb2f08aeb8..94995f7cf95ff5f7b406c3e0879514291553c7b0 100644
--- a/spec/features/projects/members/group_links_spec.rb
+++ b/spec/features/projects/members/group_links_spec.rb
@@ -16,12 +16,17 @@ feature 'Projects > Members > Anonymous user sees members', feature: true, js: t
   end
 
   it 'updates group access level' do
-    select 'Guest', from: "member_access_level_#{group.id}"
+    click_button @group_link.human_access
+
+    page.within '.dropdown-menu' do
+      click_link 'Guest'
+    end
+
     wait_for_ajax
 
     visit namespace_project_project_members_path(project.namespace, project)
 
-    expect(page).to have_select("member_access_level_#{group.id}", selected: 'Guest')
+    expect(first('.group_member')).to have_content('Guest')
   end
 
   it 'updates expiry date' do
diff --git a/spec/features/projects/members/group_member_cannot_leave_group_project_spec.rb b/spec/features/projects/members/group_member_cannot_leave_group_project_spec.rb
index 728c0e16361bc17f3f6896d7a7e1162fbf2bf6c3..b483ba4c54c6909ba58e5868a99367ceea8e0863 100644
--- a/spec/features/projects/members/group_member_cannot_leave_group_project_spec.rb
+++ b/spec/features/projects/members/group_member_cannot_leave_group_project_spec.rb
@@ -12,6 +12,6 @@ feature 'Projects > Members > Group member cannot leave group project', feature:
   end
 
   scenario 'user does not see a "Leave project" link' do
-    expect(page).not_to have_content 'Leave Project'
+    expect(page).not_to have_content 'Leave project'
   end
 end
diff --git a/spec/features/projects/members/group_requester_cannot_request_access_to_project_spec.rb b/spec/features/projects/members/group_requester_cannot_request_access_to_project_spec.rb
index 4973e0aee85a44c22bff905f7a051eb593858481..bdeeef572730d272eef97a549b77dda4fd159461 100644
--- a/spec/features/projects/members/group_requester_cannot_request_access_to_project_spec.rb
+++ b/spec/features/projects/members/group_requester_cannot_request_access_to_project_spec.rb
@@ -1,6 +1,6 @@
 require 'spec_helper'
 
-feature 'Projects > Members > Group requester cannot request access to project', feature: true do
+feature 'Projects > Members > Group requester cannot request access to project', feature: true, js: true do
   let(:user) { create(:user) }
   let(:owner) { create(:user) }
   let(:group) { create(:group, :public, :access_requestable) }
diff --git a/spec/features/projects/members/member_leaves_project_spec.rb b/spec/features/projects/members/member_leaves_project_spec.rb
index 79dec442818be6f4be60d91f36ee52aa7f03de27..5daa932e4e6e52481f2f01f45eca77d74c823ba3 100644
--- a/spec/features/projects/members/member_leaves_project_spec.rb
+++ b/spec/features/projects/members/member_leaves_project_spec.rb
@@ -11,7 +11,7 @@ feature 'Projects > Members > Member leaves project', feature: true do
   end
 
   scenario 'user leaves project' do
-    click_link 'Leave Project'
+    click_link 'Leave project'
 
     expect(current_path).to eq(dashboard_projects_path)
     expect(project.users.exists?(user.id)).to be_falsey
diff --git a/spec/features/projects/members/owner_cannot_leave_project_spec.rb b/spec/features/projects/members/owner_cannot_leave_project_spec.rb
index 6e948b7a61636643a2a6388c5de1f5d5aba62515..b26d55c5d5d186aca1f7a0c2c2ae4a0fea6af00f 100644
--- a/spec/features/projects/members/owner_cannot_leave_project_spec.rb
+++ b/spec/features/projects/members/owner_cannot_leave_project_spec.rb
@@ -8,7 +8,7 @@ feature 'Projects > Members > Owner cannot leave project', feature: true do
     visit namespace_project_path(project.namespace, project)
   end
 
-  scenario 'user does not see a "Leave Project" link' do
-    expect(page).not_to have_content 'Leave Project'
+  scenario 'user does not see a "Leave project" link' do
+    expect(page).not_to have_content 'Leave project'
   end
 end
diff --git a/spec/finders/snippets_finder_spec.rb b/spec/finders/snippets_finder_spec.rb
index 28bdc18e8405e23c7ce61f64e5b587f8dd889154..4427443208a12a6a84e3f4066b3bd3089f54a8ee 100644
--- a/spec/finders/snippets_finder_spec.rb
+++ b/spec/finders/snippets_finder_spec.rb
@@ -9,65 +9,74 @@ describe SnippetsFinder do
   let(:project2) { create(:empty_project, :private, group: group) }
 
   context ':all filter' do
-    before do
-      @snippet1 = create(:personal_snippet, :private)
-      @snippet2 = create(:personal_snippet, :internal)
-      @snippet3 = create(:personal_snippet, :public)
-    end
+    let!(:snippet1) { create(:personal_snippet, :private) }
+    let!(:snippet2) { create(:personal_snippet, :internal) }
+    let!(:snippet3) { create(:personal_snippet, :public) }
 
     it "returns all private and internal snippets" do
       snippets = SnippetsFinder.new.execute(user, filter: :all)
-      expect(snippets).to include(@snippet2, @snippet3)
-      expect(snippets).not_to include(@snippet1)
+      expect(snippets).to include(snippet2, snippet3)
+      expect(snippets).not_to include(snippet1)
     end
 
     it "returns all public snippets" do
       snippets = SnippetsFinder.new.execute(nil, filter: :all)
-      expect(snippets).to include(@snippet3)
-      expect(snippets).not_to include(@snippet1, @snippet2)
+      expect(snippets).to include(snippet3)
+      expect(snippets).not_to include(snippet1, snippet2)
     end
   end
 
-  context ':by_user filter' do
-    before do
-      @snippet1 = create(:personal_snippet, :private,  author: user)
-      @snippet2 = create(:personal_snippet, :internal, author: user)
-      @snippet3 = create(:personal_snippet, :public,   author: user)
+  context ':public filter' do
+    let!(:snippet1) { create(:personal_snippet, :private) }
+    let!(:snippet2) { create(:personal_snippet, :internal) }
+    let!(:snippet3) { create(:personal_snippet, :public) }
+
+    it "returns public public snippets" do
+      snippets = SnippetsFinder.new.execute(nil, filter: :public)
+
+      expect(snippets).to include(snippet3)
+      expect(snippets).not_to include(snippet1, snippet2)
     end
+  end
+
+  context ':by_user filter' do
+    let!(:snippet1) { create(:personal_snippet, :private, author: user) }
+    let!(:snippet2) { create(:personal_snippet, :internal, author: user) }
+    let!(:snippet3) { create(:personal_snippet, :public, author: user) }
 
     it "returns all public and internal snippets" do
       snippets = SnippetsFinder.new.execute(user1, filter: :by_user, user: user)
-      expect(snippets).to include(@snippet2, @snippet3)
-      expect(snippets).not_to include(@snippet1)
+      expect(snippets).to include(snippet2, snippet3)
+      expect(snippets).not_to include(snippet1)
     end
 
     it "returns internal snippets" do
       snippets = SnippetsFinder.new.execute(user, filter: :by_user, user: user, scope: "are_internal")
-      expect(snippets).to include(@snippet2)
-      expect(snippets).not_to include(@snippet1, @snippet3)
+      expect(snippets).to include(snippet2)
+      expect(snippets).not_to include(snippet1, snippet3)
     end
 
     it "returns private snippets" do
       snippets = SnippetsFinder.new.execute(user, filter: :by_user, user: user, scope: "are_private")
-      expect(snippets).to include(@snippet1)
-      expect(snippets).not_to include(@snippet2, @snippet3)
+      expect(snippets).to include(snippet1)
+      expect(snippets).not_to include(snippet2, snippet3)
     end
 
     it "returns public snippets" do
       snippets = SnippetsFinder.new.execute(user, filter: :by_user, user: user, scope: "are_public")
-      expect(snippets).to include(@snippet3)
-      expect(snippets).not_to include(@snippet1, @snippet2)
+      expect(snippets).to include(snippet3)
+      expect(snippets).not_to include(snippet1, snippet2)
     end
 
     it "returns all snippets" do
       snippets = SnippetsFinder.new.execute(user, filter: :by_user, user: user)
-      expect(snippets).to include(@snippet1, @snippet2, @snippet3)
+      expect(snippets).to include(snippet1, snippet2, snippet3)
     end
 
     it "returns only public snippets if unauthenticated user" do
       snippets = SnippetsFinder.new.execute(nil, filter: :by_user, user: user)
-      expect(snippets).to include(@snippet3)
-      expect(snippets).not_to include(@snippet2, @snippet1)
+      expect(snippets).to include(snippet3)
+      expect(snippets).not_to include(snippet2, snippet1)
     end
   end
 
diff --git a/spec/javascripts/merge_request_widget_spec.js b/spec/javascripts/merge_request_widget_spec.js
index 62890f1ca96b8ab049f8b0de25661447d35b3710..6f91529db00c4040cf2e8ea769122bcffff199d6 100644
--- a/spec/javascripts/merge_request_widget_spec.js
+++ b/spec/javascripts/merge_request_widget_spec.js
@@ -106,6 +106,18 @@
       });
     });
 
+    describe('mergeInProgress', function() {
+       it('should display error with h4 tag', function() {
+          spyOn(this.class.$widgetBody, 'html').and.callFake(function(html) {
+            expect(html).toBe('<h4>Sorry, something went wrong.</h4>');
+          });
+          spyOn($, 'ajax').and.callFake(function(e) {
+            e.success({ merge_error: 'Sorry, something went wrong.' });
+          });
+          this.class.mergeInProgress(null);
+        });
+      });
+
     return describe('getCIStatus', function() {
       beforeEach(function() {
         this.ciStatusData = {
diff --git a/spec/requests/api/branches_spec.rb b/spec/requests/api/branches_spec.rb
index 55b8c8c0c69b6c01c5763378b65a188e53a45de8..2878e0cb59b1a4e28c874843e14ba0c41104ed00 100644
--- a/spec/requests/api/branches_spec.rb
+++ b/spec/requests/api/branches_spec.rb
@@ -11,6 +11,7 @@ describe API::Branches, api: true  do
   let!(:guest) { create(:project_member, :guest, user: user2, project: project) }
   let!(:branch_name) { 'feature' }
   let!(:branch_sha) { '0b4bc9a49b562e85de7cc9e834518ea6828729b9' }
+  let!(:branch_with_dot) { CreateBranchService.new(project, user).execute("with.1.2.3", "master") }
 
   describe "GET /projects/:id/repository/branches" do
     it "returns an array of project branches" do
@@ -37,6 +38,13 @@ describe API::Branches, api: true  do
       expect(json_response['developers_can_merge']).to eq(false)
     end
 
+    it "returns the branch information for a single branch with dots in the name" do
+      get api("/projects/#{project.id}/repository/branches/with.1.2.3", user)
+
+      expect(response).to have_http_status(200)
+      expect(json_response['name']).to eq("with.1.2.3")
+    end
+
     context 'on a merged branch' do
       it "returns the branch information for a single branch" do
         get api("/projects/#{project.id}/repository/branches/merge-test", user)
@@ -71,6 +79,14 @@ describe API::Branches, api: true  do
         expect(json_response['developers_can_merge']).to eq(false)
       end
 
+      it "protects a single branch with dots in the name" do
+        put api("/projects/#{project.id}/repository/branches/with.1.2.3/protect", user)
+
+        expect(response).to have_http_status(200)
+        expect(json_response['name']).to eq("with.1.2.3")
+        expect(json_response['protected']).to eq(true)
+      end
+
       it 'protects a single branch and developers can push' do
         put api("/projects/#{project.id}/repository/branches/#{branch_name}/protect", user),
             developers_can_push: true
@@ -220,6 +236,14 @@ describe API::Branches, api: true  do
       expect(json_response['protected']).to eq(false)
     end
 
+    it "update branches with dots in branch name" do
+      put api("/projects/#{project.id}/repository/branches/with.1.2.3/unprotect", user)
+
+      expect(response).to have_http_status(200)
+      expect(json_response['name']).to eq("with.1.2.3")
+      expect(json_response['protected']).to eq(false)
+    end
+
     it "returns success when unprotect branch" do
       put api("/projects/#{project.id}/repository/branches/unknown/unprotect", user)
       expect(response).to have_http_status(404)
@@ -292,6 +316,13 @@ describe API::Branches, api: true  do
       expect(json_response['branch_name']).to eq(branch_name)
     end
 
+    it "removes a branch with dots in the branch name" do
+      delete api("/projects/#{project.id}/repository/branches/with.1.2.3", user)
+
+      expect(response).to have_http_status(200)
+      expect(json_response['branch_name']).to eq("with.1.2.3")
+    end
+
     it 'returns 404 if branch not exists' do
       delete api("/projects/#{project.id}/repository/branches/foobar", user)
       expect(response).to have_http_status(404)
diff --git a/spec/requests/api/groups_spec.rb b/spec/requests/api/groups_spec.rb
index 548ed8e1892382cef606224edf45a29a7a57b650..15647b262b6dd9c8ac62da06e4230e60ff723265 100644
--- a/spec/requests/api/groups_spec.rb
+++ b/spec/requests/api/groups_spec.rb
@@ -245,6 +245,17 @@ describe API::Groups, api: true  do
         expect(project_names).to match_array([project1.name, project3.name])
       end
 
+      it 'filters the groups projects' do
+        public_projet = create(:project, :public, path: 'test1', group: group1)
+
+        get api("/groups/#{group1.id}/projects", user1), visibility: 'public'
+
+        expect(response).to have_http_status(200)
+        expect(json_response).to be_an(Array)
+        expect(json_response.length).to eq(1)
+        expect(json_response.first['name']).to eq(public_projet.name)
+      end
+
       it "does not return a non existing group" do
         get api("/groups/1328/projects", user1)
         expect(response).to have_http_status(404)
diff --git a/spec/requests/api/snippets_spec.rb b/spec/requests/api/snippets_spec.rb
new file mode 100644
index 0000000000000000000000000000000000000000..f6fb6ea5506cbd44fba039d4d02b11870dd0c474
--- /dev/null
+++ b/spec/requests/api/snippets_spec.rb
@@ -0,0 +1,157 @@
+require 'rails_helper'
+
+describe API::Snippets, api: true do
+  include ApiHelpers
+  let!(:user) { create(:user) }
+
+  describe 'GET /snippets/' do
+    it 'returns snippets available' do
+      public_snippet = create(:personal_snippet, :public, author: user)
+      private_snippet = create(:personal_snippet, :private, author: user)
+      internal_snippet = create(:personal_snippet, :internal, author: user)
+
+      get api("/snippets/", user)
+
+      expect(response).to have_http_status(200)
+      expect(json_response.map { |snippet| snippet['id']} ).to contain_exactly(
+        public_snippet.id,
+        internal_snippet.id,
+        private_snippet.id)
+      expect(json_response.last).to have_key('web_url')
+      expect(json_response.last).to have_key('raw_url')
+    end
+
+    it 'hides private snippets from regular user' do
+      create(:personal_snippet, :private)
+
+      get api("/snippets/", user)
+      expect(response).to have_http_status(200)
+      expect(json_response.size).to eq(0)
+    end
+  end
+
+  describe 'GET /snippets/public' do
+    let!(:other_user) { create(:user) }
+    let!(:public_snippet) { create(:personal_snippet, :public, author: user) }
+    let!(:private_snippet) { create(:personal_snippet, :private, author: user) }
+    let!(:internal_snippet) { create(:personal_snippet, :internal, author: user) }
+    let!(:public_snippet_other) { create(:personal_snippet, :public, author: other_user) }
+    let!(:private_snippet_other) { create(:personal_snippet, :private, author: other_user) }
+    let!(:internal_snippet_other) { create(:personal_snippet, :internal, author: other_user) }
+
+    it 'returns all snippets with public visibility from all users' do
+      get api("/snippets/public", user)
+
+      expect(response).to have_http_status(200)
+      expect(json_response.map { |snippet| snippet['id']} ).to contain_exactly(
+        public_snippet.id,
+        public_snippet_other.id)
+      expect(json_response.map{ |snippet| snippet['web_url']} ).to include(
+        "http://localhost/snippets/#{public_snippet.id}",
+        "http://localhost/snippets/#{public_snippet_other.id}")
+      expect(json_response.map{ |snippet| snippet['raw_url']} ).to include(
+        "http://localhost/snippets/#{public_snippet.id}/raw",
+        "http://localhost/snippets/#{public_snippet_other.id}/raw")
+    end
+  end
+
+  describe 'GET /snippets/:id/raw' do
+    let(:snippet) { create(:personal_snippet, author: user) }
+
+    it 'returns raw text' do
+      get api("/snippets/#{snippet.id}/raw", user)
+
+      expect(response).to have_http_status(200)
+      expect(response.content_type).to eq 'text/plain'
+      expect(response.body).to eq(snippet.content)
+    end
+
+    it 'returns 404 for invalid snippet id' do
+      delete api("/snippets/1234", user)
+
+      expect(response).to have_http_status(404)
+      expect(json_response['message']).to eq('404 Snippet Not Found')
+    end
+  end
+
+  describe 'POST /snippets/' do
+    let(:params) do
+      {
+        title: 'Test Title',
+        file_name: 'test.rb',
+        content: 'puts "hello world"',
+        visibility_level: Gitlab::VisibilityLevel::PUBLIC
+      }
+    end
+
+    it 'creates a new snippet' do
+      expect do
+        post api("/snippets/", user), params
+      end.to change { PersonalSnippet.count }.by(1)
+
+      expect(response).to have_http_status(201)
+      expect(json_response['title']).to eq(params[:title])
+      expect(json_response['file_name']).to eq(params[:file_name])
+    end
+
+    it 'returns 400 for missing parameters' do
+      params.delete(:title)
+
+      post api("/snippets/", user), params
+
+      expect(response).to have_http_status(400)
+    end
+  end
+
+  describe 'PUT /snippets/:id' do
+    let(:other_user) { create(:user) }
+    let(:public_snippet) { create(:personal_snippet, :public, author: user) }
+    it 'updates snippet' do
+      new_content = 'New content'
+
+      put api("/snippets/#{public_snippet.id}", user), content: new_content
+
+      expect(response).to have_http_status(200)
+      public_snippet.reload
+      expect(public_snippet.content).to eq(new_content)
+    end
+
+    it 'returns 404 for invalid snippet id' do
+      put api("/snippets/1234", user), title: 'foo'
+
+      expect(response).to have_http_status(404)
+      expect(json_response['message']).to eq('404 Snippet Not Found')
+    end
+
+    it "returns 404 for another user's snippet" do
+      put api("/snippets/#{public_snippet.id}", other_user), title: 'fubar'
+
+      expect(response).to have_http_status(404)
+      expect(json_response['message']).to eq('404 Snippet Not Found')
+    end
+
+    it 'returns 400 for missing parameters' do
+      put api("/snippets/1234", user)
+
+      expect(response).to have_http_status(400)
+    end
+  end
+
+  describe 'DELETE /snippets/:id' do
+    let!(:public_snippet) { create(:personal_snippet, :public, author: user) }
+    it 'deletes snippet' do
+      expect do
+        delete api("/snippets/#{public_snippet.id}", user)
+
+        expect(response).to have_http_status(204)
+      end.to change { PersonalSnippet.count }.by(-1)
+    end
+
+    it 'returns 404 for invalid snippet id' do
+      delete api("/snippets/1234", user)
+
+      expect(response).to have_http_status(404)
+      expect(json_response['message']).to eq('404 Snippet Not Found')
+    end
+  end
+end
diff --git a/spec/support/login_helpers.rb b/spec/support/login_helpers.rb
index c0b3e83244ddd6fdca29dd7e77d8bba6629828d5..ad1eed5b369de544dde7af72a6b3c14924a4a8a2 100644
--- a/spec/support/login_helpers.rb
+++ b/spec/support/login_helpers.rb
@@ -75,7 +75,8 @@ module LoginHelpers
   def logout
     find(".header-user-dropdown-toggle").click
     click_link "Sign out"
-    expect(page).to have_content('Signed out successfully')
+    # check the sign_in button
+    expect(page).to have_button('Sign in')
   end
 
   # Logout without JavaScript driver