Welcome to mirror list, hosted at ThFree Co, Russian Federation.

gitlab.com/gitlab-org/gitlab-foss.git - Unnamed repository; edit this file 'description' to name the repository.
summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorGitLab Bot <gitlab-bot@gitlab.com>2021-09-21 06:11:06 +0300
committerGitLab Bot <gitlab-bot@gitlab.com>2021-09-21 06:11:06 +0300
commit175e7c55be739ac9a708448ea669ca835194c8c0 (patch)
treea7f77e56a8e90fb5054a9d5735bcf96e3814b400
parentb1031060f00cdb201cf8d790c6ec6421860c30f3 (diff)
Add latest changes from gitlab-org/gitlab@master
-rw-r--r--GITLAB_PAGES_VERSION2
-rw-r--r--app/assets/javascripts/main.js2
-rw-r--r--doc/api/issues.md107
-rw-r--r--lib/api/issues.rb28
-rw-r--r--qa/Gemfile.lock30
-rw-r--r--spec/requests/api/issues/post_projects_issues_spec.rb144
6 files changed, 294 insertions, 19 deletions
diff --git a/GITLAB_PAGES_VERSION b/GITLAB_PAGES_VERSION
index 372cf402c73..50aceaa7b71 100644
--- a/GITLAB_PAGES_VERSION
+++ b/GITLAB_PAGES_VERSION
@@ -1 +1 @@
-1.44.0
+1.45.0
diff --git a/app/assets/javascripts/main.js b/app/assets/javascripts/main.js
index 4962268c8e9..e422d9b1a32 100644
--- a/app/assets/javascripts/main.js
+++ b/app/assets/javascripts/main.js
@@ -14,6 +14,7 @@ import { GlBreakpointInstance as bp } from '@gitlab/ui/dist/utils';
import { initRails } from '~/lib/utils/rails_ujs';
import * as popovers from '~/popovers';
import * as tooltips from '~/tooltips';
+import { initHeaderSearchApp } from '~/header_search';
import initAlertHandler from './alert_handler';
import { removeFlashClickListener } from './flash';
import initTodoToggle from './header';
@@ -35,7 +36,6 @@ import GlFieldErrors from './gl_field_errors';
import initUserPopovers from './user_popovers';
import initBroadcastNotifications from './broadcast_notification';
import { initTopNav } from './nav';
-import { initHeaderSearchApp } from '~/header_search';
import 'ee_else_ce/main_ee';
import 'jh_else_ce/main_jh';
diff --git a/doc/api/issues.md b/doc/api/issues.md
index 97d0fd3ce8f..204d75e9ee4 100644
--- a/doc/api/issues.md
+++ b/doc/api/issues.md
@@ -1487,6 +1487,113 @@ NOTE:
The `closed_by` attribute was [introduced](https://gitlab.com/gitlab-org/gitlab-foss/-/merge_requests/17042) in GitLab 10.6. This value is only present for issues closed after GitLab 10.6 and if the user account that closed
the issue still exists.
+## Clone an issue
+
+Clone the issue to given project. If the user has insufficient permissions,
+an error message with status code `400` is returned.
+
+Copies as much data as possible as long as the target project contains equivalent labels, milestones,
+and so on.
+
+```plaintext
+POST /projects/:id/issues/:issue_iid/clone
+```
+
+| Attribute | Type | Required | Description |
+| --------------- | -------------- | ---------------------- | --------------------------------- |
+| `id` | integer/string | **{check-circle}** Yes | ID or [URL-encoded path of the project](index.md#namespaced-path-encoding) owned by the authenticated user. |
+| `issue_iid` | integer | **{check-circle}** Yes | Internal ID of a project's issue. |
+| `to_project_id` | integer | **{check-circle}** Yes | ID of the new project. |
+| `with_notes` | boolean | **{dotted-circle}** No | Clone the issue with [notes](notes.md). Default is `false`. |
+
+```shell
+curl --request POST \
+--header "PRIVATE-TOKEN: <your_access_token>" "https://gitlab.example.com/api/v4/projects/5/issues/1/clone?with_notes=true&to_project_id=6"
+```
+
+Example response:
+
+```json
+{
+ "id":290,
+ "iid":1,
+ "project_id":143,
+ "title":"foo",
+ "description":"closed",
+ "state":"opened",
+ "created_at":"2021-09-14T22:24:11.696Z",
+ "updated_at":"2021-09-14T22:24:11.696Z",
+ "closed_at":null,
+ "closed_by":null,
+ "labels":[
+
+ ],
+ "milestone":null,
+ "assignees":[
+ {
+ "id":179,
+ "name":"John Doe2",
+ "username":"john",
+ "state":"active",
+ "avatar_url":"https://www.gravatar.com/avatar/10fc7f102be8de7657fb4d80898bbfe3?s=80\u0026d=identicon",
+ "web_url":"https://gitlab.example.com/john"
+ }
+ ],
+ "author":{
+ "id":179,
+ "name":"John Doe2",
+ "username":"john",
+ "state":"active",
+ "avatar_url":"https://www.gravatar.com/avatar/10fc7f102be8de7657fb4d80898bbfe3?s=80\u0026d=identicon",
+ "web_url":"https://gitlab.example.com/john"
+ },
+ "type":"ISSUE",
+ "assignee":{
+ "id":179,
+ "name":"John Doe2",
+ "username":"john",
+ "state":"active",
+ "avatar_url":"https://www.gravatar.com/avatar/10fc7f102be8de7657fb4d80898bbfe3?s=80\u0026d=identicon",
+ "web_url":"https://gitlab.example.com/john"
+ },
+ "user_notes_count":1,
+ "merge_requests_count":0,
+ "upvotes":0,
+ "downvotes":0,
+ "due_date":null,
+ "confidential":false,
+ "discussion_locked":null,
+ "issue_type":"issue",
+ "web_url":"https://gitlab.example.com/namespace1/project2/-/issues/1",
+ "time_stats":{
+ "time_estimate":0,
+ "total_time_spent":0,
+ "human_time_estimate":null,
+ "human_total_time_spent":null
+ },
+ "task_completion_status":{
+ "count":0,
+ "completed_count":0
+ },
+ "blocking_issues_count":0,
+ "has_tasks":false,
+ "_links":{
+ "self":"https://gitlab.example.com/api/v4/projects/143/issues/1",
+ "notes":"https://gitlab.example.com/api/v4/projects/143/issues/1/notes",
+ "award_emoji":"https://gitlab.example.com/api/v4/projects/143/issues/1/award_emoji",
+ "project":"https://gitlab.example.com/api/v4/projects/143"
+ },
+ "references":{
+ "short":"#1",
+ "relative":"#1",
+ "full":"namespace1/project2#1"
+ },
+ "subscribed":true,
+ "moved_to_id":null,
+ "service_desk_reply_to":null
+}
+```
+
## Subscribe to an issue
Subscribes the authenticated user to an issue to receive notifications.
diff --git a/lib/api/issues.rb b/lib/api/issues.rb
index 39ce6e0b062..5e66949d264 100644
--- a/lib/api/issues.rb
+++ b/lib/api/issues.rb
@@ -375,6 +375,34 @@ module API
end
# rubocop: enable CodeReuse/ActiveRecord
+ desc 'Clone an existing issue' do
+ success Entities::Issue
+ end
+ params do
+ requires :issue_iid, type: Integer, desc: 'The internal ID of a project issue'
+ requires :to_project_id, type: Integer, desc: 'The ID of the new project'
+ optional :with_notes, type: Boolean, desc: 'Clone issue with notes', default: false
+ end
+ # rubocop: disable CodeReuse/ActiveRecord
+ post ':id/issues/:issue_iid/clone' do
+ Gitlab::QueryLimiting.disable!('https://gitlab.com/gitlab-org/gitlab/-/issues/340252')
+
+ issue = user_project.issues.find_by(iid: params[:issue_iid])
+ not_found!('Issue') unless issue
+
+ target_project = Project.find_by(id: params[:to_project_id])
+ not_found!('Project') unless target_project
+
+ begin
+ issue = ::Issues::CloneService.new(project: user_project, current_user: current_user)
+ .execute(issue, target_project, with_notes: params[:with_notes])
+ present issue, with: Entities::Issue, current_user: current_user, project: target_project
+ rescue ::Issues::CloneService::CloneError => error
+ render_api_error!(error.message, 400)
+ end
+ end
+ # rubocop: enable CodeReuse/ActiveRecord
+
desc 'Delete a project issue'
params do
requires :issue_iid, type: Integer, desc: 'The internal ID of a project issue'
diff --git a/qa/Gemfile.lock b/qa/Gemfile.lock
index 2f9f14e1ac7..8c24414555f 100644
--- a/qa/Gemfile.lock
+++ b/qa/Gemfile.lock
@@ -81,10 +81,33 @@ GEM
faraday-net_http (1.0.1)
faraday-net_http_persistent (1.2.0)
faraday-patron (1.0.0)
- gitlab-qa (4.0.0)
+ ffi (1.15.4)
+ ffi-compiler (1.0.1)
+ ffi (>= 1.0.0)
+ rake
+ gitlab (4.16.1)
+ httparty (~> 0.14, >= 0.14.0)
+ terminal-table (~> 1.5, >= 1.5.1)
+ gitlab-qa (7.9.1)
+ activesupport (~> 6.1)
+ gitlab (~> 4.16.1)
+ http (= 4.3.0)
+ nokogiri (~> 1.10)
+ table_print (= 1.5.7)
+ http (4.3.0)
+ addressable (~> 2.3)
+ http-cookie (~> 1.0)
+ http-form_data (~> 2.2)
+ http-parser (~> 1.2.0)
http-accept (1.7.0)
http-cookie (1.0.3)
domain_name (~> 0.5)
+ http-form_data (2.3.0)
+ http-parser (1.2.3)
+ ffi-compiler (>= 1.0, < 2.0)
+ httparty (0.19.0)
+ mime-types (~> 3.0)
+ multi_xml (>= 0.5.2)
i18n (1.8.10)
concurrent-ruby (~> 1.0)
ice_nine (0.11.2)
@@ -104,6 +127,7 @@ GEM
mini_mime (1.1.0)
mini_portile2 (2.5.3)
minitest (5.14.4)
+ multi_xml (0.6.0)
multipart-post (2.1.1)
netrc (0.11.0)
nokogiri (1.11.7)
@@ -179,6 +203,9 @@ GEM
rexml (~> 3.2)
rubyzip (>= 1.2.2)
systemu (2.6.5)
+ table_print (1.5.7)
+ terminal-table (1.8.0)
+ unicode-display_width (~> 1.1, >= 1.1.1)
thread_safe (0.3.6)
timecop (0.9.1)
tzinfo (2.0.4)
@@ -186,6 +213,7 @@ GEM
unf (0.1.4)
unf_ext
unf_ext (0.0.7.7)
+ unicode-display_width (1.8.0)
unparser (0.4.7)
abstract_type (~> 0.0.7)
adamantium (~> 0.2.0)
diff --git a/spec/requests/api/issues/post_projects_issues_spec.rb b/spec/requests/api/issues/post_projects_issues_spec.rb
index 9d3bd26a200..01898c6f256 100644
--- a/spec/requests/api/issues/post_projects_issues_spec.rb
+++ b/spec/requests/api/issues/post_projects_issues_spec.rb
@@ -8,15 +8,15 @@ RSpec.describe API::Issues do
create(:project, :public, creator_id: user.id, namespace: user.namespace)
end
- let(:user2) { create(:user) }
- let(:non_member) { create(:user) }
- let_it_be(:guest) { create(:user) }
- let_it_be(:author) { create(:author) }
- let_it_be(:assignee) { create(:assignee) }
- let(:admin) { create(:user, :admin) }
- let(:issue_title) { 'foo' }
- let(:issue_description) { 'closed' }
- let!(:closed_issue) do
+ let_it_be(:user2) { create(:user) }
+ let_it_be(:non_member) { create(:user) }
+ let_it_be(:guest) { create(:user) }
+ let_it_be(:author) { create(:author) }
+ let_it_be(:milestone) { create(:milestone, title: '1.0.0', project: project) }
+ let_it_be(:assignee) { create(:assignee) }
+ let_it_be(:admin) { create(:user, :admin) }
+
+ let_it_be(:closed_issue) do
create :closed_issue,
author: user,
assignees: [user],
@@ -28,7 +28,7 @@ RSpec.describe API::Issues do
closed_at: 1.hour.ago
end
- let!(:confidential_issue) do
+ let_it_be(:confidential_issue) do
create :issue,
:confidential,
project: project,
@@ -38,7 +38,7 @@ RSpec.describe API::Issues do
updated_at: 2.hours.ago
end
- let!(:issue) do
+ let_it_be(:issue) do
create :issue,
author: user,
assignees: [user],
@@ -46,22 +46,21 @@ RSpec.describe API::Issues do
milestone: milestone,
created_at: generate(:past_time),
updated_at: 1.hour.ago,
- title: issue_title,
- description: issue_description
+ title: 'foo',
+ description: 'closed'
end
+ let_it_be(:note) { create(:note_on_issue, author: user, project: project, noteable: issue) }
+
let_it_be(:label) do
create(:label, title: 'label', color: '#FFAABB', project: project)
end
let!(:label_link) { create(:label_link, label: label, target: issue) }
- let(:milestone) { create(:milestone, title: '1.0.0', project: project) }
let_it_be(:empty_milestone) do
create(:milestone, title: '2.0.0', project: project)
end
- let!(:note) { create(:note_on_issue, author: user, project: project, noteable: issue) }
-
let(:no_milestone_title) { 'None' }
let(:any_milestone_title) { 'Any' }
@@ -556,6 +555,114 @@ RSpec.describe API::Issues do
end
end
+ describe '/projects/:id/issues/:issue_iid/clone' do
+ let_it_be(:valid_target_project) { create(:project) }
+ let_it_be(:invalid_target_project) { create(:project) }
+
+ before_all do
+ valid_target_project.add_maintainer(user)
+ end
+
+ context 'when user can admin the issue' do
+ context 'when the user can admin the target project' do
+ it 'clones the issue' do
+ expect do
+ post_clone_issue(user, issue, valid_target_project)
+ end.to change { valid_target_project.issues.count }.by(1)
+
+ cloned_issue = Issue.last
+
+ expect(cloned_issue.notes.count).to eq(2)
+ expect(cloned_issue.notes.pluck(:note)).not_to include(issue.notes.first.note)
+ expect(response).to have_gitlab_http_status(:created)
+ expect(json_response['id']).to eq(cloned_issue.id)
+ expect(json_response['project_id']).to eq(valid_target_project.id)
+ end
+
+ context 'when target project is the same source project' do
+ it 'clones the issue' do
+ expect do
+ post_clone_issue(user, issue, issue.project)
+ end.to change { issue.reset.project.issues.count }.by(1)
+
+ cloned_issue = Issue.last
+
+ expect(cloned_issue.notes.count).to eq(2)
+ expect(cloned_issue.notes.pluck(:note)).not_to include(issue.notes.first.note)
+ expect(response).to have_gitlab_http_status(:created)
+ expect(json_response['id']).to eq(cloned_issue.id)
+ expect(json_response['project_id']).to eq(issue.project.id)
+ end
+ end
+ end
+ end
+
+ context 'when the user does not have the permission to clone issues' do
+ it 'returns 400' do
+ post api("/projects/#{project.id}/issues/#{issue.iid}/clone", user),
+ params: { to_project_id: invalid_target_project.id }
+
+ expect(response).to have_gitlab_http_status(:bad_request)
+ expect(json_response['message']).to eq(s_('CloneIssue|Cannot clone issue due to insufficient permissions!'))
+ end
+ end
+
+ context 'when using the issue ID instead of iid' do
+ it 'returns 404' do
+ post api("/projects/#{project.id}/issues/#{issue.id}/clone", user),
+ params: { to_project_id: valid_target_project.id }
+
+ expect(response).to have_gitlab_http_status(:not_found)
+ expect(json_response['message']).to eq('404 Issue Not Found')
+ end
+ end
+
+ context 'when issue does not exist' do
+ it 'returns 404' do
+ post api("/projects/#{project.id}/issues/12300/clone", user),
+ params: { to_project_id: valid_target_project.id }
+
+ expect(response).to have_gitlab_http_status(:not_found)
+ expect(json_response['message']).to eq('404 Issue Not Found')
+ end
+ end
+
+ context 'when source project does not exist' do
+ it 'returns 404' do
+ post api("/projects/0/issues/#{issue.iid}/clone", user),
+ params: { to_project_id: valid_target_project.id }
+
+ expect(response).to have_gitlab_http_status(:not_found)
+ expect(json_response['message']).to eq('404 Project Not Found')
+ end
+ end
+
+ context 'when target project does not exist' do
+ it 'returns 404' do
+ post api("/projects/#{project.id}/issues/#{issue.iid}/clone", user),
+ params: { to_project_id: 0 }
+
+ expect(response).to have_gitlab_http_status(:not_found)
+ expect(json_response['message']).to eq('404 Project Not Found')
+ end
+ end
+
+ it 'clones the issue with notes when with_notes is true' do
+ expect do
+ post api("/projects/#{project.id}/issues/#{issue.iid}/clone", user),
+ params: { to_project_id: valid_target_project.id, with_notes: true }
+ end.to change { valid_target_project.issues.count }.by(1)
+
+ cloned_issue = Issue.last
+
+ expect(cloned_issue.notes.count).to eq(3)
+ expect(cloned_issue.notes.pluck(:note)).to include(issue.notes.first.note)
+ expect(response).to have_gitlab_http_status(:created)
+ expect(json_response['id']).to eq(cloned_issue.id)
+ expect(json_response['project_id']).to eq(valid_target_project.id)
+ end
+ end
+
describe 'POST :id/issues/:issue_iid/subscribe' do
it 'subscribes to an issue' do
post api("/projects/#{project.id}/issues/#{issue.iid}/subscribe", user2)
@@ -621,4 +728,9 @@ RSpec.describe API::Issues do
expect(response).to have_gitlab_http_status(:not_found)
end
end
+
+ def post_clone_issue(current_user, issue, target_project)
+ post api("/projects/#{issue.project.id}/issues/#{issue.iid}/clone", current_user),
+ params: { to_project_id: target_project.id }
+ end
end