summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorSean McGivern <sean@gitlab.com>2018-03-12 15:29:45 +0000
committerSean McGivern <sean@gitlab.com>2018-03-14 13:33:19 +0000
commitd275fcd2f61cd0590a116c195b8831659fdb0a51 (patch)
treeaf84be50afc55f1d1a4be85bbf45dea5e921e0b0
parent82b6222022c496290066fbdd6c3c2490bd23622c (diff)
downloadgitlab-ce-adamco/gitlab-ce-move-issue-command.tar.gz
Add slash command for moving an issueadamco/gitlab-ce-move-issue-command
Carried over from https://gitlab.com/gitlab-org/gitlab-ce/merge_requests/8857
-rw-r--r--changelogs/unreleased/adamco-gitlab-ce-move-issue-command.yml5
-rw-r--r--doc/integration/slash_commands.md3
-rw-r--r--lib/gitlab/slash_commands/command.rb1
-rw-r--r--lib/gitlab/slash_commands/issue_move.rb45
-rw-r--r--lib/gitlab/slash_commands/presenters/base.rb5
-rw-r--r--lib/gitlab/slash_commands/presenters/issue_move.rb47
-rw-r--r--spec/lib/gitlab/slash_commands/command_spec.rb5
-rw-r--r--spec/lib/gitlab/slash_commands/issue_move_spec.rb117
-rw-r--r--spec/lib/gitlab/slash_commands/presenters/issue_move_spec.rb26
9 files changed, 251 insertions, 3 deletions
diff --git a/changelogs/unreleased/adamco-gitlab-ce-move-issue-command.yml b/changelogs/unreleased/adamco-gitlab-ce-move-issue-command.yml
new file mode 100644
index 00000000000..3b057373e7d
--- /dev/null
+++ b/changelogs/unreleased/adamco-gitlab-ce-move-issue-command.yml
@@ -0,0 +1,5 @@
+---
+title: Add slash command for moving issues
+merge_request:
+author: Adam Pahlevi
+type: added
diff --git a/doc/integration/slash_commands.md b/doc/integration/slash_commands.md
index 36a8844e953..7d73026a6c6 100644
--- a/doc/integration/slash_commands.md
+++ b/doc/integration/slash_commands.md
@@ -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
diff --git a/lib/gitlab/slash_commands/command.rb b/lib/gitlab/slash_commands/command.rb
index 85aaa6b0eba..bb778f37096 100644
--- a/lib/gitlab/slash_commands/command.rb
+++ b/lib/gitlab/slash_commands/command.rb
@@ -5,6 +5,7 @@ module Gitlab
Gitlab::SlashCommands::IssueShow,
Gitlab::SlashCommands::IssueNew,
Gitlab::SlashCommands::IssueSearch,
+ Gitlab::SlashCommands::IssueMove,
Gitlab::SlashCommands::Deploy
].freeze
diff --git a/lib/gitlab/slash_commands/issue_move.rb b/lib/gitlab/slash_commands/issue_move.rb
new file mode 100644
index 00000000000..c082f52bbc4
--- /dev/null
+++ b/lib/gitlab/slash_commands/issue_move.rb
@@ -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 old_issue && target_project && current_user.can?(:read_project, target_project)
+ 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(new_issue).display_errors(e.message)
+ end
+
+ private
+
+ def presenter(issue)
+ Gitlab::SlashCommands::Presenters::IssueMove.new(issue)
+ end
+ end
+ end
+end
diff --git a/lib/gitlab/slash_commands/presenters/base.rb b/lib/gitlab/slash_commands/presenters/base.rb
index e13808a2720..ceec9ac3b8a 100644
--- a/lib/gitlab/slash_commands/presenters/base.rb
+++ b/lib/gitlab/slash_commands/presenters/base.rb
@@ -8,8 +8,9 @@ module Gitlab
@resource = resource
end
- def display_errors
- message = header_with_list("The action was not successful, because:", @resource.errors.full_messages)
+ def display_errors(*custom_messages)
+ error_messages = custom_messages.presence || @resource.errors.full_messages
+ message = header_with_list("The action was not successful, because:", error_messages)
ephemeral_response(text: message)
end
diff --git a/lib/gitlab/slash_commands/presenters/issue_move.rb b/lib/gitlab/slash_commands/presenters/issue_move.rb
new file mode 100644
index 00000000000..8dec7009073
--- /dev/null
+++ b/lib/gitlab/slash_commands/presenters/issue_move.rb
@@ -0,0 +1,47 @@
+# 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
+
+ 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
diff --git a/spec/lib/gitlab/slash_commands/command_spec.rb b/spec/lib/gitlab/slash_commands/command_spec.rb
index e3447d974aa..194cae8c645 100644
--- a/spec/lib/gitlab/slash_commands/command_spec.rb
+++ b/spec/lib/gitlab/slash_commands/command_spec.rb
@@ -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
diff --git a/spec/lib/gitlab/slash_commands/issue_move_spec.rb b/spec/lib/gitlab/slash_commands/issue_move_spec.rb
new file mode 100644
index 00000000000..d41441c9472
--- /dev/null
+++ b/spec/lib/gitlab/slash_commands/issue_move_spec.rb
@@ -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
diff --git a/spec/lib/gitlab/slash_commands/presenters/issue_move_spec.rb b/spec/lib/gitlab/slash_commands/presenters/issue_move_spec.rb
new file mode 100644
index 00000000000..58c341a284e
--- /dev/null
+++ b/spec/lib/gitlab/slash_commands/presenters/issue_move_spec.rb
@@ -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