Skip to content

Commit 81d370d

Browse files
committed
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
2 parents e5e1b7c + 0fa139d commit 81d370d

File tree

8 files changed

+254
-1
lines changed

8 files changed

+254
-1
lines changed
Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
title: Add slash command for moving issues
3+
merge_request:
4+
author: Adam Pahlevi
5+
type: added

doc/integration/slash_commands.md

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -15,9 +15,10 @@ Taking the trigger term as `project-name`, the commands are:
1515
| `/project-name issue new <title> <shift+return> <description>` | Creates a new issue with title `<title>` and description `<description>` |
1616
| `/project-name issue show <id>` | Shows the issue with id `<id>` |
1717
| `/project-name issue search <query>` | Shows up to 5 issues matching `<query>` |
18+
| `/project-name issue move <id> to <project>` | Moves issue ID `<id>` to `<project>` |
1819
| `/project-name deploy <from> to <to>` | Deploy from the `<from>` environment to the `<to>` environment |
1920

20-
Note that if you are using the [GitLab Slack application](https://docs.gitlab.com/ee/user/project/integrations/gitlab_slack_application.html) for
21+
Note that if you are using the [GitLab Slack application](https://docs.gitlab.com/ee/user/project/integrations/gitlab_slack_application.html) for
2122
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).
2223

2324
## Issue commands

lib/gitlab/slash_commands/command.rb

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ class Command < BaseCommand
55
Gitlab::SlashCommands::IssueShow,
66
Gitlab::SlashCommands::IssueNew,
77
Gitlab::SlashCommands::IssueSearch,
8+
Gitlab::SlashCommands::IssueMove,
89
Gitlab::SlashCommands::Deploy
910
].freeze
1011

Lines changed: 45 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,45 @@
1+
module Gitlab
2+
module SlashCommands
3+
class IssueMove < IssueCommand
4+
def self.match(text)
5+
%r{
6+
\A # the beginning of a string
7+
issue\s+move\s+ # the command
8+
\#?(?<iid>\d+)\s+ # the issue id, may preceded by hash sign
9+
(to\s+)? # aid the command to be much more human-ly
10+
(?<project_path>[^\s]+) # named group for id of dest. project
11+
}x.match(text)
12+
end
13+
14+
def self.help_message
15+
'issue move <issue_id> (to)? <project_path>'
16+
end
17+
18+
def self.allowed?(project, user)
19+
can?(user, :admin_issue, project)
20+
end
21+
22+
def execute(match)
23+
old_issue = find_by_iid(match[:iid])
24+
target_project = Project.find_by_full_path(match[:project_path])
25+
26+
unless current_user.can?(:read_project, target_project) && old_issue
27+
return Gitlab::SlashCommands::Presenters::Access.new.not_found
28+
end
29+
30+
new_issue = Issues::MoveService.new(project, current_user)
31+
.execute(old_issue, target_project)
32+
33+
presenter(new_issue).present(old_issue)
34+
rescue Issues::MoveService::MoveError => e
35+
presenter(old_issue).display_move_error(e.message)
36+
end
37+
38+
private
39+
40+
def presenter(issue)
41+
Gitlab::SlashCommands::Presenters::IssueMove.new(issue)
42+
end
43+
end
44+
end
45+
end
Lines changed: 53 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,53 @@
1+
# coding: utf-8
2+
module Gitlab
3+
module SlashCommands
4+
module Presenters
5+
class IssueMove < Presenters::Base
6+
include Presenters::IssueBase
7+
8+
def present(old_issue)
9+
in_channel_response(moved_issue(old_issue))
10+
end
11+
12+
def display_move_error(error)
13+
message = header_with_list("The action was not successful, because:", [error])
14+
15+
ephemeral_response(text: message)
16+
end
17+
18+
private
19+
20+
def moved_issue(old_issue)
21+
{
22+
attachments: [
23+
{
24+
title: "#{@resource.title} · #{@resource.to_reference}",
25+
title_link: resource_url,
26+
author_name: author.name,
27+
author_icon: author.avatar_url,
28+
fallback: "Issue #{@resource.to_reference}: #{@resource.title}",
29+
pretext: pretext(old_issue),
30+
color: color(@resource),
31+
fields: fields,
32+
mrkdwn_in: [
33+
:title,
34+
:pretext,
35+
:text,
36+
:fields
37+
]
38+
}
39+
]
40+
}
41+
end
42+
43+
def pretext(old_issue)
44+
"Moved issue *#{issue_link(old_issue)}* to *#{issue_link(@resource)}*"
45+
end
46+
47+
def issue_link(issue)
48+
"[#{issue.to_reference}](#{project_issue_url(issue.project, issue)})"
49+
end
50+
end
51+
end
52+
end
53+
end

spec/lib/gitlab/slash_commands/command_spec.rb

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -108,5 +108,10 @@
108108

109109
it { is_expected.to eq(Gitlab::SlashCommands::IssueSearch) }
110110
end
111+
112+
context 'IssueMove is triggered' do
113+
let(:params) { { text: 'issue move #78291 to gitlab/gitlab-ci' } }
114+
it { is_expected.to eq(Gitlab::SlashCommands::IssueMove) }
115+
end
111116
end
112117
end
Lines changed: 117 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,117 @@
1+
require 'spec_helper'
2+
3+
describe Gitlab::SlashCommands::IssueMove, service: true do
4+
describe '#match' do
5+
shared_examples_for 'move command' do |text_command|
6+
it 'can be parsed to extract the needed fields' do
7+
match_data = described_class.match(text_command)
8+
9+
expect(match_data['iid']).to eq('123456')
10+
expect(match_data['project_path']).to eq('gitlab/gitlab-ci')
11+
end
12+
end
13+
14+
it_behaves_like 'move command', 'issue move #123456 to gitlab/gitlab-ci'
15+
it_behaves_like 'move command', 'issue move #123456 gitlab/gitlab-ci'
16+
it_behaves_like 'move command', 'issue move #123456 gitlab/gitlab-ci '
17+
it_behaves_like 'move command', 'issue move 123456 to gitlab/gitlab-ci'
18+
it_behaves_like 'move command', 'issue move 123456 gitlab/gitlab-ci'
19+
it_behaves_like 'move command', 'issue move 123456 gitlab/gitlab-ci '
20+
end
21+
22+
describe '#execute' do
23+
set(:user) { create(:user) }
24+
set(:issue) { create(:issue) }
25+
set(:chat_name) { create(:chat_name, user: user) }
26+
set(:project) { issue.project }
27+
set(:other_project) { create(:project, namespace: project.namespace) }
28+
29+
before do
30+
[project, other_project].each { |prj| prj.add_master(user) }
31+
end
32+
33+
subject { described_class.new(project, chat_name) }
34+
35+
def process_message(message)
36+
subject.execute(described_class.match(message))
37+
end
38+
39+
context 'when the user can move the issue' do
40+
context 'when the move fails' do
41+
it 'returns the error message' do
42+
message = "issue move #{issue.iid} #{project.full_path}"
43+
44+
expect(process_message(message)).to include(response_type: :ephemeral,
45+
text: a_string_matching('Cannot move issue'))
46+
end
47+
end
48+
49+
context 'when the move succeeds' do
50+
let(:message) { "issue move #{issue.iid} #{other_project.full_path}" }
51+
52+
it 'moves the issue to the new destination' do
53+
expect { process_message(message) }.to change { Issue.count }.by(1)
54+
55+
new_issue = issue.reload.moved_to
56+
57+
expect(new_issue.state).to eq('opened')
58+
expect(new_issue.project_id).to eq(other_project.id)
59+
expect(new_issue.author_id).to eq(issue.author_id)
60+
61+
expect(issue.state).to eq('closed')
62+
expect(issue.project_id).to eq(project.id)
63+
end
64+
65+
it 'returns the new issue' do
66+
expect(process_message(message))
67+
.to include(response_type: :in_channel,
68+
attachments: [a_hash_including(title_link: a_string_including(other_project.full_path))])
69+
end
70+
71+
it 'mentions the old issue' do
72+
expect(process_message(message))
73+
.to include(attachments: [a_hash_including(pretext: a_string_including(project.full_path))])
74+
end
75+
end
76+
end
77+
78+
context 'when the issue does not exist' do
79+
it 'returns not found' do
80+
message = "issue move #{issue.iid.succ} #{other_project.full_path}"
81+
82+
expect(process_message(message)).to include(response_type: :ephemeral,
83+
text: a_string_matching('not found'))
84+
end
85+
end
86+
87+
context 'when the target project does not exist' do
88+
it 'returns not found' do
89+
message = "issue move #{issue.iid} #{other_project.full_path}/foo"
90+
91+
expect(process_message(message)).to include(response_type: :ephemeral,
92+
text: a_string_matching('not found'))
93+
end
94+
end
95+
96+
context 'when the user cannot see the target project' do
97+
it 'returns not found' do
98+
message = "issue move #{issue.iid} #{other_project.full_path}"
99+
other_project.team.truncate
100+
101+
expect(process_message(message)).to include(response_type: :ephemeral,
102+
text: a_string_matching('not found'))
103+
end
104+
end
105+
106+
context 'when the user does not have the required permissions on the target project' do
107+
it 'returns the error message' do
108+
message = "issue move #{issue.iid} #{other_project.full_path}"
109+
other_project.team.truncate
110+
other_project.team.add_guest(user)
111+
112+
expect(process_message(message)).to include(response_type: :ephemeral,
113+
text: a_string_matching('Cannot move issue'))
114+
end
115+
end
116+
end
117+
end
Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,26 @@
1+
require 'spec_helper'
2+
3+
describe Gitlab::SlashCommands::Presenters::IssueMove do
4+
set(:admin) { create(:admin) }
5+
set(:project) { create(:project) }
6+
set(:other_project) { create(:project) }
7+
set(:old_issue) { create(:issue, project: project) }
8+
set(:new_issue) { Issues::MoveService.new(project, admin).execute(old_issue, other_project) }
9+
let(:attachment) { subject[:attachments].first }
10+
11+
subject { described_class.new(new_issue).present(old_issue) }
12+
13+
it { is_expected.to be_a(Hash) }
14+
15+
it 'shows the new issue' do
16+
expect(subject[:response_type]).to be(:in_channel)
17+
expect(subject).to have_key(:attachments)
18+
expect(attachment[:title]).to start_with(new_issue.title)
19+
expect(attachment[:title_link]).to include(other_project.full_path)
20+
end
21+
22+
it 'mentions the old issue and the new issue in the pretext' do
23+
expect(attachment[:pretext]).to include(project.full_path)
24+
expect(attachment[:pretext]).to include(other_project.full_path)
25+
end
26+
end

0 commit comments

Comments
 (0)