Commit e5acebd9 authored by Lin Jen-Shin's avatar Lin Jen-Shin

Merge remote-tracking branch 'upstream/master' into fix-git-hooks-when-creating-file

* upstream/master:
  API: Fix groups filter
  fix: removed signed_out notification
  Fix typo in curl example request
  Fix typo
  Change docs title to represent the edition
  remove left_align setting from notification setting dropdown in favor of a pure css solution
  fix alignment for notification settings ajax response
  [ci skip] Update "Installation from source" guide for 8.15.0
  Group links spec update
  Stop replacing `$your_email` with the user's email
  Updates the font weight of button styles because of the change to system fonts
  Updated JS based on review Fixed group links dropdown to match
  Allow branch names with dots on API endpoint
  Use a single query in Projects::ProjectMembersController to fetch members
  Use gitlab-workhose 1.1.1
  Updated members dropdowns
  Use default btn styling for Housekeeping button on projects settings page
  fix display hook error message
  API: Endpoint to expose personal snippets as /snippets
  Removed leave buttons from settings dropdowns
parents c0dfa0c6 7e9a8bb7
...@@ -650,6 +650,11 @@ ...@@ -650,6 +650,11 @@
} else if(value) { } else if(value) {
field = this.dropdown.parent().find("input[name='" + fieldName + "'][value='" + value.toString().replace(/'/g, '\\\'') + "']"); 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)) { if (el.hasClass(ACTIVE_CLASS)) {
el.removeClass(ACTIVE_CLASS); el.removeClass(ACTIVE_CLASS);
if (field && field.length) { if (field && field.length) {
......
/* eslint-disable */ /* eslint-disable class-methods-use-this */
((w) => { (() => {
w.gl = w.gl || {}; window.gl = window.gl || {};
class Members { class Members {
constructor() { constructor() {
this.addListeners(); this.addListeners();
this.initGLDropdown();
} }
addListeners() { addListeners() {
$('.project_member, .group_member').off('ajax:success').on('ajax:success', this.removeRow); $('.project_member, .group_member').off('ajax:success').on('ajax:success', this.removeRow);
$('.js-member-update-control').off('change').on('change', this.formSubmit); $('.js-member-update-control').off('change').on('change', this.formSubmit.bind(this));
$('.js-edit-member-form').off('ajax:success').on('ajax:success', this.formSuccess); $('.js-edit-member-form').off('ajax:success').on('ajax:success', this.formSuccess.bind(this));
gl.utils.disableButtonIfEmptyField('#user_ids', 'input[name=commit]', 'change'); 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) { removeRow(e) {
const $target = $(e.target); const $target = $(e.target);
if ($target.hasClass('btn-remove')) { if ($target.hasClass('btn-remove')) {
$target.closest('.member') $target.closest('.member')
.fadeOut(function () { .fadeOut(function fadeOutMemberRow() {
$(this).remove(); $(this).remove();
}); });
} }
} }
formSubmit() { formSubmit(e, $el = null) {
$(this).closest('form').trigger("submit.rails").end().disable(); const $this = e ? $(e.currentTarget) : $el;
const { $toggle, $dateInput } = this.getMemberListItems($this);
$this.closest('form').trigger('submit.rails');
$toggle.disable();
$dateInput.disable();
} }
formSuccess() { formSuccess(e) {
$(this).find('.js-member-update-control').enable(); 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; gl.Members = Members;
})(window); })();
...@@ -101,7 +101,7 @@ ...@@ -101,7 +101,7 @@
urlSuffix = deleteSourceBranch ? '?deleted_source_branch=true' : ''; urlSuffix = deleteSourceBranch ? '?deleted_source_branch=true' : '';
return window.location.href = window.location.pathname + urlSuffix; return window.location.href = window.location.pathname + urlSuffix;
} else if (data.merge_error) { } else if (data.merge_error) {
return this.$widgetBody.html("<h4>" + data.merge_error + "</h4>"); return _this.$widgetBody.html("<h4>" + data.merge_error + "</h4>");
} else { } else {
callback = function() { callback = function() {
return merge_request_widget.mergeInProgress(deleteSourceBranch); return merge_request_widget.mergeInProgress(deleteSourceBranch);
......
@mixin btn-default { @mixin btn-default {
border-radius: 3px; border-radius: 3px;
font-size: $gl-font-size; font-size: $gl-font-size;
font-weight: 500; font-weight: 400;
padding: $gl-vert-padding $gl-btn-padding; padding: $gl-vert-padding $gl-btn-padding;
&:focus, &:focus,
......
...@@ -42,6 +42,11 @@ ...@@ -42,6 +42,11 @@
border-radius: $border-radius-base; border-radius: $border-radius-base;
white-space: nowrap; white-space: nowrap;
&[disabled] {
background-color: $input-bg-disabled;
cursor: not-allowed;
}
&.no-outline { &.no-outline {
outline: 0; outline: 0;
} }
......
...@@ -54,6 +54,10 @@ ...@@ -54,6 +54,10 @@
@media (min-width: $screen-sm-min) { @media (min-width: $screen-sm-min) {
width: 50%; width: 50%;
} }
.dropdown-menu-toggle {
width: 100%;
}
} }
.member-access-text { .member-access-text {
......
.notification-list-item { .notification-list-item {
line-height: 34px; line-height: 34px;
.dropdown-menu {
@extend .dropdown-menu-align-right;
}
} }
.notification { .notification {
......
...@@ -188,6 +188,10 @@ ...@@ -188,6 +188,10 @@
margin-left: 10px; margin-left: 10px;
} }
.notification-dropdown .dropdown-menu {
@extend .dropdown-menu-align-right;
}
.download-button { .download-button {
@media (max-width: $screen-md-max) { @media (max-width: $screen-md-max) {
margin-left: 0; margin-left: 0;
......
...@@ -35,13 +35,12 @@ class Projects::ProjectMembersController < Projects::ApplicationController ...@@ -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)) @group_links = @project.project_group_links.where(group_id: @project.invited_groups.search(params[:search]).select(:id))
end 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 @project_members = Member.
member_ids += group_members.pluck(:id) where(wheres.join(' OR ')).
end order(access_level: :desc).page(params[:page])
@project_members = Member.where(id: member_ids).order(access_level: :desc).page(params[:page])
@requesters = AccessRequestsFinder.new(@project).execute(current_user) @requesters = AccessRequestsFinder.new(@project).execute(current_user)
......
...@@ -37,6 +37,12 @@ class SessionsController < Devise::SessionsController ...@@ -37,6 +37,12 @@ class SessionsController < Devise::SessionsController
end end
end end
def destroy
super
# hide the signed_out notice
flash[:notice] = nil
end
private private
# Handle an "initial setup" state, where there's only one user, it's an admin, # Handle an "initial setup" state, where there's only one user, it's an admin,
......
class SnippetsFinder class SnippetsFinder
def execute(current_user, params = {}) def execute(current_user, params = {})
filter = params[:filter] filter = params[:filter]
user = params.fetch(:user, current_user)
case filter case filter
when :all then when :all then
snippets(current_user).fresh snippets(current_user).fresh
when :public then
Snippet.are_public.fresh
when :by_user then when :by_user then
by_user(current_user, params[:user], params[:scope]) by_user(current_user, user, params[:scope])
when :by_project when :by_project
by_project(current_user, params[:project]) by_project(current_user, params[:project])
end end
......
...@@ -159,6 +159,11 @@ module GitlabRoutingHelper ...@@ -159,6 +159,11 @@ module GitlabRoutingHelper
resend_invite_namespace_project_project_member_path(project_member.source.namespace, project_member.source, project_member) resend_invite_namespace_project_project_member_path(project_member.source.namespace, project_member.source, project_member)
end end
# Snippets
def personal_snippet_url(snippet, *args)
snippet_url(snippet)
end
# Groups # Groups
## Members ## Members
......
...@@ -6,9 +6,14 @@ class PersonalSnippetPolicy < BasePolicy ...@@ -6,9 +6,14 @@ class PersonalSnippetPolicy < BasePolicy
if @subject.author == @user if @subject.author == @user
can! :read_personal_snippet can! :read_personal_snippet
can! :update_personal_snippet can! :update_personal_snippet
can! :destroy_personal_snippet
can! :admin_personal_snippet can! :admin_personal_snippet
end end
unless @user.external?
can! :create_personal_snippet
end
if @subject.internal? && !@user.external? if @subject.internal? && !@user.external?
can! :read_personal_snippet can! :read_personal_snippet
end end
......
:plain :plain
var $listItem = $('#{escape_javascript(render('shared/members/member', member: @group_member))}'); var $listItem = $('#{escape_javascript(render('shared/members/member', member: @group_member))}');
$("##{dom_id(@group_member)} .list-item-name").replaceWith($listItem.find('.list-item-name')); $("##{dom_id(@group_member)} .list-item-name").replaceWith($listItem.find('.list-item-name'));
gl.utils.localTimeAgo($('.js-timeago'), $("##{dom_id(@group_member)}"));
- page_title @path.split("/").reverse.map(&:humanize) - page_title @path.split("/").reverse.map(&:humanize)
.documentation.wiki .documentation.wiki
= markdown @markdown.gsub('$your_email', current_user.try(:email) || "email@example.com") = markdown @markdown
- if current_user - if current_user
- can_admin_group = can?(current_user, :admin_group, @group) - can_admin_group = can?(current_user, :admin_group, @group)
- can_edit = 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 .controls
.dropdown.group-settings-dropdown .dropdown.group-settings-dropdown
%a.dropdown-new.btn.btn-default#group-settings-button{href: '#', 'data-toggle' => 'dropdown'} %a.dropdown-new.btn.btn-default#group-settings-button{href: '#', 'data-toggle' => 'dropdown'}
...@@ -14,13 +12,7 @@ ...@@ -14,13 +12,7 @@
- if can_admin_group - if can_admin_group
= nav_link(path: 'groups#projects') do = nav_link(path: 'groups#projects') do
= link_to 'Projects', projects_group_path(@group), title: 'Projects' = 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 %li.divider
- if can_edit
%li %li
= link_to 'Edit Group', edit_group_path(@group) = 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
...@@ -6,23 +6,14 @@ ...@@ -6,23 +6,14 @@
= icon('caret-down') = icon('caret-down')
%ul.dropdown-menu.dropdown-menu-align-right %ul.dropdown-menu.dropdown-menu-align-right
- can_edit = can?(current_user, :admin_project, @project) - 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 = render 'layouts/nav/project_settings', can_edit: can_edit
- if can_edit || can_leave
%li.divider
- if can_edit - if can_edit
%li.divider
%li %li
= link_to edit_project_path(@project) do = link_to edit_project_path(@project) do
Edit Project 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
.scrolling-tabs-container{ class: nav_control_class } .scrolling-tabs-container{ class: nav_control_class }
.fade-left .fade-left
......
...@@ -30,7 +30,7 @@ ...@@ -30,7 +30,7 @@
%br %br
.clearfix .clearfix
.form-group.pull-left.global-notification-setting .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 .clearfix
......
...@@ -146,7 +146,7 @@ ...@@ -146,7 +146,7 @@
such as compressing file revisions and removing unreachable objects. such as compressing file revisions and removing unreachable objects.
.col-lg-9 .col-lg-9
= link_to 'Housekeeping', housekeeping_namespace_project_path(@project.namespace, @project), = link_to 'Housekeeping', housekeeping_namespace_project_path(@project.namespace, @project),
method: :post, class: "btn btn-save" method: :post, class: "btn btn-default"
%hr %hr
.row.prepend-top-default .row.prepend-top-default
.col-lg-3 .col-lg-3
......
:plain :plain
var $listItem = $('#{escape_javascript(render('shared/members/group', group_link: @group_link))}'); 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')); $("#group_member_#{@group_link.id} .list-item-name").replaceWith($listItem.find('.list-item-name'));
gl.utils.localTimeAgo($('.js-timeago'), $("#group_member_#{@group_link.id}"));
:plain :plain
var $listItem = $('#{escape_javascript(render('shared/members/member', member: @project_member))}'); var $listItem = $('#{escape_javascript(render('shared/members/member', member: @project_member))}');
$("##{dom_id(@project_member)} .list-item-name").replaceWith($listItem.find('.list-item-name')); $("##{dom_id(@project_member)} .list-item-name").replaceWith($listItem.find('.list-item-name'));
gl.utils.localTimeAgo($('.js-timeago'), $("##{dom_id(@project_member)}"));
- if can?(current_user, :request_access, source) - model_name = source.model_name.to_s.downcase
- if requester = source.requesters.find_by(user_id: current_user.id)
- 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]), = link_to 'Withdraw Access Request', polymorphic_path([:leave, source, :members]),
method: :delete, method: :delete,
data: { confirm: remove_member_message(requester) }, data: { confirm: remove_member_message(requester) },
class: 'btn' class: 'btn'
- else - elsif source.request_access_enabled && can?(current_user, :request_access, source)
= link_to 'Request Access', polymorphic_path([:request_access, source, :members]), = link_to 'Request Access', polymorphic_path([:request_access, source, :members]),
method: :post, method: :post,
class: 'btn' class: 'btn'
- group_link = local_assigns[:group_link] - group_link = local_assigns[:group_link]
- group = group_link.group - group = group_link.group
- can_admin_member = can?(current_user, :admin_project_member, @project) - 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" } %span{ class: "list-item-name" }
= image_tag group_icon(group), class: "avatar s40", alt: '' = image_tag group_icon(group), class: "avatar s40", alt: ''
%strong %strong
...@@ -14,7 +15,23 @@ ...@@ -14,7 +15,23 @@
Expires in #{distance_of_time_in_words_to_now(group_link.expires_at)} Expires in #{distance_of_time_in_words_to_now(group_link.expires_at)}
.controls.member-controls .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 = 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 .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 = 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 %i.clear-icon.js-clear-input
......
...@@ -48,9 +48,25 @@ ...@@ -48,9 +48,25 @@
- if show_controls && (member.respond_to?(:group) && @group) || (member.respond_to?(:project) && @project) - if show_controls && (member.respond_to?(:group) && @group) || (member.respond_to?(:project) && @project)
- if user != current_user - if user != current_user
= form_for member, remote: true, html: { class: 'form-horizontal js-edit-member-form' } do |f| = 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 .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 %i.clear-icon.js-clear-input
- else - else
%span.member-access-text= member.human_access %span.member-access-text= member.human_access
......
- left_align = local_assigns[:left_align]
- if notification_setting - if notification_setting
.dropdown.notification-dropdown .dropdown.notification-dropdown
= form_for notification_setting, remote: true, html: { class: "inline notification-form" } do |f| = form_for notification_setting, remote: true, html: { class: "inline notification-form" } do |f|
...@@ -19,7 +18,7 @@ ...@@ -19,7 +18,7 @@
= notification_title(notification_setting.level) = notification_title(notification_setting.level)
= icon("caret-down") = 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 = content_for :scripts_body do
= render "shared/notifications/custom_notifications", notification_setting: notification_setting = render "shared/notifications/custom_notifications", notification_setting: notification_setting
- 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)] }
%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)] }
- NotificationSetting.levels.each_key do |level| - NotificationSetting.levels.each_key do |level|
- next if level == "custom" - next if level == "custom"
- next if level == "global" && notification_setting.source.nil? - next if level == "global" && notification_setting.source.nil?
......
---
title: Moved Leave Project and Leave Group buttons to access_request_buttons from
the settings dropdown
merge_request: 7600
author:
---
title: 'fix: removed signed_out notification'
merge_request: 7958
author: jnoortheen
---
title: Changed Housekeeping button on project settings page to default styling
merge_request:
author: Ryan Harris
---
title: 'API: Endpoint to expose personal snippets as /snippets'
merge_request: 6373
author: Bernard Guyzmo Pratz
---
title: "fix display hook error message"
merge_request: 7775
author: basyura
---
title: Allow branch names with dots on API endpoint
merge_request:
author:
---
title: Updated members dropdowns
merge_request:
author:
---
title: Updates the font weight of button styles because of the change to system fonts
merge_request:
author:
# Documentation # GitLab Community Edition documentation
## User documentation ## User documentation
......
...@@ -429,7 +429,7 @@ DELETE /projects/:id/merge_requests/:merge_request_id ...@@ -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 | | `merge_request_id` | integer | yes | The ID of a project's merge request |
```bash ```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 ## Accept MR
......
# 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"
}
]
```
...@@ -33,7 +33,7 @@ built and deployed under a dynamic environment and can be previewed with an ...@@ -33,7 +33,7 @@ built and deployed under a dynamic environment and can be previewed with an
also dynamically URL. also dynamically URL.
The details of the Review Apps implementation depend widely on your real 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 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 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 on a temporary instance and booting up this instance with all required software
......
...@@ -271,9 +271,9 @@ sudo usermod -aG redis git ...@@ -271,9 +271,9 @@ sudo usermod -aG redis git
### Clone the Source ### Clone the Source
# Clone GitLab repository # 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 ### Configure It
......
...@@ -72,7 +72,7 @@ sudo -u git -H git checkout 8-15-stable-ee ...@@ -72,7 +72,7 @@ sudo -u git -H git checkout 8-15-stable-ee
```bash ```bash
cd /home/git/gitlab-shell cd /home/git/gitlab-shell
sudo -u git -H git fetch --all --tags 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 ### 6. Update gitlab-workhorse
......
...@@ -117,7 +117,12 @@ class Spinach::Features::GroupMembers < Spinach::FeatureSteps ...@@ -117,7 +117,12 @@ class Spinach::Features::GroupMembers < Spinach::FeatureSteps
member = mary_jane_member member = mary_jane_member
page.within "#group_member_#{member.id}" do 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 wait_for_ajax
end end
end end
......
...@@ -65,7 +65,11 @@ class Spinach::Features::ProjectTeamManagement < Spinach::FeatureSteps ...@@ -65,7 +65,11 @@ class Spinach::Features::ProjectTeamManagement < Spinach::FeatureSteps
user = User.find_by(name: 'Dmitriy') user = User.find_by(name: 'Dmitriy')
project_member = project.project_members.find_by(user_id: user.id) project_member = project.project_members.find_by(user_id: user.id)
page.within "#project_member_#{project_member.id}" do 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
end end
...@@ -144,7 +148,7 @@ class Spinach::Features::ProjectTeamManagement < Spinach::FeatureSteps ...@@ -144,7 +148,7 @@ class Spinach::Features::ProjectTeamManagement < Spinach::FeatureSteps
step 'I should see "Opensource" group user listing' do step 'I should see "Opensource" group user listing' do
page.within '.project-members-groups' do page.within '.project-members-groups' do
expect(page).to have_content('OpenSource') expect(page).to have_content('OpenSource')
expect(find('select').value).to eq('40') expect(first('.group_member')).to have_content('Master')
end end
end end
end end
...@@ -64,6 +64,7 @@ module API ...@@ -64,6 +64,7 @@ module API
mount ::API::Session mount ::API::Session
mount ::API::Settings mount ::API::Settings
mount ::API::SidekiqMetrics mount ::API::SidekiqMetrics
mount ::API::Snippets
mount ::API::Subscriptions mount ::API::Subscriptions
mount ::API::SystemHooks mount ::API::SystemHooks
mount ::API::Tags mount ::API::Tags
......
...@@ -23,9 +23,9 @@ module API ...@@ -23,9 +23,9 @@ module API
success Entities::RepoBranch success Entities::RepoBranch
end end
params do params do
requires :branch, type: String, regexp: /.+/, desc: 'The name of the branch' requires :branch, type: String, desc: 'The name of the branch'
end end
get ':id/repository/branches/:branch' do get ':id/repository/branches/:branch', requirements: { branch: /.+/ } do
branch = user_project.repository.find_branch(params[:branch]) branch = user_project.repository.find_branch(params[:branch])
not_found!("Branch") unless branch not_found!("Branch") unless branch
...@@ -39,11 +39,11 @@ module API ...@@ -39,11 +39,11 @@ module API
success Entities::RepoBranch success Entities::RepoBranch
end end
params do 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_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' optional :developers_can_merge, type: Boolean, desc: 'Flag if developers can merge to that branch'
end end
put ':id/repository/branches/:branch/protect' do put ':id/repository/branches/:branch/protect', requirements: { branch: /.+/ } do
authorize_admin_project authorize_admin_project
branch = user_project.repository.find_branch(params[:branch]) branch = user_project.repository.find_branch(params[:branch])
...@@ -76,9 +76,9 @@ module API ...@@ -76,9 +76,9 @@ module API
success Entities::RepoBranch success Entities::RepoBranch
end end
params do params do
requires :branch, type: String, regexp: /.+/, desc: 'The name of the branch' requires :branch, type: String, desc: 'The name of the branch'
end end
put ':id/repository/branches/:branch/unprotect' do put ':id/repository/branches/:branch/unprotect', requirements: { branch: /.+/ } do
authorize_admin_project authorize_admin_project
branch = user_project.repository.find_branch(params[:branch]) branch = user_project.repository.find_branch(params[:branch])
...@@ -112,9 +112,9 @@ module API ...@@ -112,9 +112,9 @@ module API
desc 'Delete a branch' desc 'Delete a branch'
params do params do
requires :branch, type: String, regexp: /.+/, desc: 'The name of the branch' requires :branch, type: String, desc: 'The name of the branch'
end end
delete ":id/repository/branches/:branch" do delete ":id/repository/branches/:branch", requirements: { branch: /.+/ } do
authorize_push_project authorize_push_project
result = DeleteBranchService.new(user_project, current_user). result = DeleteBranchService.new(user_project, current_user).
......
...@@ -201,6 +201,19 @@ module API ...@@ -201,6 +201,19 @@ module API
end end
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 class ProjectEntity < Grape::Entity
expose :id, :iid expose :id, :iid
expose(:project_id) { |entity| entity.project.id } expose(:project_id) { |entity| entity.project.id }
......
...@@ -117,11 +117,20 @@ module API ...@@ -117,11 +117,20 @@ module API
success Entities::Project success Entities::Project
end end
params do 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 use :pagination
end end
get ":id/projects" do get ":id/projects" do
group = find_group!(params[:id]) group = find_group!(params[:id])
projects = GroupProjectsFinder.new(group).execute(current_user) projects = GroupProjectsFinder.new(group).execute(current_user)
projects = filter_projects(projects)
present paginate(projects), with: Entities::Project, user: current_user present paginate(projects), with: Entities::Project, user: current_user
end end
......
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
...@@ -24,6 +24,8 @@ module Gitlab ...@@ -24,6 +24,8 @@ module Gitlab
wiki_page_url wiki_page_url
when ProjectSnippet when ProjectSnippet
project_snippet_url(object) project_snippet_url(object)
when Snippet
personal_snippet_url(object)
else else
raise NotImplementedError.new("No URL builder defined for #{object.class}") raise NotImplementedError.new("No URL builder defined for #{object.class}")
end end
......
...@@ -10,7 +10,7 @@ feature 'Groups > Members > Last owner cannot leave group', feature: true do ...@@ -10,7 +10,7 @@ feature 'Groups > Members > Last owner cannot leave group', feature: true do
visit group_path(group) visit group_path(group)
end end
scenario 'user does not see a "Leave Group" link' do scenario 'user does not see a "Leave group" link' do
expect(page).not_to have_content 'Leave Group' expect(page).not_to have_content 'Leave group'
end end
end end
...@@ -13,7 +13,7 @@ feature 'Groups > Members > Member leaves group', feature: true do ...@@ -13,7 +13,7 @@ feature 'Groups > Members > Member leaves group', feature: true do
end end
scenario 'user leaves group' do scenario 'user leaves group' do
click_link 'Leave Group' click_link 'Leave group'
expect(current_path).to eq(dashboard_groups_path) expect(current_path).to eq(dashboard_groups_path)
expect(group.users.exists?(user.id)).to be_falsey expect(group.users.exists?(user.id)).to be_falsey
......
...@@ -29,7 +29,7 @@ feature 'Groups > Members > User requests access', feature: true do ...@@ -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 'Your request for access has been queued for review.'
expect(page).to have_content 'Withdraw Access Request' 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 end
scenario 'user does not see private projects' do scenario 'user does not see private projects' do
......
require 'spec_helper' require 'spec_helper'
describe 'Help Pages', feature: true do 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 describe 'Get the main help page' do
shared_examples_for 'help page' do |prefix: ''| shared_examples_for 'help page' do |prefix: ''|
it 'prefixes links correctly' do it 'prefixes links correctly' do
......
...@@ -16,12 +16,17 @@ feature 'Projects > Members > Anonymous user sees members', feature: true, js: t ...@@ -16,12 +16,17 @@ feature 'Projects > Members > Anonymous user sees members', feature: true, js: t
end end
it 'updates group access level' do 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 wait_for_ajax
visit namespace_project_project_members_path(project.namespace, project) 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 end
it 'updates expiry date' do it 'updates expiry date' do
......
...@@ -12,6 +12,6 @@ feature 'Projects > Members > Group member cannot leave group project', feature: ...@@ -12,6 +12,6 @@ feature 'Projects > Members > Group member cannot leave group project', feature:
end end
scenario 'user does not see a "Leave project" link' do 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
end end
require 'spec_helper' 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(:user) { create(:user) }
let(:owner) { create(:user) } let(:owner) { create(:user) }
let(:group) { create(:group, :public, :access_requestable) } let(:group) { create(:group, :public, :access_requestable) }
......
...@@ -11,7 +11,7 @@ feature 'Projects > Members > Member leaves project', feature: true do ...@@ -11,7 +11,7 @@ feature 'Projects > Members > Member leaves project', feature: true do
end end
scenario 'user leaves project' do scenario 'user leaves project' do
click_link 'Leave Project' click_link 'Leave project'
expect(current_path).to eq(dashboard_projects_path) expect(current_path).to eq(dashboard_projects_path)
expect(project.users.exists?(user.id)).to be_falsey expect(project.users.exists?(user.id)).to be_falsey
......
...@@ -8,7 +8,7 @@ feature 'Projects > Members > Owner cannot leave project', feature: true do ...@@ -8,7 +8,7 @@ feature 'Projects > Members > Owner cannot leave project', feature: true do
visit namespace_project_path(project.namespace, project) visit namespace_project_path(project.namespace, project)
end end
scenario 'user does not see a "Leave Project" link' do 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
end end
...@@ -9,65 +9,74 @@ describe SnippetsFinder do ...@@ -9,65 +9,74 @@ describe SnippetsFinder do
let(:project2) { create(:empty_project, :private, group: group) } let(:project2) { create(:empty_project, :private, group: group) }
context ':all filter' do context ':all filter' do
before do let!(:snippet1) { create(:personal_snippet, :private) }
@snippet1 = create(:personal_snippet, :private) let!(:snippet2) { create(:personal_snippet, :internal) }
@snippet2 = create(:personal_snippet, :internal) let!(:snippet3) { create(:personal_snippet, :public) }
@snippet3 = create(:personal_snippet, :public)
end
it "returns all private and internal snippets" do it "returns all private and internal snippets" do
snippets = SnippetsFinder.new.execute(user, filter: :all) snippets = SnippetsFinder.new.execute(user, filter: :all)
expect(snippets).to include(@snippet2, @snippet3) expect(snippets).to include(snippet2, snippet3)
expect(snippets).not_to include(@snippet1) expect(snippets).not_to include(snippet1)
end end
it "returns all public snippets" do it "returns all public snippets" do
snippets = SnippetsFinder.new.execute(nil, filter: :all) snippets = SnippetsFinder.new.execute(nil, filter: :all)
expect(snippets).to include(@snippet3) expect(snippets).to include(snippet3)
expect(snippets).not_to include(@snippet1, @snippet2) expect(snippets).not_to include(snippet1, snippet2)
end end
end end
context ':by_user filter' do context ':public filter' do
before do let!(:snippet1) { create(:personal_snippet, :private) }
@snippet1 = create(:personal_snippet, :private, author: user) let!(:snippet2) { create(:personal_snippet, :internal) }
@snippet2 = create(:personal_snippet, :internal, author: user) let!(:snippet3) { create(:personal_snippet, :public) }
@snippet3 = create(:personal_snippet, :public, author: user)
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
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 it "returns all public and internal snippets" do
snippets = SnippetsFinder.new.execute(user1, filter: :by_user, user: user) snippets = SnippetsFinder.new.execute(user1, filter: :by_user, user: user)
expect(snippets).to include(@snippet2, @snippet3) expect(snippets).to include(snippet2, snippet3)
expect(snippets).not_to include(@snippet1) expect(snippets).not_to include(snippet1)
end end
it "returns internal snippets" do it "returns internal snippets" do
snippets = SnippetsFinder.new.execute(user, filter: :by_user, user: user, scope: "are_internal") snippets = SnippetsFinder.new.execute(user, filter: :by_user, user: user, scope: "are_internal")
expect(snippets).to include(@snippet2) expect(snippets).to include(snippet2)
expect(snippets).not_to include(@snippet1, @snippet3) expect(snippets).not_to include(snippet1, snippet3)
end end
it "returns private snippets" do it "returns private snippets" do
snippets = SnippetsFinder.new.execute(user, filter: :by_user, user: user, scope: "are_private") snippets = SnippetsFinder.new.execute(user, filter: :by_user, user: user, scope: "are_private")
expect(snippets).to include(@snippet1) expect(snippets).to include(snippet1)
expect(snippets).not_to include(@snippet2, @snippet3) expect(snippets).not_to include(snippet2, snippet3)
end end
it "returns public snippets" do it "returns public snippets" do
snippets = SnippetsFinder.new.execute(user, filter: :by_user, user: user, scope: "are_public") snippets = SnippetsFinder.new.execute(user, filter: :by_user, user: user, scope: "are_public")
expect(snippets).to include(@snippet3) expect(snippets).to include(snippet3)
expect(snippets).not_to include(@snippet1, @snippet2) expect(snippets).not_to include(snippet1, snippet2)
end end
it "returns all snippets" do it "returns all snippets" do
snippets = SnippetsFinder.new.execute(user, filter: :by_user, user: user) 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 end
it "returns only public snippets if unauthenticated user" do it "returns only public snippets if unauthenticated user" do
snippets = SnippetsFinder.new.execute(nil, filter: :by_user, user: user) snippets = SnippetsFinder.new.execute(nil, filter: :by_user, user: user)
expect(snippets).to include(@snippet3) expect(snippets).to include(snippet3)
expect(snippets).not_to include(@snippet2, @snippet1) expect(snippets).not_to include(snippet2, snippet1)
end end
end end
......
...@@ -106,6 +106,18 @@ ...@@ -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() { return describe('getCIStatus', function() {
beforeEach(function() { beforeEach(function() {
this.ciStatusData = { this.ciStatusData = {
......
...@@ -11,6 +11,7 @@ describe API::Branches, api: true do ...@@ -11,6 +11,7 @@ describe API::Branches, api: true do
let!(:guest) { create(:project_member, :guest, user: user2, project: project) } let!(:guest) { create(:project_member, :guest, user: user2, project: project) }
let!(:branch_name) { 'feature' } let!(:branch_name) { 'feature' }
let!(:branch_sha) { '0b4bc9a49b562e85de7cc9e834518ea6828729b9' } 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 describe "GET /projects/:id/repository/branches" do
it "returns an array of project branches" do it "returns an array of project branches" do
...@@ -37,6 +38,13 @@ describe API::Branches, api: true do ...@@ -37,6 +38,13 @@ describe API::Branches, api: true do
expect(json_response['developers_can_merge']).to eq(false) expect(json_response['developers_can_merge']).to eq(false)
end end
it "returns the branch information for a single branch with dots in the name" do
get api("/projects/#{project.id}/repository/branches/with.1.2.3", user)
expect(response).to have_http_status(200)
expect(json_response['name']).to eq("with.1.2.3")
end
context 'on a merged branch' do context 'on a merged branch' do
it "returns the branch information for a single branch" do it "returns the branch information for a single branch" do
get api("/projects/#{project.id}/repository/branches/merge-test", user) get api("/projects/#{project.id}/repository/branches/merge-test", user)
...@@ -71,6 +79,14 @@ describe API::Branches, api: true do ...@@ -71,6 +79,14 @@ describe API::Branches, api: true do
expect(json_response['developers_can_merge']).to eq(false) expect(json_response['developers_can_merge']).to eq(false)
end 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 it 'protects a single branch and developers can push' do
put api("/projects/#{project.id}/repository/branches/#{branch_name}/protect", user), put api("/projects/#{project.id}/repository/branches/#{branch_name}/protect", user),
developers_can_push: true developers_can_push: true
...@@ -220,6 +236,14 @@ describe API::Branches, api: true do ...@@ -220,6 +236,14 @@ describe API::Branches, api: true do
expect(json_response['protected']).to eq(false) expect(json_response['protected']).to eq(false)
end 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 it "returns success when unprotect branch" do
put api("/projects/#{project.id}/repository/branches/unknown/unprotect", user) put api("/projects/#{project.id}/repository/branches/unknown/unprotect", user)
expect(response).to have_http_status(404) expect(response).to have_http_status(404)
...@@ -292,6 +316,13 @@ describe API::Branches, api: true do ...@@ -292,6 +316,13 @@ describe API::Branches, api: true do
expect(json_response['branch_name']).to eq(branch_name) expect(json_response['branch_name']).to eq(branch_name)
end 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 it 'returns 404 if branch not exists' do
delete api("/projects/#{project.id}/repository/branches/foobar", user) delete api("/projects/#{project.id}/repository/branches/foobar", user)
expect(response).to have_http_status(404) expect(response).to have_http_status(404)
......
...@@ -245,6 +245,17 @@ describe API::Groups, api: true do ...@@ -245,6 +245,17 @@ describe API::Groups, api: true do
expect(project_names).to match_array([project1.name, project3.name]) expect(project_names).to match_array([project1.name, project3.name])
end 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 it "does not return a non existing group" do
get api("/groups/1328/projects", user1) get api("/groups/1328/projects", user1)
expect(response).to have_http_status(404) expect(response).to have_http_status(404)
......
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
...@@ -75,7 +75,8 @@ module LoginHelpers ...@@ -75,7 +75,8 @@ module LoginHelpers
def logout def logout
find(".header-user-dropdown-toggle").click find(".header-user-dropdown-toggle").click
click_link "Sign out" 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 end
# Logout without JavaScript driver # Logout without JavaScript driver
......
Markdown is supported
0%
or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment