Merge branch 'adamco/gitlab-ce-move-issue-command' into 'master'

Add slash command for moving an issue

See merge request gitlab-org/gitlab-ce!17691
This commit is contained in:
Grzegorz Bizon 2018-03-15 13:57:50 +00:00
commit 81d370d419
8 changed files with 254 additions and 1 deletions

View file

@ -0,0 +1,5 @@
---
title: Add slash command for moving issues
merge_request:
author: Adam Pahlevi
type: added

View file

@ -15,9 +15,10 @@ Taking the trigger term as `project-name`, the commands are:
| `/project-name issue new <title> <shift+return> <description>` | Creates a new issue with title `<title>` and description `<description>` |
| `/project-name issue show <id>` | Shows the issue with id `<id>` |
| `/project-name issue search <query>` | Shows up to 5 issues matching `<query>` |
| `/project-name issue move <id> to <project>` | Moves issue ID `<id>` to `<project>` |
| `/project-name deploy <from> to <to>` | Deploy from the `<from>` environment to the `<to>` environment |
Note that if you are using the [GitLab Slack application](https://docs.gitlab.com/ee/user/project/integrations/gitlab_slack_application.html) for
Note that if you are using the [GitLab Slack application](https://docs.gitlab.com/ee/user/project/integrations/gitlab_slack_application.html) for
your GitLab.com projects, you need to [add the `gitlab` keyword at the beginning of the command](https://docs.gitlab.com/ee/user/project/integrations/gitlab_slack_application.html#usage).
## Issue commands

View file

@ -5,6 +5,7 @@ module Gitlab
Gitlab::SlashCommands::IssueShow,
Gitlab::SlashCommands::IssueNew,
Gitlab::SlashCommands::IssueSearch,
Gitlab::SlashCommands::IssueMove,
Gitlab::SlashCommands::Deploy
].freeze

View file

@ -0,0 +1,45 @@
module Gitlab
module SlashCommands
class IssueMove < IssueCommand
def self.match(text)
%r{
\A # the beginning of a string
issue\s+move\s+ # the command
\#?(?<iid>\d+)\s+ # the issue id, may preceded by hash sign
(to\s+)? # aid the command to be much more human-ly
(?<project_path>[^\s]+) # named group for id of dest. project
}x.match(text)
end
def self.help_message
'issue move <issue_id> (to)? <project_path>'
end
def self.allowed?(project, user)
can?(user, :admin_issue, project)
end
def execute(match)
old_issue = find_by_iid(match[:iid])
target_project = Project.find_by_full_path(match[:project_path])
unless current_user.can?(:read_project, target_project) && old_issue
return Gitlab::SlashCommands::Presenters::Access.new.not_found
end
new_issue = Issues::MoveService.new(project, current_user)
.execute(old_issue, target_project)
presenter(new_issue).present(old_issue)
rescue Issues::MoveService::MoveError => e
presenter(old_issue).display_move_error(e.message)
end
private
def presenter(issue)
Gitlab::SlashCommands::Presenters::IssueMove.new(issue)
end
end
end
end

View file

@ -0,0 +1,53 @@
# coding: utf-8
module Gitlab
module SlashCommands
module Presenters
class IssueMove < Presenters::Base
include Presenters::IssueBase
def present(old_issue)
in_channel_response(moved_issue(old_issue))
end
def display_move_error(error)
message = header_with_list("The action was not successful, because:", [error])
ephemeral_response(text: message)
end
private
def moved_issue(old_issue)
{
attachments: [
{
title: "#{@resource.title} · #{@resource.to_reference}",
title_link: resource_url,
author_name: author.name,
author_icon: author.avatar_url,
fallback: "Issue #{@resource.to_reference}: #{@resource.title}",
pretext: pretext(old_issue),
color: color(@resource),
fields: fields,
mrkdwn_in: [
:title,
:pretext,
:text,
:fields
]
}
]
}
end
def pretext(old_issue)
"Moved issue *#{issue_link(old_issue)}* to *#{issue_link(@resource)}*"
end
def issue_link(issue)
"[#{issue.to_reference}](#{project_issue_url(issue.project, issue)})"
end
end
end
end
end

View file

@ -108,5 +108,10 @@ describe Gitlab::SlashCommands::Command do
it { is_expected.to eq(Gitlab::SlashCommands::IssueSearch) }
end
context 'IssueMove is triggered' do
let(:params) { { text: 'issue move #78291 to gitlab/gitlab-ci' } }
it { is_expected.to eq(Gitlab::SlashCommands::IssueMove) }
end
end
end

View file

@ -0,0 +1,117 @@
require 'spec_helper'
describe Gitlab::SlashCommands::IssueMove, service: true do
describe '#match' do
shared_examples_for 'move command' do |text_command|
it 'can be parsed to extract the needed fields' do
match_data = described_class.match(text_command)
expect(match_data['iid']).to eq('123456')
expect(match_data['project_path']).to eq('gitlab/gitlab-ci')
end
end
it_behaves_like 'move command', 'issue move #123456 to gitlab/gitlab-ci'
it_behaves_like 'move command', 'issue move #123456 gitlab/gitlab-ci'
it_behaves_like 'move command', 'issue move #123456 gitlab/gitlab-ci '
it_behaves_like 'move command', 'issue move 123456 to gitlab/gitlab-ci'
it_behaves_like 'move command', 'issue move 123456 gitlab/gitlab-ci'
it_behaves_like 'move command', 'issue move 123456 gitlab/gitlab-ci '
end
describe '#execute' do
set(:user) { create(:user) }
set(:issue) { create(:issue) }
set(:chat_name) { create(:chat_name, user: user) }
set(:project) { issue.project }
set(:other_project) { create(:project, namespace: project.namespace) }
before do
[project, other_project].each { |prj| prj.add_master(user) }
end
subject { described_class.new(project, chat_name) }
def process_message(message)
subject.execute(described_class.match(message))
end
context 'when the user can move the issue' do
context 'when the move fails' do
it 'returns the error message' do
message = "issue move #{issue.iid} #{project.full_path}"
expect(process_message(message)).to include(response_type: :ephemeral,
text: a_string_matching('Cannot move issue'))
end
end
context 'when the move succeeds' do
let(:message) { "issue move #{issue.iid} #{other_project.full_path}" }
it 'moves the issue to the new destination' do
expect { process_message(message) }.to change { Issue.count }.by(1)
new_issue = issue.reload.moved_to
expect(new_issue.state).to eq('opened')
expect(new_issue.project_id).to eq(other_project.id)
expect(new_issue.author_id).to eq(issue.author_id)
expect(issue.state).to eq('closed')
expect(issue.project_id).to eq(project.id)
end
it 'returns the new issue' do
expect(process_message(message))
.to include(response_type: :in_channel,
attachments: [a_hash_including(title_link: a_string_including(other_project.full_path))])
end
it 'mentions the old issue' do
expect(process_message(message))
.to include(attachments: [a_hash_including(pretext: a_string_including(project.full_path))])
end
end
end
context 'when the issue does not exist' do
it 'returns not found' do
message = "issue move #{issue.iid.succ} #{other_project.full_path}"
expect(process_message(message)).to include(response_type: :ephemeral,
text: a_string_matching('not found'))
end
end
context 'when the target project does not exist' do
it 'returns not found' do
message = "issue move #{issue.iid} #{other_project.full_path}/foo"
expect(process_message(message)).to include(response_type: :ephemeral,
text: a_string_matching('not found'))
end
end
context 'when the user cannot see the target project' do
it 'returns not found' do
message = "issue move #{issue.iid} #{other_project.full_path}"
other_project.team.truncate
expect(process_message(message)).to include(response_type: :ephemeral,
text: a_string_matching('not found'))
end
end
context 'when the user does not have the required permissions on the target project' do
it 'returns the error message' do
message = "issue move #{issue.iid} #{other_project.full_path}"
other_project.team.truncate
other_project.team.add_guest(user)
expect(process_message(message)).to include(response_type: :ephemeral,
text: a_string_matching('Cannot move issue'))
end
end
end
end

View file

@ -0,0 +1,26 @@
require 'spec_helper'
describe Gitlab::SlashCommands::Presenters::IssueMove do
set(:admin) { create(:admin) }
set(:project) { create(:project) }
set(:other_project) { create(:project) }
set(:old_issue) { create(:issue, project: project) }
set(:new_issue) { Issues::MoveService.new(project, admin).execute(old_issue, other_project) }
let(:attachment) { subject[:attachments].first }
subject { described_class.new(new_issue).present(old_issue) }
it { is_expected.to be_a(Hash) }
it 'shows the new issue' do
expect(subject[:response_type]).to be(:in_channel)
expect(subject).to have_key(:attachments)
expect(attachment[:title]).to start_with(new_issue.title)
expect(attachment[:title_link]).to include(other_project.full_path)
end
it 'mentions the old issue and the new issue in the pretext' do
expect(attachment[:pretext]).to include(project.full_path)
expect(attachment[:pretext]).to include(other_project.full_path)
end
end