diff --git a/CHANGELOG b/CHANGELOG index 77556ad8234..c633c4bb35f 100644 --- a/CHANGELOG +++ b/CHANGELOG @@ -17,6 +17,7 @@ v 8.7.0 (unreleased) - Expose label description in API (Mariusz Jachimowicz) - Allow back dating on issues when created through the API - API: Ability to update a group (Robert Schilling) + - API: Ability to move issues (Robert Schilling) - Fix Error 500 after renaming a project path (Stan Hu) - Fix avatar stretching by providing a cropping feature - API: Expose `subscribed` for issues and merge requests (Robert Schilling) diff --git a/doc/api/issues.md b/doc/api/issues.md index 1c635a6cdcf..f09847aef95 100644 --- a/doc/api/issues.md +++ b/doc/api/issues.md @@ -351,6 +351,61 @@ DELETE /projects/:id/issues/:issue_id curl -X DELETE -H "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" https://gitlab.example.com/api/v3/projects/4/issues/85 ``` +## Move an issue + +Moves an issue to a different project. If the operation is successful, a status +code `201` together with moved issue is returned. If the project, issue, or +target project is not found, error `404` is returned. If the target project +equals the source project or the user has insufficient permissions to move an +issue, error `400` together with an explaining error message is returned. + +``` +POST /projects/:id/issues/:issue_id/move +``` + +| Attribute | Type | Required | Description | +| --------- | ---- | -------- | ----------- | +| `id` | integer | yes | The ID of a project | +| `issue_id` | integer | yes | The ID of a project's issue | +| `to_project_id` | integer | yes | The ID of the new project | + +```bash +curl -X POST -H "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" https://gitlab.example.com/api/v3/projects/4/issues/85/move +``` + +Example response: + +```json +{ + "id": 92, + "iid": 11, + "project_id": 5, + "title": "Sit voluptas tempora quisquam aut doloribus et.", + "description": "Repellat voluptas quibusdam voluptatem exercitationem.", + "state": "opened", + "created_at": "2016-04-05T21:41:45.652Z", + "updated_at": "2016-04-07T12:20:17.596Z", + "labels": [], + "milestone": null, + "assignee": { + "name": "Miss Monserrate Beier", + "username": "axel.block", + "id": 12, + "state": "active", + "avatar_url": "http://www.gravatar.com/avatar/46f6f7dc858ada7be1853f7fb96e81da?s=80&d=identicon", + "web_url": "https://gitlab.example.com/u/axel.block" + }, + "author": { + "name": "Kris Steuber", + "username": "solon.cremin", + "id": 10, + "state": "active", + "avatar_url": "http://www.gravatar.com/avatar/7a190fecbaa68212a4b68aeb6e3acd10?s=80&d=identicon", + "web_url": "https://gitlab.example.com/u/solon.cremin" + } +} +``` + ## Comments on issues Comments are done via the [notes](notes.md) resource. diff --git a/lib/api/issues.rb b/lib/api/issues.rb index c4ea05ee6cf..850e99981ff 100644 --- a/lib/api/issues.rb +++ b/lib/api/issues.rb @@ -195,6 +195,29 @@ module API end end + # Move an existing issue + # + # Parameters: + # id (required) - The ID of a project + # issue_id (required) - The ID of a project issue + # to_project_id (required) - The ID of the new project + # Example Request: + # POST /projects/:id/issues/:issue_id/move + post ':id/issues/:issue_id/move' do + required_attributes! [:to_project_id] + + issue = user_project.issues.find(params[:issue_id]) + new_project = Project.find(params[:to_project_id]) + + begin + issue = ::Issues::MoveService.new(user_project, current_user).execute(issue, new_project) + present issue, with: Entities::Issue, current_user: current_user + rescue ::Issues::MoveService::MoveError => error + render_api_error!(error.message, 400) + end + end + + # # Delete a project issue # # Parameters: diff --git a/spec/requests/api/issues_spec.rb b/spec/requests/api/issues_spec.rb index 822d3ad3017..3d7a31cbb6a 100644 --- a/spec/requests/api/issues_spec.rb +++ b/spec/requests/api/issues_spec.rb @@ -7,7 +7,7 @@ describe API::API, api: true do let(:author) { create(:author) } let(:assignee) { create(:assignee) } let(:admin) { create(:user, :admin) } - let!(:project) { create(:project, :public, namespace: user.namespace ) } + let!(:project) { create(:project, :public, creator_id: user.id, namespace: user.namespace ) } let!(:closed_issue) do create :closed_issue, author: user, @@ -501,4 +501,72 @@ describe API::API, api: true do end end end + + describe '/projects/:id/issues/:issue_id/move' do + let!(:target_project) { create(:project, path: 'project2', creator_id: user.id, namespace: user.namespace ) } + let!(:target_project2) { create(:project, creator_id: non_member.id, namespace: non_member.namespace ) } + + it 'moves an issue' do + post api("/projects/#{project.id}/issues/#{issue.id}/move", user), + to_project_id: target_project.id + + expect(response.status).to eq(201) + expect(json_response['project_id']).to eq(target_project.id) + end + + context 'when source and target projects are the same' do + it 'returns 400 when trying to move an issue' do + post api("/projects/#{project.id}/issues/#{issue.id}/move", user), + to_project_id: project.id + + expect(response.status).to eq(400) + expect(json_response['message']).to eq('Cannot move issue to project it originates from!') + end + end + + context 'when the user does not have the permission to move issues' do + it 'returns 400 when trying to move an issue' do + post api("/projects/#{project.id}/issues/#{issue.id}/move", user), + to_project_id: target_project2.id + + expect(response.status).to eq(400) + expect(json_response['message']).to eq('Cannot move issue due to insufficient permissions!') + end + end + + it 'moves the issue to another namespace if I am admin' do + post api("/projects/#{project.id}/issues/#{issue.id}/move", admin), + to_project_id: target_project2.id + + expect(response.status).to eq(201) + expect(json_response['project_id']).to eq(target_project2.id) + end + + context 'when issue does not exist' do + it 'returns 404 when trying to move an issue' do + post api("/projects/#{project.id}/issues/123/move", user), + to_project_id: target_project.id + + expect(response.status).to eq(404) + end + end + + context 'when source project does not exist' do + it 'returns 404 when trying to move an issue' do + post api("/projects/123/issues/#{issue.id}/move", user), + to_project_id: target_project.id + + expect(response.status).to eq(404) + end + end + + context 'when target project does not exist' do + it 'returns 404 when trying to move an issue' do + post api("/projects/#{project.id}/issues/#{issue.id}/move", user), + to_project_id: 123 + + expect(response.status).to eq(404) + end + end + end end