Skip to content
Projects
Groups
Snippets
Help
Loading...
Help
Support
Keyboard shortcuts
?
Submit feedback
Contribute to GitLab
Sign in / Register
Toggle navigation
G
gitlab-ce
Project overview
Project overview
Details
Activity
Releases
Repository
Repository
Files
Commits
Branches
Tags
Contributors
Graph
Compare
Issues
0
Issues
0
List
Boards
Labels
Milestones
Merge Requests
0
Merge Requests
0
Analytics
Analytics
Repository
Value Stream
Wiki
Wiki
Snippets
Snippets
Members
Members
Collapse sidebar
Close sidebar
Activity
Graph
Create a new issue
Commits
Issue Boards
Open sidebar
Jérome Perrin
gitlab-ce
Commits
1df696f5
Commit
1df696f5
authored
Jul 20, 2017
by
Sean McGivern
Browse files
Options
Browse Files
Download
Email Patches
Plain Diff
Move duplicate issue management to a service
parent
3498e825
Changes
13
Hide whitespace changes
Inline
Side-by-side
Showing
13 changed files
with
205 additions
and
86 deletions
+205
-86
app/helpers/system_note_helper.rb
app/helpers/system_note_helper.rb
+2
-1
app/services/issuable_base_service.rb
app/services/issuable_base_service.rb
+1
-22
app/services/issues/base_service.rb
app/services/issues/base_service.rb
+8
-0
app/services/issues/duplicate_service.rb
app/services/issues/duplicate_service.rb
+24
-0
app/services/issues/update_service.rb
app/services/issues/update_service.rb
+10
-8
app/services/quick_actions/interpret_service.rb
app/services/quick_actions/interpret_service.rb
+7
-3
app/services/system_note_service.rb
app/services/system_note_service.rb
+25
-6
app/views/shared/icons/_icon_clone.svg
app/views/shared/icons/_icon_clone.svg
+3
-0
changelogs/unreleased/26372-duplicate-issue-slash-command.yml
...gelogs/unreleased/26372-duplicate-issue-slash-command.yml
+2
-2
spec/services/issues/duplicate_service_spec.rb
spec/services/issues/duplicate_service_spec.rb
+80
-0
spec/services/issues/update_service_spec.rb
spec/services/issues/update_service_spec.rb
+10
-31
spec/services/quick_actions/interpret_service_spec.rb
spec/services/quick_actions/interpret_service_spec.rb
+3
-8
spec/services/system_note_service_spec.rb
spec/services/system_note_service_spec.rb
+30
-5
No files found.
app/helpers/system_note_helper.rb
View file @
1df696f5
...
...
@@ -18,7 +18,8 @@ module SystemNoteHelper
'milestone'
=>
'icon_clock_o'
,
'discussion'
=>
'icon_comment_o'
,
'moved'
=>
'icon_arrow_circle_o_right'
,
'outdated'
=>
'icon_edit'
'outdated'
=>
'icon_edit'
,
'duplicate'
=>
'icon_clone'
}.
freeze
def
icon_for_system_note
(
note
)
...
...
app/services/issuable_base_service.rb
View file @
1df696f5
...
...
@@ -46,10 +46,6 @@ 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
filter_params
(
issuable
)
ability_name
=
:"admin_
#{
issuable
.
to_ability_name
}
"
...
...
@@ -62,7 +58,7 @@ class IssuableBaseService < BaseService
params
.
delete
(
:assignee_ids
)
params
.
delete
(
:assignee_id
)
params
.
delete
(
:due_date
)
params
.
delete
(
:
origin
al_issue_id
)
params
.
delete
(
:
canonic
al_issue_id
)
end
filter_assignee
(
issuable
)
...
...
@@ -214,7 +210,6 @@ 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
...
...
@@ -297,22 +292,6 @@ class IssuableBaseService < BaseService
end
end
def
change_issue_duplicate
(
issuable
)
original_issue_id
=
params
.
delete
(
:original_issue_id
)
return
unless
original_issue_id
begin
original_issue
=
IssuesFinder
.
new
(
current_user
).
find
(
original_issue_id
)
rescue
ActiveRecord
::
RecordNotFound
return
end
note
=
create_issue_duplicate_note
(
issuable
,
original_issue
)
note
.
create_cross_references!
close_service
.
new
(
project
,
current_user
,
{}).
execute
(
issuable
)
original_issue
.
create_award_emoji
(
AwardEmoji
::
UPVOTE_NAME
,
issuable
.
author
)
end
def
toggle_award
(
issuable
)
award
=
params
.
delete
(
:emoji_award
)
if
award
...
...
app/services/issues/base_service.rb
View file @
1df696f5
...
...
@@ -7,6 +7,14 @@ module Issues
issue_data
end
def
reopen_service
Issues
::
ReopenService
end
def
close_service
Issues
::
CloseService
end
private
def
create_assignee_note
(
issue
,
old_assignees
)
...
...
app/services/issues/duplicate_service.rb
0 → 100644
View file @
1df696f5
module
Issues
class
DuplicateService
<
Issues
::
BaseService
def
execute
(
duplicate_issue
,
canonical_issue
)
return
if
canonical_issue
==
duplicate_issue
return
unless
can?
(
current_user
,
:update_issue
,
duplicate_issue
)
return
unless
can?
(
current_user
,
:create_note
,
canonical_issue
)
create_issue_duplicate_note
(
duplicate_issue
,
canonical_issue
)
create_issue_canonical_note
(
canonical_issue
,
duplicate_issue
)
close_service
.
new
(
project
,
current_user
,
{}).
execute
(
duplicate_issue
)
end
private
def
create_issue_duplicate_note
(
duplicate_issue
,
canonical_issue
)
SystemNoteService
.
mark_duplicate_issue
(
duplicate_issue
,
duplicate_issue
.
project
,
current_user
,
canonical_issue
)
end
def
create_issue_canonical_note
(
canonical_issue
,
duplicate_issue
)
SystemNoteService
.
mark_canonical_issue_of_duplicate
(
canonical_issue
,
canonical_issue
.
project
,
current_user
,
duplicate_issue
)
end
end
end
app/services/issues/update_service.rb
View file @
1df696f5
...
...
@@ -5,6 +5,7 @@ module Issues
def
execute
(
issue
)
handle_move_between_iids
(
issue
)
filter_spam_check_params
change_issue_duplicate
(
issue
)
update
(
issue
)
end
...
...
@@ -53,14 +54,6 @@ module Issues
end
end
def
reopen_service
Issues
::
ReopenService
end
def
close_service
Issues
::
CloseService
end
def
handle_move_between_iids
(
issue
)
return
unless
params
[
:move_between_iids
]
...
...
@@ -72,6 +65,15 @@ module Issues
issue
.
move_between
(
issue_before
,
issue_after
)
end
def
change_issue_duplicate
(
issue
)
canonical_issue_id
=
params
.
delete
(
:canonical_issue_id
)
canonical_issue
=
IssuesFinder
.
new
(
current_user
).
find_by
(
id:
canonical_issue_id
)
if
canonical_issue
Issues
::
DuplicateService
.
new
(
project
,
current_user
).
execute
(
issue
,
canonical_issue
)
end
end
private
def
get_issue_if_allowed
(
project
,
iid
)
...
...
app/services/quick_actions/interpret_service.rb
View file @
1df696f5
...
...
@@ -472,6 +472,9 @@ module QuickActions
end
desc
'Mark this issue as a duplicate of another issue'
explanation
do
|
duplicate_reference
|
"Marks this issue as a duplicate of
#{
duplicate_reference
}
."
end
params
'#issue'
condition
do
issuable
.
is_a?
(
Issue
)
&&
...
...
@@ -479,9 +482,10 @@ module QuickActions
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
canonical_issue
=
extract_references
(
duplicate_param
,
:issue
).
first
if
canonical_issue
.
present?
@updates
[
:canonical_issue_id
]
=
canonical_issue
.
id
end
end
...
...
app/services/system_note_service.rb
View file @
1df696f5
...
...
@@ -554,10 +554,10 @@ module SystemNoteService
# Called when a Noteable has been marked as a duplicate of another Issue
#
# noteable - Noteable object
# project - Project owning noteable
# author - User performing the change
#
origin
al_issue - Issue that this is a duplicate of
# noteable
- Noteable object
# project
- Project owning noteable
# author
- User performing the change
#
canonic
al_issue - Issue that this is a duplicate of
#
# Example Note text:
#
...
...
@@ -566,8 +566,27 @@ module SystemNoteService
# "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
)
}
"
def
mark_duplicate_issue
(
noteable
,
project
,
author
,
canonical_issue
)
body
=
"marked this issue as a duplicate of
#{
canonical_issue
.
to_reference
(
project
)
}
"
create_note
(
NoteSummary
.
new
(
noteable
,
project
,
author
,
body
,
action:
'duplicate'
))
end
# Called when a Noteable has been marked as the canonical Issue of a duplicate
#
# noteable - Noteable object
# project - Project owning noteable
# author - User performing the change
# duplicate_issue - Issue that was a duplicate of this
#
# Example Note text:
#
# "marked #1234 as a duplicate of this issue"
#
# "marked other_project#5678 as a duplicate of this issue"
#
# Returns the created Note object
def
mark_canonical_issue_of_duplicate
(
noteable
,
project
,
author
,
duplicate_issue
)
body
=
"marked
#{
duplicate_issue
.
to_reference
(
project
)
}
as a duplicate of this issue"
create_note
(
NoteSummary
.
new
(
noteable
,
project
,
author
,
body
,
action:
'duplicate'
))
end
...
...
app/views/shared/icons/_icon_clone.svg
0 → 100644
View file @
1df696f5
<svg
version=
"1.1"
xmlns=
"http://www.w3.org/2000/svg"
xmlns:xlink=
"http://www.w3.org/1999/xlink"
width=
"14"
height=
"14"
viewBox=
"0 0 14 14"
>
<path
d=
"M13 12.75v-8.5q0-0.102-0.074-0.176t-0.176-0.074h-8.5q-0.102 0-0.176 0.074t-0.074 0.176v8.5q0 0.102 0.074 0.176t0.176 0.074h8.5q0.102 0 0.176-0.074t0.074-0.176zM14 4.25v8.5q0 0.516-0.367 0.883t-0.883 0.367h-8.5q-0.516 0-0.883-0.367t-0.367-0.883v-8.5q0-0.516 0.367-0.883t0.883-0.367h8.5q0.516 0 0.883 0.367t0.367 0.883zM11 1.25v1.25h-1v-1.25q0-0.102-0.074-0.176t-0.176-0.074h-8.5q-0.102 0-0.176 0.074t-0.074 0.176v8.5q0 0.102 0.074 0.176t0.176 0.074h1.25v1h-1.25q-0.516 0-0.883-0.367t-0.367-0.883v-8.5q0-0.516 0.367-0.883t0.883-0.367h8.5q0.516 0 0.883 0.367t0.367 0.883z"
></path>
</svg>
changelogs/unreleased/26372-duplicate-issue-slash-command.yml
View file @
1df696f5
---
title
:
Added /duplicate
slash command
to close a duplicate issue
merge_request
:
title
:
Added /duplicate
quick action
to close a duplicate issue
merge_request
:
12845
author
:
Ryan Scott
spec/services/issues/duplicate_service_spec.rb
0 → 100644
View file @
1df696f5
require
'spec_helper'
describe
Issues
::
DuplicateService
,
services:
true
do
let
(
:user
)
{
create
(
:user
)
}
let
(
:canonical_project
)
{
create
(
:empty_project
)
}
let
(
:duplicate_project
)
{
create
(
:empty_project
)
}
let
(
:canonical_issue
)
{
create
(
:issue
,
project:
canonical_project
)
}
let
(
:duplicate_issue
)
{
create
(
:issue
,
project:
duplicate_project
)
}
subject
{
described_class
.
new
(
duplicate_project
,
user
,
{})
}
describe
'#execute'
do
context
'when the issues passed are the same'
do
it
'does nothing'
do
expect
(
subject
).
not_to
receive
(
:close_service
)
expect
(
SystemNoteService
).
not_to
receive
(
:mark_duplicate_issue
)
expect
(
SystemNoteService
).
not_to
receive
(
:mark_canonical_issue_of_duplicate
)
subject
.
execute
(
duplicate_issue
,
duplicate_issue
)
end
end
context
'when the user cannot update the duplicate issue'
do
before
do
canonical_project
.
add_reporter
(
user
)
end
it
'does nothing'
do
expect
(
subject
).
not_to
receive
(
:close_service
)
expect
(
SystemNoteService
).
not_to
receive
(
:mark_duplicate_issue
)
expect
(
SystemNoteService
).
not_to
receive
(
:mark_canonical_issue_of_duplicate
)
subject
.
execute
(
duplicate_issue
,
canonical_issue
)
end
end
context
'when the user cannot comment on the canonical issue'
do
before
do
duplicate_project
.
add_reporter
(
user
)
end
it
'does nothing'
do
expect
(
subject
).
not_to
receive
(
:close_service
)
expect
(
SystemNoteService
).
not_to
receive
(
:mark_duplicate_issue
)
expect
(
SystemNoteService
).
not_to
receive
(
:mark_canonical_issue_of_duplicate
)
subject
.
execute
(
duplicate_issue
,
canonical_issue
)
end
end
context
'when the user can mark the issue as a duplicate'
do
before
do
canonical_project
.
add_reporter
(
user
)
duplicate_project
.
add_reporter
(
user
)
end
it
'closes the duplicate issue'
do
subject
.
execute
(
duplicate_issue
,
canonical_issue
)
expect
(
duplicate_issue
.
reload
).
to
be_closed
expect
(
canonical_issue
.
reload
).
to
be_open
end
it
'adds a system note to the duplicate issue'
do
expect
(
SystemNoteService
)
.
to
receive
(
:mark_duplicate_issue
).
with
(
duplicate_issue
,
duplicate_project
,
user
,
canonical_issue
)
subject
.
execute
(
duplicate_issue
,
canonical_issue
)
end
it
'adds a system note to the canonical issue'
do
expect
(
SystemNoteService
)
.
to
receive
(
:mark_canonical_issue_of_duplicate
).
with
(
canonical_issue
,
canonical_project
,
user
,
duplicate_issue
)
subject
.
execute
(
duplicate_issue
,
canonical_issue
)
end
end
end
end
spec/services/issues/update_service_spec.rb
View file @
1df696f5
...
...
@@ -492,43 +492,22 @@ describe Issues::UpdateService, services: true do
end
context
'duplicate issue'
do
let
(
:
origin
al_issue
)
{
create
(
:issue
,
project:
project
)
}
let
(
:
canonic
al_issue
)
{
create
(
:issue
,
project:
project
)
}
context
'invalid original_issue_id'
do
before
do
update_issue
(
original_issue_id:
123456789
)
end
it
'does not close the issue'
do
expect
(
issue
.
reload
).
not_to
be_closed
end
context
'invalid canonical_issue_id'
do
it
'does not call the duplicate service'
do
expect
(
Issues
::
DuplicateService
).
not_to
receive
(
:new
)
it
'does not create a system note'
do
note
=
find_note
(
"marked this issue as a duplicate of
#{
original_issue
.
to_reference
}
"
)
expect
(
note
).
to
be_nil
end
it
'does not upvote the issue on behalf of the author'
do
expect
(
original_issue
).
not_to
be_awarded_emoji
(
AwardEmoji
::
UPVOTE_NAME
,
issue
.
author
)
update_issue
(
canonical_issue_id:
123456789
)
end
end
context
'valid original_issue_id'
do
before
do
update_issue
(
original_issue_id:
original_issue
.
id
)
end
it
'closes the issue'
do
expect
(
issue
.
reload
).
to
be_closed
end
it
'creates a system note that this issue is a duplicate'
do
note
=
find_note
(
"marked this issue as a duplicate of
#{
original_issue
.
to_reference
}
"
)
expect
(
note
).
not_to
be_nil
end
context
'valid canonical_issue_id'
do
it
'calls the duplicate service with both issues'
do
expect_any_instance_of
(
Issues
::
DuplicateService
)
.
to
receive
(
:execute
).
with
(
issue
,
canonical_issue
)
it
'upvotes the issue on behalf of the author'
do
expect
(
original_issue
).
to
be_awarded_emoji
(
AwardEmoji
::
UPVOTE_NAME
,
issue
.
author
)
update_issue
(
canonical_issue_id:
canonical_issue
.
id
)
end
end
end
...
...
spec/services/quick_actions/interpret_service_spec.rb
View file @
1df696f5
...
...
@@ -262,11 +262,11 @@ describe QuickActions::InterpretService, services: true do
end
shared_examples
'duplicate command'
do
it
'fetches issue and populates
origin
al_issue_id if content contains /duplicate issue_reference'
do
it
'fetches issue and populates
canonic
al_issue_id if content contains /duplicate issue_reference'
do
issue_duplicate
# populate the issue
_
,
updates
=
service
.
execute
(
content
,
issuable
)
expect
(
updates
).
to
eq
(
origin
al_issue_id:
issue_duplicate
.
id
)
expect
(
updates
).
to
eq
(
canonic
al_issue_id:
issue_duplicate
.
id
)
end
end
...
...
@@ -660,11 +660,6 @@ describe QuickActions::InterpretService, services: true do
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
}
...
...
@@ -679,7 +674,7 @@ describe QuickActions::InterpretService, services: true do
end
it_behaves_like
'empty command'
do
let
(
:content
)
{
'/duplicate imaginary#1234'
}
let
(
:content
)
{
"/duplicate imaginary#1234"
}
let
(
:issuable
)
{
issue
}
end
...
...
spec/services/system_note_service_spec.rb
View file @
1df696f5
...
...
@@ -1103,27 +1103,52 @@ describe SystemNoteService, services: true do
end
describe
'.mark_duplicate_issue'
do
subject
{
described_class
.
mark_duplicate_issue
(
noteable
,
project
,
author
,
origin
al_issue
)
}
subject
{
described_class
.
mark_duplicate_issue
(
noteable
,
project
,
author
,
canonic
al_issue
)
}
context
'within the same project'
do
let
(
:
origin
al_issue
)
{
create
(
:issue
,
project:
project
)
}
let
(
:
canonic
al_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
#{
origin
al_issue
.
to_reference
}
"
}
it
{
expect
(
subject
.
note
).
to
eq
"marked this issue as a duplicate of
#{
canonic
al_issue
.
to_reference
}
"
}
end
context
'across different projects'
do
let
(
:other_project
)
{
create
(
:empty_project
)
}
let
(
:
origin
al_issue
)
{
create
(
:issue
,
project:
other_project
)
}
let
(
:
canonic
al_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
)
}
"
}
it
{
expect
(
subject
.
note
).
to
eq
"marked this issue as a duplicate of
#{
canonical_issue
.
to_reference
(
project
)
}
"
}
end
end
describe
'.mark_canonical_issue_of_duplicate'
do
subject
{
described_class
.
mark_canonical_issue_of_duplicate
(
noteable
,
project
,
author
,
duplicate_issue
)
}
context
'within the same project'
do
let
(
:duplicate_issue
)
{
create
(
:issue
,
project:
project
)
}
it_behaves_like
'a system note'
do
let
(
:action
)
{
'duplicate'
}
end
it
{
expect
(
subject
.
note
).
to
eq
"marked
#{
duplicate_issue
.
to_reference
}
as a duplicate of this issue"
}
end
context
'across different projects'
do
let
(
:other_project
)
{
create
(
:empty_project
)
}
let
(
:duplicate_issue
)
{
create
(
:issue
,
project:
other_project
)
}
it_behaves_like
'a system note'
do
let
(
:action
)
{
'duplicate'
}
end
it
{
expect
(
subject
.
note
).
to
eq
"marked
#{
duplicate_issue
.
to_reference
(
project
)
}
as a duplicate of this issue"
}
end
end
end
Write
Preview
Markdown
is supported
0%
Try again
or
attach a new file
Attach a file
Cancel
You are about to add
0
people
to the discussion. Proceed with caution.
Finish editing this message first!
Cancel
Please
register
or
sign in
to comment