Commit be3578d2 authored by Alexandru Croitor's avatar Alexandru Croitor

Add array support for labels

* Support label parameter as comma separated and array of strings
for merge requests and issues api endpoints
parent ee8cb2d1
......@@ -89,7 +89,7 @@ def find_or_create_label_ids
return unless labels
params[:label_ids] = labels.split(",").map do |label_name|
params[:label_ids] = labels.map do |label_name|
label = Labels::FindOrCreateService.new(
current_user,
parent,
......
......@@ -34,7 +34,7 @@ def find_issues(args = {})
# rubocop: enable CodeReuse/ActiveRecord
params :issues_params do
optional :labels, type: String, desc: 'Comma-separated list of label names'
optional :labels, type: Array[String], coerce_with: Validations::Types::LabelsList.coerce, desc: 'Comma-separated list of label names'
optional :milestone, type: String, desc: 'Milestone title'
optional :order_by, type: String, values: %w[created_at updated_at], default: 'created_at',
desc: 'Return issues ordered by `created_at` or `updated_at` fields.'
......@@ -65,7 +65,7 @@ def find_issues(args = {})
optional :assignee_ids, type: Array[Integer], desc: 'The array of user IDs to assign issue'
optional :assignee_id, type: Integer, desc: '[Deprecated] The ID of a user to assign issue'
optional :milestone_id, type: Integer, desc: 'The ID of a milestone to assign issue'
optional :labels, type: String, desc: 'Comma-separated list of label names'
optional :labels, type: Array[String], coerce_with: Validations::Types::LabelsList.coerce, desc: 'Comma-separated list of label names'
optional :due_date, type: String, desc: 'Date string in the format YEAR-MONTH-DAY'
optional :confidential, type: Boolean, desc: 'Boolean parameter if the issue should be confidential'
optional :discussion_locked, type: Boolean, desc: " Boolean parameter indicating if the issue's discussion is locked"
......
......@@ -95,7 +95,7 @@ def authorize_push_to_merge_request!(merge_request)
optional :sort, type: String, values: %w[asc desc], default: 'desc',
desc: 'Return merge requests sorted in `asc` or `desc` order.'
optional :milestone, type: String, desc: 'Return merge requests for a specific milestone'
optional :labels, type: String, desc: 'Comma-separated list of label names'
optional :labels, type: Array[String], coerce_with: Validations::Types::LabelsList.coerce, desc: 'Comma-separated list of label names'
optional :created_after, type: DateTime, desc: 'Return merge requests created after the specified time'
optional :created_before, type: DateTime, desc: 'Return merge requests created before the specified time'
optional :updated_after, type: DateTime, desc: 'Return merge requests updated after the specified time'
......@@ -179,7 +179,7 @@ def handle_merge_request_errors!(errors)
optional :description, type: String, desc: 'The description of the merge request'
optional :assignee_id, type: Integer, desc: 'The ID of a user to assign the merge request'
optional :milestone_id, type: Integer, desc: 'The ID of a milestone to assign the merge request'
optional :labels, type: String, coerce_with: ->(val) { val.is_a?(Array) ? val.join(', ') : val }, desc: 'Comma-separated list of label names'
optional :labels, type: Array[String], coerce_with: Validations::Types::LabelsList.coerce, desc: 'Comma-separated list of label names'
optional :remove_source_branch, type: Boolean, desc: 'Remove source branch when merging'
optional :allow_collaboration, type: Boolean, desc: 'Allow commits from members who can merge to the target branch'
optional :allow_maintainer_to_push, type: Boolean, as: :allow_collaboration, desc: '[deprecated] See allow_collaboration'
......
# frozen_string_literal: true
module API
module Validations
module Types
class LabelsList
def self.coerce
lambda do |value|
case value
when String
value.split(',').map(&:strip)
when Array
value.map { |v| v.to_s.split(',').map(&:strip) }.flatten
when LabelsList
value
else
[]
end
end
end
end
end
end
end
......@@ -271,7 +271,14 @@
end
it 'returns an array of labeled issues' do
get api("/issues", user), params: { labels: label.title }
get api('/issues', user), params: { labels: label.title }
expect_paginated_array_response(issue.id)
expect(json_response.first['labels']).to eq([label.title])
end
it 'returns an array of labeled issues with labels param as array' do
get api('/issues', user), params: { labels: [label.title] }
expect_paginated_array_response(issue.id)
expect(json_response.first['labels']).to eq([label.title])
......@@ -284,7 +291,20 @@
create(:label_link, label: label_b, target: issue)
create(:label_link, label: label_c, target: issue)
get api("/issues", user), params: { labels: "#{label.title},#{label_b.title},#{label_c.title}" }
get api('/issues', user), params: { labels: "#{label.title},#{label_b.title},#{label_c.title}" }
expect_paginated_array_response(issue.id)
expect(json_response.first['labels']).to eq([label_c.title, label_b.title, label.title])
end
it 'returns an array of labeled issues when all labels matches with labels param as array' do
label_b = create(:label, title: 'foo', project: project)
label_c = create(:label, title: 'bar', project: project)
create(:label_link, label: label_b, target: issue)
create(:label_link, label: label_c, target: issue)
get api('/issues', user), params: { labels: [label.title, label_b.title, label_c.title] }
expect_paginated_array_response(issue.id)
expect(json_response.first['labels']).to eq([label_c.title, label_b.title, label.title])
......@@ -296,8 +316,22 @@
expect_paginated_array_response([])
end
it 'returns an empty array if no issue matches labels with labels param as array' do
get api('/issues', user), params: { labels: %w(foo bar) }
expect_paginated_array_response([])
end
it 'returns an array of labeled issues matching given state' do
get api("/issues", user), params: { labels: label.title, state: :opened }
get api('/issues', user), params: { labels: label.title, state: :opened }
expect_paginated_array_response(issue.id)
expect(json_response.first['labels']).to eq([label.title])
expect(json_response.first['state']).to eq('opened')
end
it 'returns an array of labeled issues matching given state with labels param as array' do
get api('/issues', user), params: { labels: [label.title], state: :opened }
expect_paginated_array_response(issue.id)
expect(json_response.first['labels']).to eq([label.title])
......@@ -305,25 +339,43 @@
end
it 'returns an empty array if no issue matches labels and state filters' do
get api("/issues", user), params: { labels: label.title, state: :closed }
get api('/issues', user), params: { labels: label.title, state: :closed }
expect_paginated_array_response([])
end
it 'returns an array of issues with any label' do
get api("/issues", user), params: { labels: IssuesFinder::FILTER_ANY }
get api('/issues', user), params: { labels: IssuesFinder::FILTER_ANY }
expect_paginated_array_response(issue.id)
end
it 'returns an array of issues with any label with labels param as array' do
get api('/issues', user), params: { labels: [IssuesFinder::FILTER_ANY] }
expect_paginated_array_response(issue.id)
end
it 'returns an array of issues with no label' do
get api("/issues", user), params: { labels: IssuesFinder::FILTER_NONE }
get api('/issues', user), params: { labels: IssuesFinder::FILTER_NONE }
expect_paginated_array_response(closed_issue.id)
end
it 'returns an array of issues with no label with labels param as array' do
get api('/issues', user), params: { labels: [IssuesFinder::FILTER_NONE] }
expect_paginated_array_response(closed_issue.id)
end
it 'returns an array of issues with no label when using the legacy No+Label filter' do
get api("/issues", user), params: { labels: "No Label" }
get api('/issues', user), params: { labels: 'No Label' }
expect_paginated_array_response(closed_issue.id)
end
it 'returns an array of issues with no label when using the legacy No+Label filter with labels param as array' do
get api('/issues', user), params: { labels: ['No Label'] }
expect_paginated_array_response(closed_issue.id)
end
......@@ -588,12 +640,25 @@
expect(json_response.first['labels']).to eq([group_label.title])
end
it 'returns an array of labeled group issues with labels param as array' do
get api(base_url, user), params: { labels: [group_label.title] }
expect_paginated_array_response(group_issue.id)
expect(json_response.first['labels']).to eq([group_label.title])
end
it 'returns an array of labeled group issues where all labels match' do
get api(base_url, user), params: { labels: "#{group_label.title},foo,bar" }
expect_paginated_array_response([])
end
it 'returns an array of labeled group issues where all labels match with labels param as array' do
get api(base_url, user), params: { labels: [group_label.title, 'foo', 'bar'] }
expect_paginated_array_response([])
end
it 'returns issues matching given search string for title' do
get api(base_url, user), params: { search: group_issue.title }
......@@ -619,6 +684,19 @@
expect(json_response.first['labels']).to eq([label_c.title, label_b.title, group_label.title])
end
it 'returns an array of labeled issues when all labels matches with labels param as array' do
label_b = create(:label, title: 'foo', project: group_project)
label_c = create(:label, title: 'bar', project: group_project)
create(:label_link, label: label_b, target: group_issue)
create(:label_link, label: label_c, target: group_issue)
get api(base_url, user), params: { labels: [group_label.title, label_b.title, label_c.title] }
expect_paginated_array_response(group_issue.id)
expect(json_response.first['labels']).to eq([label_c.title, label_b.title, group_label.title])
end
it 'returns an array of issues found by iids' do
get api(base_url, user), params: { iids: [group_issue.iid] }
......@@ -645,12 +723,25 @@
expect(json_response.first['id']).to eq(group_issue.id)
end
it 'returns an array of group issues with any label with labels param as array' do
get api(base_url, user), params: { labels: [IssuesFinder::FILTER_ANY] }
expect_paginated_array_response(group_issue.id)
expect(json_response.first['id']).to eq(group_issue.id)
end
it 'returns an array of group issues with no label' do
get api(base_url, user), params: { labels: IssuesFinder::FILTER_NONE }
expect_paginated_array_response([group_closed_issue.id, group_confidential_issue.id])
end
it 'returns an array of group issues with no label with labels param as array' do
get api(base_url, user), params: { labels: [IssuesFinder::FILTER_NONE] }
expect_paginated_array_response([group_closed_issue.id, group_confidential_issue.id])
end
it 'returns an empty array if no issue matches milestone' do
get api(base_url, user), params: { milestone: group_empty_milestone.title }
......@@ -842,6 +933,12 @@
expect_paginated_array_response(issue.id)
end
it 'returns an array of labeled project issues with labels param as array' do
get api("#{base_url}/issues", user), params: { labels: [label.title] }
expect_paginated_array_response(issue.id)
end
it 'returns an array of labeled issues when all labels matches' do
label_b = create(:label, title: 'foo', project: project)
label_c = create(:label, title: 'bar', project: project)
......@@ -854,6 +951,18 @@
expect_paginated_array_response(issue.id)
end
it 'returns an array of labeled issues when all labels matches with labels param as array' do
label_b = create(:label, title: 'foo', project: project)
label_c = create(:label, title: 'bar', project: project)
create(:label_link, label: label_b, target: issue)
create(:label_link, label: label_c, target: issue)
get api("#{base_url}/issues", user), params: { labels: [label.title, label_b.title, label_c.title] }
expect_paginated_array_response(issue.id)
end
it 'returns issues matching given search string for title' do
get api("#{base_url}/issues?search=#{issue.title}", user)
......@@ -890,12 +999,24 @@
expect_paginated_array_response(issue.id)
end
it 'returns an array of project issues with any label with labels param as array' do
get api("#{base_url}/issues", user), params: { labels: [IssuesFinder::FILTER_ANY] }
expect_paginated_array_response(issue.id)
end
it 'returns an array of project issues with no label' do
get api("#{base_url}/issues", user), params: { labels: IssuesFinder::FILTER_NONE }
expect_paginated_array_response([confidential_issue.id, closed_issue.id])
end
it 'returns an array of project issues with no label with labels param as array' do
get api("#{base_url}/issues", user), params: { labels: [IssuesFinder::FILTER_NONE] }
expect_paginated_array_response([confidential_issue.id, closed_issue.id])
end
it 'returns an empty array if no project issue matches labels' do
get api("#{base_url}/issues", user), params: { labels: 'foo,bar' }
......@@ -1215,6 +1336,19 @@
expect(json_response['assignees'].first['name']).to eq(user2.name)
end
it 'creates a new project issue with labels param as array' do
post api("/projects/#{project.id}/issues", user),
params: { title: 'new issue', labels: %w(label label2), weight: 3, assignee_ids: [user2.id] }
expect(response).to have_gitlab_http_status(201)
expect(json_response['title']).to eq('new issue')
expect(json_response['description']).to be_nil
expect(json_response['labels']).to eq(%w(label label2))
expect(json_response['confidential']).to be_falsy
expect(json_response['assignee']['name']).to eq(user2.name)
expect(json_response['assignees'].first['name']).to eq(user2.name)
end
it 'creates a new confidential project issue' do
post api("/projects/#{project.id}/issues", user),
params: { title: 'new issue', confidential: true }
......@@ -1269,6 +1403,20 @@
expect(json_response['labels']).to include '&'
end
it 'allows special label names with labels param as array' do
post api("/projects/#{project.id}/issues", user),
params: {
title: 'new issue',
labels: ['label', 'label?', 'label&foo, ?, &']
}
expect(response.status).to eq(201)
expect(json_response['labels']).to include 'label'
expect(json_response['labels']).to include 'label?'
expect(json_response['labels']).to include 'label&foo'
expect(json_response['labels']).to include '?'
expect(json_response['labels']).to include '&'
end
it 'returns 400 if title is too long' do
post api("/projects/#{project.id}/issues", user),
params: { title: 'g' * 256 }
......@@ -1377,6 +1525,12 @@
post api("/projects/#{project.id}/issues", non_member), params: { title: 'new issue', labels: 'label, label2' }
end.not_to change { project.labels.count }
end
it 'cannot create new labels with labels param as array' do
expect do
post api("/projects/#{project.id}/issues", non_member), params: { title: 'new issue', labels: %w(label label2) }
end.not_to change { project.labels.count }
end
end
end
......@@ -1444,6 +1598,21 @@
expect(json_response['labels']).to include '&'
end
it 'allows special label names with labels param as array' do
put api("/projects/#{project.id}/issues/#{issue.iid}", user),
params: {
title: 'updated title',
labels: ['label', 'label?', 'label&foo, ?, &']
}
expect(response.status).to eq(200)
expect(json_response['labels']).to include 'label'
expect(json_response['labels']).to include 'label?'
expect(json_response['labels']).to include 'label&foo'
expect(json_response['labels']).to include '?'
expect(json_response['labels']).to include '&'
end
context 'confidential issues' do
it "returns 403 for non project members" do
put api("/projects/#{project.id}/issues/#{confidential_issue.iid}", non_member),
......@@ -1603,6 +1772,16 @@
expect(json_response['updated_at']).to be > Time.now
end
it 'removes all labels and touches the record with labels param as array' do
Timecop.travel(1.minute.from_now) do
put api("/projects/#{project.id}/issues/#{issue.iid}", user), params: { labels: [''] }
end
expect(response).to have_gitlab_http_status(200)
expect(json_response['labels']).to eq([])
expect(json_response['updated_at']).to be > Time.now
end
it 'updates labels and touches the record' do
Timecop.travel(1.minute.from_now) do
put api("/projects/#{project.id}/issues/#{issue.iid}", user),
......@@ -1614,6 +1793,17 @@
expect(json_response['updated_at']).to be > Time.now
end
it 'updates labels and touches the record with labels param as array' do
Timecop.travel(1.minute.from_now) do
put api("/projects/#{project.id}/issues/#{issue.iid}", user),
params: { labels: %w(foo bar) }
end
expect(response).to have_gitlab_http_status(200)
expect(json_response['labels']).to include 'foo'
expect(json_response['labels']).to include 'bar'
expect(json_response['updated_at']).to be > Time.now
end
it 'allows special label names' do
put api("/projects/#{project.id}/issues/#{issue.iid}", user),
params: { labels: 'label:foo, label-bar,label_bar,label/bar,label?bar,label&bar,?,&' }
......@@ -1628,6 +1818,20 @@
expect(json_response['labels']).to include '&'
end
it 'allows special label names with labels param as array' do
put api("/projects/#{project.id}/issues/#{issue.iid}", user),
params: { labels: ['label:foo', 'label-bar', 'label_bar', 'label/bar,label?bar,label&bar,?,&'] }
expect(response.status).to eq(200)
expect(json_response['labels']).to include 'label:foo'
expect(json_response['labels']).to include 'label-bar'
expect(json_response['labels']).to include 'label_bar'
expect(json_response['labels']).to include 'label/bar'
expect(json_response['labels']).to include 'label?bar'
expect(json_response['labels']).to include 'label&bar'
expect(json_response['labels']).to include '?'
expect(json_response['labels']).to include '&'
end
it 'returns 400 if title is too long' do
put api("/projects/#{project.id}/issues/#{issue.iid}", user),
params: { title: 'g' * 256 }
......
......@@ -617,26 +617,115 @@
end
end
describe "POST /projects/:id/merge_requests" do
describe 'POST /projects/:id/merge_requests' do
context 'between branches projects' do
it "returns merge_request" do
post api("/projects/#{project.id}/merge_requests", user),
params: {
title: 'Test merge_request',
source_branch: 'feature_conflict',
target_branch: 'master',
author: user,
labels: 'label, label2',
milestone_id: milestone.id,
squash: true
}
context 'different labels' do
let(:params) do
{
title: 'Test merge_request',
source_branch: 'feature_conflict',
target_branch: 'master',
author_id: user.id,
milestone_id: milestone.id,
squash: true
}
end
expect(response).to have_gitlab_http_status(201)
expect(json_response['title']).to eq('Test merge_request')
expect(json_response['labels']).to eq(%w(label label2))
expect(json_response['milestone']['id']).to eq(milestone.id)
expect(json_response['squash']).to be_truthy
expect(json_response['force_remove_source_branch']).to be_falsy
shared_examples_for 'creates merge request with labels' do
it 'returns merge_request' do
params[:labels] = labels
post api("/projects/#{project.id}/merge_requests", user), params: params
expect(response).to have_gitlab_http_status(201)
expect(json_response['title']).to eq('Test merge_request')
expect(json_response['labels']).to eq(%w(label label2))
expect(json_response['milestone']['id']).to eq(milestone.id)
expect(json_response['squash']).to be_truthy
expect(json_response['force_remove_source_branch']).to be_falsy
end
end
it_behaves_like 'creates merge request with labels' do
let(:labels) { 'label, label2' }
end
it_behaves_like 'creates merge request with labels' do
let(:labels) { %w(label label2) }
end
it_behaves_like 'creates merge request with labels' do
let(:labels) { %w(label label2) }
end
it 'creates merge request with special label names' do
params[:labels] = 'label, label?, label&foo, ?, &'
post api("/projects/#{project.id}/merge_requests", user), params: params
expect(response).to have_gitlab_http_status(201)
expect(json_response['labels']).to include 'label'
expect(json_response['labels']).to include 'label?'
expect(json_response['labels']).to include 'label&foo'
expect(json_response['labels']).to include '?'
expect(json_response['labels']).to include '&'
end
it 'creates merge request with special label names as array' do
params[:labels] = ['label', 'label?', 'label&foo, ?, &', '1, 2', 3, 4]
post api("/projects/#{project.id}/merge_requests", user), params: params
expect(response).to have_gitlab_http_status(201)
expect(json_response['labels']).to include 'label'
expect(json_response['labels']).to include 'label?'
expect(json_response['labels']).to include 'label&foo'
expect(json_response['labels']).to include '?'
expect(json_response['labels']).to include '&'
expect(json_response['labels']).to include '1'
expect(json_response['labels']).to include '2'
expect(json_response['labels']).to include '3'
expect(json_response['labels']).to include '4'
end
it 'empty label param does not add any labels' do
params[:labels] = ''
post api("/projects/#{project.id}/merge_requests", user), params: params
expect(response).to have_gitlab_http_status(201)
expect(json_response['labels']).to eq([])
end
it 'empty label param as array does not add any labels, but only explicitly as json' do
params[:labels] = []
post api("/projects/#{project.id}/merge_requests", user),
params: params.to_json,
headers: { 'Content-Type': 'application/json' }
expect(response).to have_gitlab_http_status(201)
expect(json_response['labels']).to eq([])
end
xit 'empty label param as array, does not add any labels' do
params[:labels] = []
post api("/projects/#{project.id}/merge_requests", user), params: params
expect(response).to have_gitlab_http_status(201)
expect(json_response['labels']).to eq([])
end
it 'array with one empty string element does not add labels' do
params[:labels] = ['']
post api("/projects/#{project.id}/merge_requests", user), params: params
expect(response).to have_gitlab_http_status(201)
expect(json_response['labels']).to eq([])
end
it 'array with multiple empty string elements, does not add labels' do
params[:labels] = ['', '', '']
post api("/projects/#{project.id}/merge_requests", user), params: params
expect(response).to have_gitlab_http_status(201)
expect(json_response['labels']).to eq([])
end
end
it "returns 422 when source_branch equals target_branch" do
......@@ -663,23 +752,6 @@
expect(response).to have_gitlab_http_status(400)
end
it 'allows special label names' do
post api("/projects/#{project.id}/merge_requests", user),
params: {
title: 'Test merge_request',
source_branch: 'markdown',
target_branch: 'master',
author: user,
labels: 'label, label?, label&foo, ?, &'
}
expect(response).to have_gitlab_http_status(201)
expect(json_response['labels']).to include 'label'
expect(json_response['labels']).to include 'label?'
expect(json_response['labels']).to include 'label&foo'
expect(json_response['labels']).to include '?'
expect(json_response['labels']).to include '&'
end
context 'with existing MR' do
before do
post api("/projects/#{project.id}/merge_requests", user),
......@@ -1122,32 +1194,97 @@
expect(json_response['force_remove_source_branch']).to be_truthy
end
it 'allows special label names' do
put api("/projects/#{project.id}/merge_requests/#{merge_request.iid}", user),
params: {
title: 'new issue',
labels: 'label, label?, label&foo, ?, &'
}
context 'when updating labels' do
it 'allows special label names' do
put api("/projects/#{project.id}/merge_requests/#{merge_request.iid}", user),
params: {
title: 'new issue',
labels: 'label, label?, label&foo, ?, &'
}
expect(response.status).to eq(200)
expect(json_response['labels']).to include 'label'
expect(json_response['labels']).to include 'label?'
expect(json_response['labels']).to include 'label&foo'
expect(json_response['labels']).to include '?'
expect(json_response['labels']).to include '&'