diff options
author | Ryan Scott <ryan@ryan-scott.me> | 2017-03-30 10:39:06 +0900 |
---|---|---|
committer | Sean McGivern <sean@gitlab.com> | 2017-07-20 15:33:24 +0100 |
commit | 01c9488f4a559063eba77074ba2d369de87b8018 (patch) | |
tree | ffd18d3ebd174cf47ab9eb71cbacd940d4cf2831 | |
parent | b6555693a8e445405c5746b7c76c9f603395bba2 (diff) | |
download | gitlab-ce-01c9488f4a559063eba77074ba2d369de87b8018.tar.gz |
Added slash command to close an issue as a duplicate. Closes #26372
-rw-r--r-- | app/models/system_note_metadata.rb | 2 | ||||
-rw-r--r-- | app/services/issuable_base_service.rb | 22 | ||||
-rw-r--r-- | app/services/quick_actions/interpret_service.rb | 14 | ||||
-rw-r--r-- | app/services/system_note_service.rb | 19 | ||||
-rw-r--r-- | changelogs/unreleased/26372-duplicate-issue-slash-command.yml | 4 | ||||
-rw-r--r-- | doc/user/project/quick_actions.md | 1 | ||||
-rw-r--r-- | spec/features/issues/user_uses_slash_commands_spec.rb | 41 | ||||
-rw-r--r-- | spec/services/issues/update_service_spec.rb | 56 | ||||
-rw-r--r-- | spec/services/quick_actions/interpret_service_spec.rb | 36 | ||||
-rw-r--r-- | spec/services/system_note_service_spec.rb | 25 |
10 files changed, 219 insertions, 1 deletions
diff --git a/app/models/system_note_metadata.rb b/app/models/system_note_metadata.rb index 414c95f7705..1ffdd285b91 100644 --- a/app/models/system_note_metadata.rb +++ b/app/models/system_note_metadata.rb @@ -2,7 +2,7 @@ class SystemNoteMetadata < ActiveRecord::Base ICON_TYPES = %w[ commit description merge confidential visible label assignee cross_reference title time_tracking branch milestone discussion task moved opened closed merged - outdated + outdated duplicate ].freeze validates :note, presence: true diff --git a/app/services/issuable_base_service.rb b/app/services/issuable_base_service.rb index 9078b1f0983..c7e646222bb 100644 --- a/app/services/issuable_base_service.rb +++ b/app/services/issuable_base_service.rb @@ -46,6 +46,14 @@ class IssuableBaseService < BaseService SystemNoteService.change_time_spent(issuable, issuable.project, current_user) end + def create_issue_duplicate_note(issuable, original_issue) + SystemNoteService.mark_duplicate_issue(issuable, issuable.project, current_user, original_issue) + end + + def create_cross_reference_note(noteable, mentioner) + SystemNoteService.cross_reference(noteable, mentioner, current_user) + end + def filter_params(issuable) ability_name = :"admin_#{issuable.to_ability_name}" @@ -58,6 +66,7 @@ class IssuableBaseService < BaseService params.delete(:assignee_ids) params.delete(:assignee_id) params.delete(:due_date) + params.delete(:original_issue_id) end filter_assignee(issuable) @@ -209,6 +218,7 @@ class IssuableBaseService < BaseService change_state(issuable) change_subscription(issuable) change_todo(issuable) + change_issue_duplicate(issuable) toggle_award(issuable) filter_params(issuable) old_labels = issuable.labels.to_a @@ -291,6 +301,18 @@ class IssuableBaseService < BaseService end end + def change_issue_duplicate(issuable) + original_issue_id = params.delete(:original_issue_id) + return if original_issue_id.nil? + + original_issue = IssuesFinder.new(current_user).find(original_issue_id) + if original_issue.present? + create_issue_duplicate_note(issuable, original_issue) + close_service.new(project, current_user, {}).execute(issuable) + create_cross_reference_note(original_issue, issuable) + end + end + def toggle_award(issuable) award = params.delete(:emoji_award) if award diff --git a/app/services/quick_actions/interpret_service.rb b/app/services/quick_actions/interpret_service.rb index 6f82159e6c7..3eecf0b5545 100644 --- a/app/services/quick_actions/interpret_service.rb +++ b/app/services/quick_actions/interpret_service.rb @@ -471,6 +471,20 @@ module QuickActions end end + desc 'Mark this issue as a duplicate of another issue' + params '#issue' + condition do + issuable.is_a?(Issue) && + issuable.persisted? && + current_user.can?(:"update_#{issuable.to_ability_name}", issuable) + end + command :duplicate do |duplicate_param| + original_issue = extract_references(duplicate_param, :issue).first + if original_issue.present? && original_issue != issuable + @updates[:original_issue_id] = original_issue.id + end + end + def extract_users(params) return [] if params.nil? diff --git a/app/services/system_note_service.rb b/app/services/system_note_service.rb index da0f21d449a..2e5e904c43d 100644 --- a/app/services/system_note_service.rb +++ b/app/services/system_note_service.rb @@ -552,6 +552,25 @@ module SystemNoteService create_note(NoteSummary.new(noteable, project, author, body, action: 'moved')) end + # Called when a Notable has been marked as a duplicate of another Issue + # + # noteable - Noteable object + # project - Project owning noteable + # author - User performing the change + # original_issue - Issue that this is a duplicate of + # + # Example Note text: + # + # "marked this issue as a duplicate of #1234" + # + # "marked this issue as a duplicate of other_project#5678" + # + # Returns the created Note object + def mark_duplicate_issue(noteable, project, author, original_issue) + body = "marked this issue as a duplicate of #{original_issue.to_reference(project)}" + create_note(NoteSummary.new(noteable, project, author, body, action: 'duplicate')) + end + private def notes_for_mentioner(mentioner, noteable, notes) diff --git a/changelogs/unreleased/26372-duplicate-issue-slash-command.yml b/changelogs/unreleased/26372-duplicate-issue-slash-command.yml new file mode 100644 index 00000000000..079ebe59f98 --- /dev/null +++ b/changelogs/unreleased/26372-duplicate-issue-slash-command.yml @@ -0,0 +1,4 @@ +--- +title: Added /duplicate slash command to close a duplicate issue +merge_request: +author: Ryan Scott diff --git a/doc/user/project/quick_actions.md b/doc/user/project/quick_actions.md index 19b51c83222..ce4dd4e99d5 100644 --- a/doc/user/project/quick_actions.md +++ b/doc/user/project/quick_actions.md @@ -37,3 +37,4 @@ do. | `/target_branch <Branch Name>` | Set target branch for current merge request | | `/award :emoji:` | Toggle award for :emoji: | | `/board_move ~column` | Move issue to column on the board | +| `/duplicate #issue` | Closes this issue and marks it as a duplicate of another issue | diff --git a/spec/features/issues/user_uses_slash_commands_spec.rb b/spec/features/issues/user_uses_slash_commands_spec.rb index 1cd1f016674..d5de060b033 100644 --- a/spec/features/issues/user_uses_slash_commands_spec.rb +++ b/spec/features/issues/user_uses_slash_commands_spec.rb @@ -134,5 +134,46 @@ feature 'Issues > User uses quick actions', feature: true, js: true do expect(page).not_to have_content '/wip' end end + + describe 'mark issue as duplicate' do + let(:issue) { create(:issue, project: project) } + let(:original_issue) { create(:issue, project: project) } + + context 'when the current user can update issues' do + it 'does not create a note, and marks the issue as a duplicate' do + write_note("/duplicate ##{original_issue.to_reference}") + + expect(page).not_to have_content "/duplicate #{original_issue.to_reference}" + expect(page).to have_content 'Commands applied' + expect(page).to have_content "marked this issue as a duplicate of #{original_issue.to_reference}" + + issue.reload + + expect(issue.closed?).to be_truthy + end + end + + context 'when the current user cannot update the issue' do + let(:guest) { create(:user) } + before do + project.team << [guest, :guest] + logout + login_with(guest) + visit namespace_project_issue_path(project.namespace, project, issue) + end + + it 'does not create a note, and does not mark the issue as a duplicate' do + write_note("/duplicate ##{original_issue.to_reference}") + + expect(page).to have_content "/duplicate ##{original_issue.to_reference}" + expect(page).not_to have_content 'Commands applied' + expect(page).not_to have_content "marked this issue as a duplicate of #{original_issue.to_reference}" + + issue.reload + + expect(issue.closed?).to be_falsey + end + end + end end end diff --git a/spec/services/issues/update_service_spec.rb b/spec/services/issues/update_service_spec.rb index d0b991f19ab..3e7abf85106 100644 --- a/spec/services/issues/update_service_spec.rb +++ b/spec/services/issues/update_service_spec.rb @@ -491,6 +491,62 @@ describe Issues::UpdateService, services: true do include_examples 'updating mentions', Issues::UpdateService end + context 'duplicate issue' do + let(:issues_finder) { spy(:issues_finder) } + let(:close_service) { spy(:close_service) } + + before do + allow(IssuesFinder).to receive(:new).and_return(issues_finder) + allow(Issues::CloseService).to receive(:new).and_return(close_service) + allow(SystemNoteService).to receive(:cross_reference) + allow(SystemNoteService).to receive(:mark_duplicate_issue) + end + + context 'invalid original_issue_id' do + let(:original_issue_id) { double } + before { update_issue({ original_issue_id: original_issue_id }) } + + it 'finds the root issue' do + expect(issues_finder).to have_received(:find).with(original_issue_id) + end + + it 'does not close the issue' do + expect(close_service).not_to have_received(:execute) + end + + it 'does not create system notes' do + expect(SystemNoteService).not_to have_received(:cross_reference) + expect(SystemNoteService).not_to have_received(:mark_duplicate_issue) + end + end + + context 'valid original_issue_id' do + let(:original_issue) { create(:issue, project: project) } + let(:original_issue_id) { double } + + before do + allow(issues_finder).to receive(:find).and_return(original_issue) + update_issue({ original_issue_id: original_issue_id }) + end + + it 'finds the root issue' do + expect(issues_finder).to have_received(:find).with(original_issue_id) + end + + it 'closes the issue' do + expect(close_service).to have_received(:execute).with(issue) + end + + it 'creates a system note that this issue is a duplicate' do + expect(SystemNoteService).to have_received(:mark_duplicate_issue).with(issue, project, user, original_issue) + end + + it 'creates a cross reference system note in the other issue' do + expect(SystemNoteService).to have_received(:cross_reference).with(original_issue, issue, user) + end + end + end + include_examples 'issuable update service' do let(:open_issuable) { issue } let(:closed_issuable) { create(:closed_issue, project: project) } diff --git a/spec/services/quick_actions/interpret_service_spec.rb b/spec/services/quick_actions/interpret_service_spec.rb index a2db3f68ff7..3e4aa66756c 100644 --- a/spec/services/quick_actions/interpret_service_spec.rb +++ b/spec/services/quick_actions/interpret_service_spec.rb @@ -261,6 +261,17 @@ describe QuickActions::InterpretService, services: true do end end + shared_examples 'duplicate command' do + let(:issue_duplicate) { create(:issue, project: project) } + + it 'fetches issue and populates original_issue_id if content contains /duplicate issue_reference' do + issue_duplicate # populate the issue + _, updates = service.execute(content, issuable) + + expect(updates).to eq(original_issue_id: issue_duplicate.id) + end + end + it_behaves_like 'reopen command' do let(:content) { '/reopen' } let(:issuable) { issue } @@ -644,6 +655,26 @@ describe QuickActions::InterpretService, services: true do let(:issuable) { issue } end + it_behaves_like 'duplicate command' do + let(:content) { "/duplicate #{issue_duplicate.to_reference}" } + let(:issuable) { issue } + end + + it_behaves_like 'empty command' do + let(:content) { '/duplicate #{issue.to_reference}' } + let(:issuable) { issue } + end + + it_behaves_like 'empty command' do + let(:content) { '/duplicate' } + let(:issuable) { issue } + end + + it_behaves_like 'empty command' do + let(:content) { '/duplicate imaginary#1234' } + let(:issuable) { issue } + end + context 'when current_user cannot :admin_issue' do let(:visitor) { create(:user) } let(:issue) { create(:issue, project: project, author: visitor) } @@ -693,6 +724,11 @@ describe QuickActions::InterpretService, services: true do let(:content) { '/remove_due_date' } let(:issuable) { issue } end + + it_behaves_like 'empty command' do + let(:content) { '/duplicate #{issue.to_reference}' } + let(:issuable) { issue } + end end context '/award command' do diff --git a/spec/services/system_note_service_spec.rb b/spec/services/system_note_service_spec.rb index 60477b8e9ba..db120889119 100644 --- a/spec/services/system_note_service_spec.rb +++ b/spec/services/system_note_service_spec.rb @@ -1101,4 +1101,29 @@ describe SystemNoteService, services: true do expect(subject.note).to include(diffs_project_merge_request_url(project, merge_request, diff_id: diff_id, anchor: line_code)) end end + + describe '.mark_duplicate_issue' do + subject { described_class.mark_duplicate_issue(noteable, project, author, original_issue) } + + context 'within the same project' do + let(:original_issue) { create(:issue, project: project) } + + it_behaves_like 'a system note' do + let(:action) { 'duplicate' } + end + + it { expect(subject.note).to eq "marked this issue as a duplicate of #{original_issue.to_reference}" } + end + + context 'across different projects' do + let(:other_project) { create(:empty_project) } + let(:original_issue) { create(:issue, project: other_project) } + + it_behaves_like 'a system note' do + let(:action) { 'duplicate' } + end + + it { expect(subject.note).to eq "marked this issue as a duplicate of #{original_issue.to_reference(project)}" } + end + end end |