Add latest changes from gitlab-org/gitlab@master

This commit is contained in:
GitLab Bot 2019-09-26 18:06:29 +00:00
parent 4e9f718e19
commit e66d6781ef
34 changed files with 350 additions and 206 deletions

View File

@ -17,7 +17,7 @@
- [ ] Follow the [Documentation Guidelines](https://docs.gitlab.com/ee/development/documentation/) and [Style Guide](https://docs.gitlab.com/ee/development/documentation/styleguide.html).
- [ ] If applicable, update the [permissions table](https://docs.gitlab.com/ee/user/permissions.html).
- [ ] Link docs to and from the higher-level index page, plus other related docs where helpful.
- [ ] Apply the ~Documentation label.
- [ ] Apply the ~documentation label.
## Review checklist

View File

@ -4,96 +4,97 @@ import Api from './api';
import { normalizeHeaders } from './lib/utils/common_utils';
import { __ } from '~/locale';
export default function groupsSelect() {
import(/* webpackChunkName: 'select2' */ 'select2/select2')
.then(() => {
// Needs to be accessible in rspec
window.GROUP_SELECT_PER_PAGE = 20;
$('.ajax-groups-select').each(function setAjaxGroupsSelect2() {
const $select = $(this);
const allAvailable = $select.data('allAvailable');
const skipGroups = $select.data('skipGroups') || [];
const parentGroupID = $select.data('parentId');
const groupsPath = parentGroupID
? Api.subgroupsPath.replace(':id', parentGroupID)
: Api.groupsPath;
const groupsSelect = () => {
// Needs to be accessible in rspec
window.GROUP_SELECT_PER_PAGE = 20;
$('.ajax-groups-select').each(function setAjaxGroupsSelect2() {
const $select = $(this);
const allAvailable = $select.data('allAvailable');
const skipGroups = $select.data('skipGroups') || [];
const parentGroupID = $select.data('parentId');
const groupsPath = parentGroupID
? Api.subgroupsPath.replace(':id', parentGroupID)
: Api.groupsPath;
$select.select2({
placeholder: __('Search for a group'),
allowClear: $select.hasClass('allowClear'),
multiple: $select.hasClass('multiselect'),
minimumInputLength: 0,
ajax: {
url: Api.buildUrl(groupsPath),
dataType: 'json',
quietMillis: 250,
transport(params) {
axios[params.type.toLowerCase()](params.url, {
params: params.data,
})
.then(res => {
const results = res.data || [];
const headers = normalizeHeaders(res.headers);
const currentPage = parseInt(headers['X-PAGE'], 10) || 0;
const totalPages = parseInt(headers['X-TOTAL-PAGES'], 10) || 0;
const more = currentPage < totalPages;
$select.select2({
placeholder: __('Search for a group'),
allowClear: $select.hasClass('allowClear'),
multiple: $select.hasClass('multiselect'),
minimumInputLength: 0,
ajax: {
url: Api.buildUrl(groupsPath),
dataType: 'json',
quietMillis: 250,
transport(params) {
axios[params.type.toLowerCase()](params.url, {
params: params.data,
})
.then(res => {
const results = res.data || [];
const headers = normalizeHeaders(res.headers);
const currentPage = parseInt(headers['X-PAGE'], 10) || 0;
const totalPages = parseInt(headers['X-TOTAL-PAGES'], 10) || 0;
const more = currentPage < totalPages;
params.success({
results,
pagination: {
more,
},
});
})
.catch(params.error);
},
data(search, page) {
return {
search,
page,
per_page: window.GROUP_SELECT_PER_PAGE,
all_available: allAvailable,
};
},
results(data, page) {
if (data.length) return { results: [] };
const groups = data.length ? data : data.results || [];
const more = data.pagination ? data.pagination.more : false;
const results = groups.filter(group => skipGroups.indexOf(group.id) === -1);
return {
params.success({
results,
page,
more,
};
},
},
// eslint-disable-next-line consistent-return
initSelection(element, callback) {
const id = $(element).val();
if (id !== '') {
return Api.group(id, callback);
}
},
formatResult(object) {
return `<div class='group-result'> <div class='group-name'>${object.full_name}</div> <div class='group-path'>${object.full_path}</div> </div>`;
},
formatSelection(object) {
return object.full_name;
},
dropdownCssClass: 'ajax-groups-dropdown select2-infinite',
// we do not want to escape markup since we are displaying html in results
escapeMarkup(m) {
return m;
},
});
pagination: {
more,
},
});
})
.catch(params.error);
},
data(search, page) {
return {
search,
page,
per_page: window.GROUP_SELECT_PER_PAGE,
all_available: allAvailable,
};
},
results(data, page) {
if (data.length) return { results: [] };
$select.on('select2-loaded', () => {
const dropdown = document.querySelector('.select2-infinite .select2-results');
dropdown.style.height = `${Math.floor(dropdown.scrollHeight)}px`;
});
});
})
const groups = data.length ? data : data.results || [];
const more = data.pagination ? data.pagination.more : false;
const results = groups.filter(group => skipGroups.indexOf(group.id) === -1);
return {
results,
page,
more,
};
},
},
// eslint-disable-next-line consistent-return
initSelection(element, callback) {
const id = $(element).val();
if (id !== '') {
return Api.group(id, callback);
}
},
formatResult(object) {
return `<div class='group-result'> <div class='group-name'>${object.full_name}</div> <div class='group-path'>${object.full_path}</div> </div>`;
},
formatSelection(object) {
return object.full_name;
},
dropdownCssClass: 'ajax-groups-dropdown select2-infinite',
// we do not want to escape markup since we are displaying html in results
escapeMarkup(m) {
return m;
},
});
$select.on('select2-loaded', () => {
const dropdown = document.querySelector('.select2-infinite .select2-results');
dropdown.style.height = `${Math.floor(dropdown.scrollHeight)}px`;
});
});
};
export default () =>
import(/* webpackChunkName: 'select2' */ 'select2/select2')
.then(groupsSelect)
.catch(() => {});
}

View File

@ -120,7 +120,7 @@ export default class LabelsSelect {
labelCount = 0;
if (data.labels.length && issueUpdateURL) {
template = LabelsSelect.getLabelTemplate({
labels: data.labels,
labels: _.sortBy(data.labels, 'title'),
issueUpdateURL,
enableScopedLabels: scopedLabels,
scopedLabelsDocumentationLink,

View File

@ -1,6 +1,8 @@
# frozen_string_literal: true
class Import::BitbucketController < Import::BaseController
include ActionView::Helpers::SanitizeHelper
before_action :verify_bitbucket_import_enabled
before_action :bitbucket_auth, except: :callback
@ -21,7 +23,7 @@ class Import::BitbucketController < Import::BaseController
# rubocop: disable CodeReuse/ActiveRecord
def status
bitbucket_client = Bitbucket::Client.new(credentials)
repos = bitbucket_client.repos
repos = bitbucket_client.repos(filter: sanitized_filter_param)
@repos, @incompatible_repos = repos.partition { |repo| repo.valid? }
@ -104,4 +106,8 @@ class Import::BitbucketController < Import::BaseController
refresh_token: session[:bitbucket_refresh_token]
}
end
def sanitized_filter_param
@filter ||= sanitize(params[:filter])
end
end

View File

@ -9,6 +9,7 @@ module Groups
def execute
remove_unallowed_params
set_visibility_level
@group = Group.new(params)
@ -68,6 +69,12 @@ module Groups
true
end
def set_visibility_level
return if visibility_level.present?
params[:visibility_level] = Gitlab::CurrentSettings.current_application_settings.default_group_visibility
end
end
end

View File

@ -8,7 +8,6 @@
- if @repos.any?
%p.light
= _('Select projects you want to import.')
%hr
%p
- if @incompatible_repos.any?
= button_tag class: 'btn btn-import btn-success js-import-all' do
@ -19,6 +18,14 @@
= _('Import all projects')
= icon('spinner spin', class: 'loading-icon')
.position-relative.ms-no-clear.d-flex.flex-fill.float-right.append-bottom-10
= form_tag status_import_bitbucket_path, method: 'get' do
= text_field_tag :filter, @filter, class: 'form-control pr-5', placeholder: _('Filter projects'), size: 40, autofocus: true, 'aria-label': _('Search')
.position-absolute.position-top-0.d-flex.align-items-center.text-muted.position-right-0.h-100
.border-left
%button{ class: 'btn btn-transparent btn-secondary', 'aria-label': _('Search Button'), type: 'submit' }
%i{ class: 'fa fa-search', 'aria-hidden': true }
.table-responsive
%table.table.import-jobs
%colgroup.import-jobs-from-col
@ -59,7 +66,7 @@
- if current_user.can_select_namespace?
- selected = params[:namespace_id] || :current_user
- opts = current_user.can_create_group? ? { extra_group: Group.new(name: repo.owner, path: repo.owner) } : {}
= select_tag :namespace_id, namespaces_options(selected, opts.merge({ display_path: true })), { class: 'input-group-text select2 js-select-namespace', tabindex: 1 }
= select_tag :namespace_id, namespaces_options(selected, opts.merge({ display_path: true })), { class: 'select2 js-select-namespace', tabindex: 1 }
- else
= text_field_tag :path, current_user.namespace_path, class: "input-group-text input-large form-control", tabindex: 1, disabled: true
%span.input-group-prepend

View File

@ -62,7 +62,7 @@
- if current_user.can_select_namespace?
- selected = params[:namespace_id] || :extra_group
- opts = current_user.can_create_group? ? { extra_group: Group.new(name: sanitize_project_name(repo.project_key), path: sanitize_project_name(repo.project_key)) } : {}
= select_tag :namespace_id, namespaces_options(selected, opts.merge({ display_path: true })), { class: 'input-group-text select2 js-select-namespace', tabindex: 1 }
= select_tag :namespace_id, namespaces_options(selected, opts.merge({ display_path: true })), { class: 'select2 js-select-namespace', tabindex: 1 }
- else
= text_field_tag :path, current_user.namespace_path, class: "input-group-text input-large form-control", tabindex: 1, disabled: true
%span.input-group-prepend

View File

@ -0,0 +1,5 @@
---
title: Alphabetically sorts selected sidebar labels.
merge_request: 17309
author:
type: fixed

View File

@ -0,0 +1,5 @@
---
title: 'Allow to exclude ancestor groups on group labels API'
merge_request: 17221
author: Mathieu Parent
type: added

View File

@ -0,0 +1,5 @@
---
title: Add project filtering to Bitbucket Cloud import
merge_request: 16828
author:
type: added

View File

@ -0,0 +1,5 @@
---
title: Fix visibility level error when updating group from API
merge_request: 17227
author: Mathieu Parent
type: fixed

View File

@ -0,0 +1,5 @@
---
title: Remove Postgresql specific setup tasks and move to schema.rb
merge_request:
author:
type: other

View File

@ -20,9 +20,7 @@ class ReworkRedirectRoutesIndexes < ActiveRecord::Migration[4.2]
def up
disable_statement_timeout do
# this is a plain btree on a single boolean column. It'll never be
# selective enough to be valuable. This class is called by
# setup_postgresql.rake so it needs to be able to handle this
# index not existing.
# selective enough to be valuable.
if index_exists?(:redirect_routes, :permanent)
remove_concurrent_index(:redirect_routes, :permanent)
end

View File

@ -14,9 +14,7 @@ class AddPathIndexToRedirectRoutes < ActiveRecord::Migration[4.2]
# RedirectRoute.matching_path_and_descendants
#
# This same index is also added in the `ReworkRedirectRoutesIndexes` so this
# is a no-op in most cases. But this migration is also called from the
# `setup_postgresql.rake` task when setting up a new database, in which case
# we want to create the index.
# is a no-op in most cases.
def up
return unless Gitlab::Database.postgresql?
@ -31,8 +29,5 @@ class AddPathIndexToRedirectRoutes < ActiveRecord::Migration[4.2]
# Do nothing in the DOWN. Since the index above is originally created in the
# `ReworkRedirectRoutesIndexes`. This migration wouldn't have actually
# created any new index.
#
# This migration is only here to be called form `setup_postgresql.rake` so
# any newly created database would have this index.
end
end

View File

@ -2917,6 +2917,7 @@ ActiveRecord::Schema.define(version: 2019_09_26_041216) do
t.boolean "emails_disabled"
t.integer "max_pages_size"
t.integer "max_artifacts_size"
t.index "lower((name)::text)", name: "index_projects_on_lower_name"
t.index ["archived", "pending_delete", "merge_requests_require_code_owner_approval"], name: "projects_requiring_code_owner_approval", where: "((pending_delete = false) AND (archived = false) AND (merge_requests_require_code_owner_approval = true))"
t.index ["created_at"], name: "index_projects_on_created_at"
t.index ["creator_id"], name: "index_projects_on_creator_id"
@ -3113,6 +3114,7 @@ ActiveRecord::Schema.define(version: 2019_09_26_041216) do
t.string "path", null: false
t.datetime "created_at", null: false
t.datetime "updated_at", null: false
t.index "lower((path)::text) varchar_pattern_ops", name: "index_redirect_routes_on_path_unique_text_pattern_ops", unique: true
t.index ["path"], name: "index_redirect_routes_on_path", unique: true
t.index ["source_type", "source_id"], name: "index_redirect_routes_on_source_type_and_source_id"
end
@ -3641,6 +3643,7 @@ ActiveRecord::Schema.define(version: 2019_09_26_041216) do
t.string "first_name", limit: 255
t.string "last_name", limit: 255
t.string "static_object_token", limit: 255
t.index "lower((name)::text)", name: "index_on_users_name_lower"
t.index ["accepted_term_id"], name: "index_users_on_accepted_term_id"
t.index ["admin"], name: "index_users_on_admin"
t.index ["bot_type"], name: "index_users_on_bot_type"

View File

@ -16,6 +16,7 @@ GET /groups/:id/labels
| --------- | ---- | -------- | ----------- |
| `id` | integer/string | yes | The ID or [URL-encoded path of the group](README.md#namespaced-path-encoding) owned by the authenticated user. |
| `with_counts` | boolean | no | Whether or not to include issue and merge request counts. Defaults to `false`. _([Introduced in GitLab 12.2](https://gitlab.com/gitlab-org/gitlab-foss/merge_requests/31543))_ |
| `include_ancestor_groups` | boolean | no | Include ancestor groups. Defaults to `true`. |
```bash
curl --header "PRIVATE-TOKEN: <your_access_token>" https://gitlab.example.com/api/v4/groups/5/labels?with_counts=true

View File

@ -12,6 +12,7 @@ GET /projects/:id/labels
| --------- | ------- | -------- | --------------------- |
| `id` | integer/string | yes | The ID or [URL-encoded path of the project](README.md#namespaced-path-encoding) owned by the authenticated user |
| `with_counts` | boolean | no | Whether or not to include issue and merge request counts. Defaults to `false`. _([Introduced in GitLab 12.2](https://gitlab.com/gitlab-org/gitlab-foss/merge_requests/31543))_ |
| `include_ancestor_groups` | boolean | no | Include ancestor groups. Defaults to `true`. |
```bash
curl --header "PRIVATE-TOKEN: <your_access_token>" https://gitlab.example.com/api/v4/projects/1/labels?with_counts=true

View File

@ -56,10 +56,10 @@ namespace that started the import process.
![Grant access](img/bitbucket_import_grant_access.png)
1. Click on the projects that you'd like to import or **Import all projects**.
You can also select the namespace under which each project will be
imported.
You can also filter projects by name and select the namespace under which
each project will be imported.
![Import projects](img/bitbucket_import_select_project.png)
![Import projects](img/bitbucket_import_select_project_v12_3.png)
[bb-import]: ../../../integration/bitbucket.md
[social sign-in]: ../../profile/account/social_sign_in.md

Binary file not shown.

Before

Width:  |  Height:  |  Size: 8.5 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 110 KiB

View File

@ -18,10 +18,12 @@ module API
params do
optional :with_counts, type: Boolean, default: false,
desc: 'Include issue and merge request counts'
optional :include_ancestor_groups, type: Boolean, default: true,
desc: 'Include ancestor groups'
use :pagination
end
get ':id/labels' do
get_labels(user_group, Entities::GroupLabel)
get_labels(user_group, Entities::GroupLabel, include_ancestor_groups: params[:include_ancestor_groups])
end
desc 'Create a new label' do

View File

@ -10,8 +10,6 @@ module API
optional :description, type: String, desc: 'The description of the group'
optional :visibility, type: String,
values: Gitlab::VisibilityLevel.string_values,
default: Gitlab::VisibilityLevel.string_level(
Gitlab::CurrentSettings.current_application_settings.default_group_visibility),
desc: 'The visibility of the group'
optional :lfs_enabled, type: Boolean, desc: 'Enable/disable LFS for the projects in this group'
optional :request_access_enabled, type: Boolean, desc: 'Allow users to request member access'

View File

@ -18,8 +18,8 @@ module API
label || not_found!('Label')
end
def get_labels(parent, entity)
present paginate(available_labels_for(parent)),
def get_labels(parent, entity, include_ancestor_groups: true)
present paginate(available_labels_for(parent, include_ancestor_groups: include_ancestor_groups)),
with: entity,
current_user: current_user,
parent: parent,

View File

@ -17,10 +17,12 @@ module API
params do
optional :with_counts, type: Boolean, default: false,
desc: 'Include issue and merge request counts'
optional :include_ancestor_groups, type: Boolean, default: true,
desc: 'Include ancestor groups'
use :pagination
end
get ':id/labels' do
get_labels(user_project, Entities::ProjectLabel)
get_labels(user_project, Entities::ProjectLabel, include_ancestor_groups: params[:include_ancestor_groups])
end
desc 'Create a new label' do

View File

@ -38,8 +38,10 @@ module Bitbucket
Representation::Repo.new(parsed_response)
end
def repos
def repos(filter: nil)
path = "/repositories?role=member"
path += "&q=name~\"#{filter}\"" if filter
get_collection(path, :repo)
end

View File

@ -31,7 +31,6 @@ namespace :gitlab do
terminate_all_connections unless Rails.env.production?
Rake::Task["db:reset"].invoke
Rake::Task["setup_postgresql"].invoke
Rake::Task["db:seed_fu"].invoke
rescue Gitlab::TaskAbortedByUserError
puts "Quitting...".color(:red)

View File

@ -1,14 +1,3 @@
desc 'GitLab | Sets up PostgreSQL'
task setup_postgresql: :environment do
require Rails.root.join('db/migrate/20180215181245_users_name_lower_index.rb')
require Rails.root.join('db/migrate/20180504195842_project_name_lower_index.rb')
require Rails.root.join('db/post_migrate/20180306164012_add_path_index_to_redirect_routes.rb')
UsersNameLowerIndex.new.up
ProjectNameLowerIndex.new.up
AddPathIndexToRedirectRoutes.new.up
end
desc 'GitLab | Generate PostgreSQL Password Hash'
task :postgresql_md5_hash do
require 'digest'

View File

@ -6795,6 +6795,9 @@ msgstr ""
msgid "Filter by two-factor authentication"
msgstr ""
msgid "Filter projects"
msgstr ""
msgid "Filter results by group"
msgstr ""
@ -13602,6 +13605,9 @@ msgstr ""
msgid "Search"
msgstr ""
msgid "Search Button"
msgstr ""
msgid "Search an environment spec"
msgstr ""

View File

@ -26,8 +26,16 @@ module QA::Page
end
# Reminder: You may wish to wait for a particular job status before checking output
def output
find_element(:build_trace).text
def output(wait: 5)
result = ''
wait(reload: false, max: wait, interval: 1) do
result = find_element(:build_trace).text
!result.empty?
end
result
end
private

View File

@ -80,6 +80,21 @@ describe Import::BitbucketController do
expect(assigns(:already_added_projects)).to eq([@project])
expect(assigns(:repos)).to eq([])
end
context 'when filtering' do
let(:filter) { '<html>test</html>' }
let(:expected_filter) { 'test' }
subject { get :status, params: { filter: filter }, as: :json }
it 'passes sanitized filter param to bitbucket client' do
expect_next_instance_of(Bitbucket::Client) do |client|
expect(client).to receive(:repos).with(filter: expected_filter).and_return([@repo])
end
subject
end
end
end
describe "POST create" do

View File

@ -5,6 +5,7 @@ import MockAdapter from 'axios-mock-adapter';
import axios from '~/lib/utils/axios_utils';
import IssuableContext from '~/issuable_context';
import LabelsSelect from '~/labels_select';
import _ from 'underscore';
import '~/gl_dropdown';
import 'select2';
@ -15,6 +16,35 @@ import '~/users_select';
let saveLabelCount = 0;
let mock;
function testLabelClicks(labelOrder, done) {
$('.edit-link')
.get(0)
.click();
setTimeout(() => {
const labelsInDropdown = $('.dropdown-content a');
expect(labelsInDropdown.length).toBe(10);
const arrayOfLabels = labelsInDropdown.get();
const randomArrayOfLabels = _.shuffle(arrayOfLabels);
randomArrayOfLabels.forEach((label, i) => {
if (i < saveLabelCount) {
$(label).click();
}
});
$('.edit-link')
.get(0)
.click();
setTimeout(() => {
expect($('.sidebar-collapsed-icon').attr('data-original-title')).toBe(labelOrder);
done();
}, 0);
}, 0);
}
describe('Issue dropdown sidebar', () => {
preloadFixtures('static/issue_sidebar_label.html');
@ -29,7 +59,7 @@ describe('Issue dropdown sidebar', () => {
mock.onGet('/root/test/labels.json').reply(() => {
const labels = Array(10)
.fill()
.map((_, i) => ({
.map((_val, i) => ({
id: i,
title: `test ${i}`,
color: '#5CB85C',
@ -41,7 +71,7 @@ describe('Issue dropdown sidebar', () => {
mock.onPut('/root/test/issues/2.json').reply(() => {
const labels = Array(saveLabelCount)
.fill()
.map((_, i) => ({
.map((_val, i) => ({
id: i,
title: `test ${i}`,
color: '#5CB85C',
@ -57,61 +87,11 @@ describe('Issue dropdown sidebar', () => {
it('changes collapsed tooltip when changing labels when less than 5', done => {
saveLabelCount = 5;
$('.edit-link')
.get(0)
.click();
setTimeout(() => {
expect($('.dropdown-content a').length).toBe(10);
$('.dropdown-content a').each(function(i) {
if (i < saveLabelCount) {
$(this)
.get(0)
.click();
}
});
$('.edit-link')
.get(0)
.click();
setTimeout(() => {
expect($('.sidebar-collapsed-icon').attr('data-original-title')).toBe(
'test 0, test 1, test 2, test 3, test 4',
);
done();
}, 0);
}, 0);
testLabelClicks('test 0, test 1, test 2, test 3, test 4', done);
});
it('changes collapsed tooltip when changing labels when more than 5', done => {
saveLabelCount = 6;
$('.edit-link')
.get(0)
.click();
setTimeout(() => {
expect($('.dropdown-content a').length).toBe(10);
$('.dropdown-content a').each(function(i) {
if (i < saveLabelCount) {
$(this)
.get(0)
.click();
}
});
$('.edit-link')
.get(0)
.click();
setTimeout(() => {
expect($('.sidebar-collapsed-icon').attr('data-original-title')).toBe(
'test 0, test 1, test 2, test 3, test 4, and 1 more',
);
done();
}, 0);
}, 0);
testLabelClicks('test 0, test 1, test 2, test 3, test 4, and 1 more', done);
});
});

View File

@ -5,9 +5,11 @@ require 'spec_helper'
describe API::GroupLabels do
let(:user) { create(:user) }
let(:group) { create(:group) }
let(:subgroup) { create(:group, parent: group) }
let!(:group_member) { create(:group_member, group: group, user: user) }
let!(:label1) { create(:group_label, title: 'feature', group: group) }
let!(:label2) { create(:group_label, title: 'bug', group: group) }
let!(:group_label1) { create(:group_label, title: 'feature', group: group) }
let!(:group_label2) { create(:group_label, title: 'bug', group: group) }
let!(:subgroup_label) { create(:group_label, title: 'support', group: subgroup) }
describe 'GET :id/labels' do
it 'returns all available labels for the group' do
@ -35,6 +37,34 @@ describe API::GroupLabels do
end
end
describe 'GET :subgroup_id/labels' do
context 'when the include_ancestor_groups parameter is not set' do
it 'returns all available labels for the group and ancestor groups' do
get api("/groups/#{subgroup.id}/labels", user)
expect(response).to have_gitlab_http_status(200)
expect(response).to include_pagination_headers
expect(json_response).to be_an Array
expect(json_response).to all(match_schema('public_api/v4/labels/label'))
expect(json_response.size).to eq(3)
expect(json_response.map {|r| r['name'] }).to contain_exactly('feature', 'bug', 'support')
end
end
context 'when the include_ancestor_groups parameter is set to false' do
it 'returns all available labels for the group but not for ancestor groups' do
get api("/groups/#{subgroup.id}/labels", user), params: { include_ancestor_groups: false }
expect(response).to have_gitlab_http_status(200)
expect(response).to include_pagination_headers
expect(json_response).to be_an Array
expect(json_response).to all(match_schema('public_api/v4/labels/label'))
expect(json_response.size).to eq(1)
expect(json_response.map {|r| r['name'] }).to contain_exactly('support')
end
end
end
describe 'POST /groups/:id/labels' do
it 'returns created label when all params are given' do
post api("/groups/#{group.id}/labels", user),
@ -78,7 +108,7 @@ describe API::GroupLabels do
it 'returns 409 if label already exists' do
post api("/groups/#{group.id}/labels", user),
params: {
name: label1.name,
name: group_label1.name,
color: '#FFAABB'
}
@ -89,13 +119,13 @@ describe API::GroupLabels do
describe 'DELETE /groups/:id/labels' do
it 'returns 204 for existing label' do
delete api("/groups/#{group.id}/labels", user), params: { name: label1.name }
delete api("/groups/#{group.id}/labels", user), params: { name: group_label1.name }
expect(response).to have_gitlab_http_status(204)
end
it 'returns 404 for non existing label' do
delete api("/groups/#{group.id}/labels", user), params: { name: 'label2' }
delete api("/groups/#{group.id}/labels", user), params: { name: 'not_exists' }
expect(response).to have_gitlab_http_status(404)
expect(json_response['message']).to eq('404 Label Not Found')
@ -115,12 +145,12 @@ describe API::GroupLabels do
expect(response).to have_gitlab_http_status(204)
expect(subgroup.labels.size).to eq(0)
expect(group.labels).to include(label1)
expect(group.labels).to include(group_label1)
end
it_behaves_like '412 response' do
let(:request) { api("/groups/#{group.id}/labels", user) }
let(:params) { { name: label1.name } }
let(:params) { { name: group_label1.name } }
end
end
@ -128,7 +158,7 @@ describe API::GroupLabels do
it 'returns 200 if name and colors and description are changed' do
put api("/groups/#{group.id}/labels", user),
params: {
name: label1.name,
name: group_label1.name,
new_name: 'New Label',
color: '#FFFFFF',
description: 'test'
@ -152,13 +182,13 @@ describe API::GroupLabels do
expect(response).to have_gitlab_http_status(200)
expect(subgroup.labels[0].name).to eq('New Label')
expect(label1.name).to eq('feature')
expect(group_label1.name).to eq('feature')
end
it 'returns 404 if label does not exist' do
put api("/groups/#{group.id}/labels", user),
params: {
name: 'label2',
name: 'not_exists',
new_name: 'label3'
}
@ -166,14 +196,14 @@ describe API::GroupLabels do
end
it 'returns 400 if no label name given' do
put api("/groups/#{group.id}/labels", user), params: { new_name: label1.name }
put api("/groups/#{group.id}/labels", user), params: { new_name: group_label1.name }
expect(response).to have_gitlab_http_status(400)
expect(json_response['error']).to eq('name is missing')
end
it 'returns 400 if no new parameters given' do
put api("/groups/#{group.id}/labels", user), params: { name: label1.name }
put api("/groups/#{group.id}/labels", user), params: { name: group_label1.name }
expect(response).to have_gitlab_http_status(400)
expect(json_response['error']).to eq('new_name, color, description are missing, '\
@ -184,31 +214,31 @@ describe API::GroupLabels do
describe 'POST /groups/:id/labels/:label_id/subscribe' do
context 'when label_id is a label title' do
it 'subscribes to the label' do
post api("/groups/#{group.id}/labels/#{label1.title}/subscribe", user)
post api("/groups/#{group.id}/labels/#{group_label1.title}/subscribe", user)
expect(response).to have_gitlab_http_status(201)
expect(json_response['name']).to eq(label1.title)
expect(json_response['name']).to eq(group_label1.title)
expect(json_response['subscribed']).to be_truthy
end
end
context 'when label_id is a label ID' do
it 'subscribes to the label' do
post api("/groups/#{group.id}/labels/#{label1.id}/subscribe", user)
post api("/groups/#{group.id}/labels/#{group_label1.id}/subscribe", user)
expect(response).to have_gitlab_http_status(201)
expect(json_response['name']).to eq(label1.title)
expect(json_response['name']).to eq(group_label1.title)
expect(json_response['subscribed']).to be_truthy
end
end
context 'when user is already subscribed to label' do
before do
label1.subscribe(user)
group_label1.subscribe(user)
end
it 'returns 304' do
post api("/groups/#{group.id}/labels/#{label1.id}/subscribe", user)
post api("/groups/#{group.id}/labels/#{group_label1.id}/subscribe", user)
expect(response).to have_gitlab_http_status(304)
end
@ -225,36 +255,36 @@ describe API::GroupLabels do
describe 'POST /groups/:id/labels/:label_id/unsubscribe' do
before do
label1.subscribe(user)
group_label1.subscribe(user)
end
context 'when label_id is a label title' do
it 'unsubscribes from the label' do
post api("/groups/#{group.id}/labels/#{label1.title}/unsubscribe", user)
post api("/groups/#{group.id}/labels/#{group_label1.title}/unsubscribe", user)
expect(response).to have_gitlab_http_status(201)
expect(json_response['name']).to eq(label1.title)
expect(json_response['name']).to eq(group_label1.title)
expect(json_response['subscribed']).to be_falsey
end
end
context 'when label_id is a label ID' do
it 'unsubscribes from the label' do
post api("/groups/#{group.id}/labels/#{label1.id}/unsubscribe", user)
post api("/groups/#{group.id}/labels/#{group_label1.id}/unsubscribe", user)
expect(response).to have_gitlab_http_status(201)
expect(json_response['name']).to eq(label1.title)
expect(json_response['name']).to eq(group_label1.title)
expect(json_response['subscribed']).to be_falsey
end
end
context 'when user is already unsubscribed from label' do
before do
label1.unsubscribe(user)
group_label1.unsubscribe(user)
end
it 'returns 304' do
post api("/groups/#{group.id}/labels/#{label1.id}/unsubscribe", user)
post api("/groups/#{group.id}/labels/#{group_label1.id}/unsubscribe", user)
expect(response).to have_gitlab_http_status(304)
end

View File

@ -497,6 +497,29 @@ describe API::Groups do
expect(response).to have_gitlab_http_status(404)
end
context 'within a subgroup' do
let(:group3) { create(:group, visibility_level: Gitlab::VisibilityLevel::PUBLIC) }
let!(:subgroup) { create(:group, parent: group3, visibility_level: Gitlab::VisibilityLevel::PUBLIC) }
before do
group3.add_owner(user3)
end
it 'does not change visibility when not requested' do
put api("/groups/#{group3.id}", user3), params: { description: 'Bug #23083' }
expect(response).to have_gitlab_http_status(200)
expect(json_response['visibility']).to eq('public')
end
it 'prevents making private a group containing public subgroups' do
put api("/groups/#{group3.id}", user3), params: { visibility: 'private' }
expect(response).to have_gitlab_http_status(400)
expect(json_response['message']['visibility_level']).to contain_exactly('private is not allowed since there are sub-groups with higher visibility.')
end
end
end
context 'when authenticated as the admin' do

View File

@ -256,6 +256,52 @@ describe API::Labels do
'is_project_label' => true)
end
end
context 'when the include_ancestor_groups parameter is not set' do
let(:group) { create(:group) }
let!(:group_label) { create(:group_label, title: 'feature', group: group) }
let(:subgroup) { create(:group, parent: group) }
let!(:subgroup_label) { create(:group_label, title: 'support', group: subgroup) }
before do
subgroup.add_owner(user)
project.update!(group: subgroup)
end
it 'returns all available labels for the project, parent group and ancestor groups' do
get api("/projects/#{project.id}/labels", user)
expect(response).to have_gitlab_http_status(200)
expect(response).to include_pagination_headers
expect(json_response).to be_an Array
expect(json_response).to all(match_schema('public_api/v4/labels/label'))
expect(json_response.size).to eq(4)
expect(json_response.map {|r| r['name'] }).to contain_exactly(group_label.name, subgroup_label.name, priority_label.name, label1.name)
end
end
context 'when the include_ancestor_groups parameter is set to false' do
let(:group) { create(:group) }
let!(:group_label) { create(:group_label, title: 'feature', group: group) }
let(:subgroup) { create(:group, parent: group) }
let!(:subgroup_label) { create(:group_label, title: 'support', group: subgroup) }
before do
subgroup.add_owner(user)
project.update!(group: subgroup)
end
it 'returns all available labels for the project and the parent group only' do
get api("/projects/#{project.id}/labels", user), params: { include_ancestor_groups: false }
expect(response).to have_gitlab_http_status(200)
expect(response).to include_pagination_headers
expect(json_response).to be_an Array
expect(json_response).to all(match_schema('public_api/v4/labels/label'))
expect(json_response.size).to eq(3)
expect(json_response.map {|r| r['name'] }).to contain_exactly(subgroup_label.name, priority_label.name, label1.name)
end
end
end
describe 'POST /projects/:id/labels' do