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
This commit is contained in:
commit
e5acebd9b9
|
@ -1 +1 @@
|
|||
1.1.0
|
||||
1.1.1
|
||||
|
|
|
@ -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) {
|
||||
|
|
|
@ -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);
|
||||
})();
|
||||
|
|
|
@ -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);
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
|
|
|
@ -54,6 +54,10 @@
|
|||
@media (min-width: $screen-sm-min) {
|
||||
width: 50%;
|
||||
}
|
||||
|
||||
.dropdown-menu-toggle {
|
||||
width: 100%;
|
||||
}
|
||||
}
|
||||
|
||||
.member-access-text {
|
||||
|
|
|
@ -1,5 +1,9 @@
|
|||
.notification-list-item {
|
||||
line-height: 34px;
|
||||
|
||||
.dropdown-menu {
|
||||
@extend .dropdown-menu-align-right;
|
||||
}
|
||||
}
|
||||
|
||||
.notification {
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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)
|
||||
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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)}"));
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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}"));
|
||||
|
|
|
@ -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)}"));
|
||||
|
|
|
@ -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'
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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?
|
||||
|
|
|
@ -0,0 +1,5 @@
|
|||
---
|
||||
title: Moved Leave Project and Leave Group buttons to access_request_buttons from
|
||||
the settings dropdown
|
||||
merge_request: 7600
|
||||
author:
|
|
@ -0,0 +1,4 @@
|
|||
---
|
||||
title: 'fix: removed signed_out notification'
|
||||
merge_request: 7958
|
||||
author: jnoortheen
|
|
@ -0,0 +1,4 @@
|
|||
---
|
||||
title: Changed Housekeeping button on project settings page to default styling
|
||||
merge_request:
|
||||
author: Ryan Harris
|
|
@ -0,0 +1,4 @@
|
|||
---
|
||||
title: 'API: Endpoint to expose personal snippets as /snippets'
|
||||
merge_request: 6373
|
||||
author: Bernard Guyzmo Pratz
|
|
@ -0,0 +1,4 @@
|
|||
---
|
||||
title: "fix display hook error message"
|
||||
merge_request: 7775
|
||||
author: basyura
|
|
@ -0,0 +1,4 @@
|
|||
---
|
||||
title: Allow branch names with dots on API endpoint
|
||||
merge_request:
|
||||
author:
|
|
@ -0,0 +1,4 @@
|
|||
---
|
||||
title: Updated members dropdowns
|
||||
merge_request:
|
||||
author:
|
|
@ -0,0 +1,4 @@
|
|||
---
|
||||
title: Updates the font weight of button styles because of the change to system fonts
|
||||
merge_request:
|
||||
author:
|
|
@ -1,4 +1,4 @@
|
|||
# Documentation
|
||||
# GitLab Community Edition documentation
|
||||
|
||||
## User documentation
|
||||
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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"
|
||||
}
|
||||
]
|
||||
```
|
||||
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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).
|
||||
|
|
|
@ -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 }
|
||||
|
|
|
@ -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
|
||||
|
||||
|
|
|
@ -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
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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) }
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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 ':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
|
||||
before do
|
||||
@snippet1 = create(:personal_snippet, :private, author: user)
|
||||
@snippet2 = create(:personal_snippet, :internal, author: user)
|
||||
@snippet3 = create(:personal_snippet, :public, author: user)
|
||||
end
|
||||
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
|
||||
|
||||
|
|
|
@ -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 = {
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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
|
|
@ -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
|
||||
|
|
Loading…
Reference in New Issue