From b53d9f8a0ec6c044548fa21a04ba227a0c46d43a Mon Sep 17 00:00:00 2001 From: Grzegorz Bizon Date: Mon, 15 Feb 2016 14:41:40 +0100 Subject: Add scaffold of service that moves issue to another project --- app/services/issues/move_service.rb | 41 +++++++++++++++++++++++++++++++ spec/services/issues/move_service_spec.rb | 22 +++++++++++++++++ 2 files changed, 63 insertions(+) create mode 100644 app/services/issues/move_service.rb create mode 100644 spec/services/issues/move_service_spec.rb diff --git a/app/services/issues/move_service.rb b/app/services/issues/move_service.rb new file mode 100644 index 00000000000..85c0d6192e4 --- /dev/null +++ b/app/services/issues/move_service.rb @@ -0,0 +1,41 @@ +module Issues + class MoveService < Issues::BaseService + def execute(issue_old, project_new) + @issue_old = issue_old + @issue_new = issue_old.dup + @project_new = project_new + + open_new_issue + rewrite_notes + close_old_issue + notify_participants + + @issue_new + end + + private + + def open_new_issue + @issue_new.project = @project_new + @issue_new.save! + + add_note_moved_from + end + + def rewrite_notes + end + + def close_old_issue + add_note_moved_to + end + + def notify_participants + end + + def add_note_moved_from + end + + def add_note_moved_to + end + end +end diff --git a/spec/services/issues/move_service_spec.rb b/spec/services/issues/move_service_spec.rb new file mode 100644 index 00000000000..27081e76d48 --- /dev/null +++ b/spec/services/issues/move_service_spec.rb @@ -0,0 +1,22 @@ +require 'spec_helper' + +describe Issues::MoveService, services: true do + let(:user) { create(:user) } + let(:issue) { create(:issue, title: 'Some issue', description: 'Some issue description') } + let(:current_project) { issue.project } + let(:new_project) { create(:project) } + + before do + current_project.team << [user, :master] + end + + describe '#execute' do + let!(:new_issue) do + described_class.new(current_project, user).execute(issue, new_project) + end + + it 'should create a new issue in a new project' do + expect(new_issue.project).to eq new_project + end + end +end -- cgit v1.2.1 From 69b89d4ec8f9788dc11f30cdf1f782d316ab252c Mon Sep 17 00:00:00 2001 From: Grzegorz Bizon Date: Mon, 15 Feb 2016 15:14:57 +0100 Subject: Add new system note used when issue has been moved --- app/services/issues/move_service.rb | 1 + app/services/system_note_service.rb | 17 +++++++++++++++++ spec/services/issues/move_service_spec.rb | 4 ++++ spec/services/system_note_service_spec.rb | 20 ++++++++++++++++++++ 4 files changed, 42 insertions(+) diff --git a/app/services/issues/move_service.rb b/app/services/issues/move_service.rb index 85c0d6192e4..49ec9609e1f 100644 --- a/app/services/issues/move_service.rb +++ b/app/services/issues/move_service.rb @@ -36,6 +36,7 @@ module Issues end def add_note_moved_to + SystemNoteService.issue_moved_to_another_project(@issue_old, @project, @project_new, @current_user) end end end diff --git a/app/services/system_note_service.rb b/app/services/system_note_service.rb index 58a861ee08e..95379d260f1 100644 --- a/app/services/system_note_service.rb +++ b/app/services/system_note_service.rb @@ -387,4 +387,21 @@ class SystemNoteService body = "Marked the task **#{new_task.source}** as #{status_label}" create_note(noteable: noteable, project: project, author: author, note: body) end + + # Called when issue has been moved to another project + # + # issue - Issue that has been moved to another project + # project_from - Source project of the issue + # project_to - Destination project for the issue + # author - User performing the move + # + # Example Note text: + # + # "This issue has been moved to SomeNamespace / SomeProject" + # + # Returns the created Note object + def self.issue_moved_to_another_project(issue, project_from, project_to, author) + body = "This issue has been moved to #{project_to.to_reference} by #{author.to_reference}" + create_note(noteable: issue, project: project_from, author: author, note: body) + end end diff --git a/spec/services/issues/move_service_spec.rb b/spec/services/issues/move_service_spec.rb index 27081e76d48..25dacb2068b 100644 --- a/spec/services/issues/move_service_spec.rb +++ b/spec/services/issues/move_service_spec.rb @@ -18,5 +18,9 @@ describe Issues::MoveService, services: true do it 'should create a new issue in a new project' do expect(new_issue.project).to eq new_project end + + it 'should add system note to old issue' do + expect(issue.notes.last.note).to match /This issue has been moved to/ + end end end diff --git a/spec/services/system_note_service_spec.rb b/spec/services/system_note_service_spec.rb index 5dcc39f5fdc..9074d3dbe19 100644 --- a/spec/services/system_note_service_spec.rb +++ b/spec/services/system_note_service_spec.rb @@ -441,6 +441,26 @@ describe SystemNoteService, services: true do end end + describe '.issue_moved_to_another_project' do + subject do + described_class.issue_moved_to_another_project(noteable, project, new_project, author) + end + + let(:new_project) { create(:project) } + + it 'should notify about issue being moved' do + expect(subject.note).to match /This issue has been moved to/ + end + + it 'should mention destination project' do + expect(subject.note).to include new_project.to_reference + end + + it 'should mention author of that change' do + expect(subject.note).to include author.to_reference + end + end + include JiraServiceHelper describe 'JIRA integration' do -- cgit v1.2.1 From 9882802a8b70e998ff6850a3d096b3b52730ab85 Mon Sep 17 00:00:00 2001 From: Grzegorz Bizon Date: Tue, 16 Feb 2016 11:47:00 +0100 Subject: Improve system notes that are added when issue is moved --- app/services/issues/move_service.rb | 4 ++- app/services/system_note_service.rb | 25 +++++++++------ spec/services/issues/move_service_spec.rb | 6 +++- spec/services/system_note_service_spec.rb | 51 +++++++++++++++++++++++++------ 4 files changed, 66 insertions(+), 20 deletions(-) diff --git a/app/services/issues/move_service.rb b/app/services/issues/move_service.rb index 49ec9609e1f..e2ab06ac332 100644 --- a/app/services/issues/move_service.rb +++ b/app/services/issues/move_service.rb @@ -4,6 +4,7 @@ module Issues @issue_old = issue_old @issue_new = issue_old.dup @project_new = project_new + @project_old = @project open_new_issue rewrite_notes @@ -33,10 +34,11 @@ module Issues end def add_note_moved_from + SystemNoteService.noteable_moved(:from, @issue_new, @project_new, @issue_old, @current_user) end def add_note_moved_to - SystemNoteService.issue_moved_to_another_project(@issue_old, @project, @project_new, @current_user) + SystemNoteService.noteable_moved(:to, @issue_old, @project_old, @issue_new, @current_user) end end end diff --git a/app/services/system_note_service.rb b/app/services/system_note_service.rb index 95379d260f1..a8d6555dec3 100644 --- a/app/services/system_note_service.rb +++ b/app/services/system_note_service.rb @@ -3,6 +3,7 @@ # Used for creating system notes (e.g., when a user references a merge request # from an issue, an issue's assignee changes, an issue is closed, etc.) class SystemNoteService + extend GitlabMarkdownHelper # Called when commits are added to a Merge Request # # noteable - Noteable object @@ -388,20 +389,26 @@ class SystemNoteService create_note(noteable: noteable, project: project, author: author, note: body) end - # Called when issue has been moved to another project + # Called when noteable has been moved to another project # - # issue - Issue that has been moved to another project - # project_from - Source project of the issue - # project_to - Destination project for the issue - # author - User performing the move + # direction - symbol, :to or :from + # noteable - Noteable object + # noteable_ref - Referenced noteable + # author - User performing the move # # Example Note text: # - # "This issue has been moved to SomeNamespace / SomeProject" + # "Moved to project_new/#11" # # Returns the created Note object - def self.issue_moved_to_another_project(issue, project_from, project_to, author) - body = "This issue has been moved to #{project_to.to_reference} by #{author.to_reference}" - create_note(noteable: issue, project: project_from, author: author, note: body) + def self.noteable_moved(direction, noteable, project, noteable_ref, author) + unless [:to, :from].include?(direction) + raise StandardError, "Invalid direction `#{direction}`" + end + + cross_reference = cross_project_reference(noteable_ref.project, noteable_ref) + + body = "Moved #{direction} #{cross_reference}" + create_note(noteable: noteable, project: project, author: author, note: body) end end diff --git a/spec/services/issues/move_service_spec.rb b/spec/services/issues/move_service_spec.rb index 25dacb2068b..569e155e617 100644 --- a/spec/services/issues/move_service_spec.rb +++ b/spec/services/issues/move_service_spec.rb @@ -20,7 +20,11 @@ describe Issues::MoveService, services: true do end it 'should add system note to old issue' do - expect(issue.notes.last.note).to match /This issue has been moved to/ + expect(issue.notes.last.note).to match /^Moved to/ + end + + it 'should add system note to new issue' do + expect(new_issue.notes.last.note).to match /^Moved from/ end end end diff --git a/spec/services/system_note_service_spec.rb b/spec/services/system_note_service_spec.rb index 9074d3dbe19..e0ae49ab37c 100644 --- a/spec/services/system_note_service_spec.rb +++ b/spec/services/system_note_service_spec.rb @@ -441,23 +441,56 @@ describe SystemNoteService, services: true do end end - describe '.issue_moved_to_another_project' do + describe '.noteable_moved' do + let(:new_project) { create(:project) } + let(:new_noteable) { create(:issue, project: new_project) } + subject do - described_class.issue_moved_to_another_project(noteable, project, new_project, author) + described_class.noteable_moved(direction, noteable, project, new_noteable, author) end - let(:new_project) { create(:project) } + shared_examples 'cross project mentionable' do + include GitlabMarkdownHelper + + it 'should contain cross reference to new noteable' do + expect(subject.note).to include cross_project_reference(new_project, new_noteable) + end + + it 'should mention referenced noteable' do + expect(subject.note).to include new_noteable.to_reference + end + + it 'should mention referenced project' do + expect(subject.note).to include new_project.to_reference + end + end + + context 'moved to' do + let(:direction) { :to } + + it_behaves_like 'cross project mentionable' - it 'should notify about issue being moved' do - expect(subject.note).to match /This issue has been moved to/ + it 'should notify about noteable being moved to' do + expect(subject.note).to match /Moved to/ + end end - it 'should mention destination project' do - expect(subject.note).to include new_project.to_reference + context 'moved from' do + let(:direction) { :from } + + it_behaves_like 'cross project mentionable' + + it 'should notify about noteable being moved from' do + expect(subject.note).to match /Moved from/ + end end - it 'should mention author of that change' do - expect(subject.note).to include author.to_reference + context 'invalid direction' do + let (:direction) { :invalid } + + it 'should raise error' do + expect { subject }.to raise_error StandardError, /Invalid direction/ + end end end -- cgit v1.2.1 From 5dea8fb9eda2490e5a05221afd15f1ddd6d79fe5 Mon Sep 17 00:00:00 2001 From: Grzegorz Bizon Date: Tue, 16 Feb 2016 14:05:11 +0100 Subject: Add mock-up for interface being used to move issue --- app/views/shared/issuable/_form.html.haml | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/app/views/shared/issuable/_form.html.haml b/app/views/shared/issuable/_form.html.haml index d5a4aad05d9..16412581ccb 100644 --- a/app/views/shared/issuable/_form.html.haml +++ b/app/views/shared/issuable/_form.html.haml @@ -67,6 +67,14 @@ - if can? current_user, :admin_label, issuable.project = link_to 'Create new label', new_namespace_project_label_path(issuable.project.namespace, issuable.project), target: :blank +- if issuable.is_a?(Issue) + %hr + .form-group + = f.label :move_to_project_id, 'Move', class: 'control-label' + .col-sm-10 + = project_select_tag("#{issuable.class.model_name.param_key}[move_to_project_id]", + placeholder: 'Select project', class: 'custom-form-control') + - if issuable.is_a?(MergeRequest) %hr - if @merge_request.new_record? -- cgit v1.2.1 From 11f817b3fda3f77da5fdfda787816fd36d4d98a6 Mon Sep 17 00:00:00 2001 From: Grzegorz Bizon Date: Wed, 17 Feb 2016 14:45:32 +0100 Subject: Add ability to choose id attribute in project select helper --- app/assets/javascripts/project_select.js.coffee | 5 +++-- app/views/shared/issuable/_form.html.haml | 2 +- 2 files changed, 4 insertions(+), 3 deletions(-) diff --git a/app/assets/javascripts/project_select.js.coffee b/app/assets/javascripts/project_select.js.coffee index be8ab9b428d..5b6a71714b2 100644 --- a/app/assets/javascripts/project_select.js.coffee +++ b/app/assets/javascripts/project_select.js.coffee @@ -4,6 +4,7 @@ class @ProjectSelect @groupId = $(select).data('group-id') @includeGroups = $(select).data('include-groups') @orderBy = $(select).data('order-by') || 'id' + @selectId = $(select).data('select-id') || 'web_url' placeholder = "Search for project" placeholder += " or group" if @includeGroups @@ -31,8 +32,8 @@ class @ProjectSelect else Api.projects query.term, @orderBy, projectsCallback - id: (project) -> - project.web_url + id: (project) => + project[@selectId] text: (project) -> project.name_with_namespace || project.name diff --git a/app/views/shared/issuable/_form.html.haml b/app/views/shared/issuable/_form.html.haml index 16412581ccb..22540cbfe69 100644 --- a/app/views/shared/issuable/_form.html.haml +++ b/app/views/shared/issuable/_form.html.haml @@ -73,7 +73,7 @@ = f.label :move_to_project_id, 'Move', class: 'control-label' .col-sm-10 = project_select_tag("#{issuable.class.model_name.param_key}[move_to_project_id]", - placeholder: 'Select project', class: 'custom-form-control') + placeholder: 'Select project', class: 'custom-form-control', data: { 'select-id' => 'id' }) - if issuable.is_a?(MergeRequest) %hr -- cgit v1.2.1 From c8e7d1ed8e3eafcc8234a0e6f443bf85369c8de1 Mon Sep 17 00:00:00 2001 From: Grzegorz Bizon Date: Wed, 17 Feb 2016 15:59:25 +0100 Subject: Add issue move implementation to controller --- app/controllers/projects/issues_controller.rb | 5 ++++ app/services/issues/move_service.rb | 29 +++++++++++++++++--- spec/services/issues/move_service_spec.rb | 38 ++++++++++++++++++++------- spec/services/system_note_service_spec.rb | 2 +- 4 files changed, 59 insertions(+), 15 deletions(-) diff --git a/app/controllers/projects/issues_controller.rb b/app/controllers/projects/issues_controller.rb index b0a03ee45cc..e3486f576c0 100644 --- a/app/controllers/projects/issues_controller.rb +++ b/app/controllers/projects/issues_controller.rb @@ -88,6 +88,11 @@ class Projects::IssuesController < Projects::ApplicationController def update @issue = Issues::UpdateService.new(project, current_user, issue_params).execute(issue) + move_service = Issues::MoveService.new(project, current_user, params.require(:issue).permit!, @issue) + + if move_service.move? + @issue = move_service.execute + end respond_to do |format| format.js diff --git a/app/services/issues/move_service.rb b/app/services/issues/move_service.rb index e2ab06ac332..935bd8a87f7 100644 --- a/app/services/issues/move_service.rb +++ b/app/services/issues/move_service.rb @@ -1,11 +1,20 @@ module Issues class MoveService < Issues::BaseService - def execute(issue_old, project_new) - @issue_old = issue_old - @issue_new = issue_old.dup - @project_new = project_new + def initialize(project, current_user, params, issue) + super(project, current_user, params) + + @issue_old = issue + @issue_new = @issue_old.dup @project_old = @project + if params['move_to_project_id'] + @project_new = Project.find(params['move_to_project_id']) + end + end + + def execute + return unless move? + open_new_issue rewrite_notes close_old_issue @@ -14,8 +23,20 @@ module Issues @issue_new end + def move? + return false unless @project_new + return false unless @issue_new + return false unless can_move? + + true + end + private + def can_move? + true + end + def open_new_issue @issue_new.project = @project_new @issue_new.save! diff --git a/spec/services/issues/move_service_spec.rb b/spec/services/issues/move_service_spec.rb index 569e155e617..412a817208e 100644 --- a/spec/services/issues/move_service_spec.rb +++ b/spec/services/issues/move_service_spec.rb @@ -5,26 +5,44 @@ describe Issues::MoveService, services: true do let(:issue) { create(:issue, title: 'Some issue', description: 'Some issue description') } let(:current_project) { issue.project } let(:new_project) { create(:project) } + let(:move_params) { { 'move_to_project_id' => new_project.id } } + let(:move_service) { described_class.new(current_project, user, move_params, issue) } before do current_project.team << [user, :master] end - describe '#execute' do - let!(:new_issue) do - described_class.new(current_project, user).execute(issue, new_project) + context 'issue movable' do + describe '#move?' do + subject { move_service.move? } + it { is_expected.to be_truthy } end - it 'should create a new issue in a new project' do - expect(new_issue.project).to eq new_project - end + describe '#execute' do + let!(:new_issue) { move_service.execute } + + it 'should create a new issue in a new project' do + expect(new_issue.project).to eq new_project + end + + it 'should add system note to old issue' do + expect(issue.notes.last.note).to match /^Moved to/ + end - it 'should add system note to old issue' do - expect(issue.notes.last.note).to match /^Moved to/ + it 'should add system note to new issue' do + expect(new_issue.notes.last.note).to match /^Moved from/ + end end + end + + context 'issue not movable' do + context 'move not requested' do + let(:move_params) { {} } - it 'should add system note to new issue' do - expect(new_issue.notes.last.note).to match /^Moved from/ + describe '#move?' do + subject { move_service.move? } + it { is_expected.to be_falsey } + end end end end diff --git a/spec/services/system_note_service_spec.rb b/spec/services/system_note_service_spec.rb index e0ae49ab37c..7efb9f2ddba 100644 --- a/spec/services/system_note_service_spec.rb +++ b/spec/services/system_note_service_spec.rb @@ -486,7 +486,7 @@ describe SystemNoteService, services: true do end context 'invalid direction' do - let (:direction) { :invalid } + let(:direction) { :invalid } it 'should raise error' do expect { subject }.to raise_error StandardError, /Invalid direction/ -- cgit v1.2.1 From 14c983fb696b7b75065df2b5defd458e563444e3 Mon Sep 17 00:00:00 2001 From: Grzegorz Bizon Date: Mon, 22 Feb 2016 11:00:40 +0100 Subject: Add implementation that rewrites issue notes when moving --- app/services/issues/move_service.rb | 23 ++++++++--- spec/services/issues/move_service_spec.rb | 67 ++++++++++++++++++++++++------- 2 files changed, 71 insertions(+), 19 deletions(-) diff --git a/app/services/issues/move_service.rb b/app/services/issues/move_service.rb index 935bd8a87f7..2cba6147429 100644 --- a/app/services/issues/move_service.rb +++ b/app/services/issues/move_service.rb @@ -15,9 +15,19 @@ module Issues def execute return unless move? + # New issue tasks + # open_new_issue rewrite_notes + add_moved_from_note + + # Old issue tasks + # close_old_issue + add_moved_to_note + + # Notifications + # notify_participants @issue_new @@ -40,25 +50,28 @@ module Issues def open_new_issue @issue_new.project = @project_new @issue_new.save! - - add_note_moved_from end def rewrite_notes + @issue_old.notes.find_each do |note| + note_new = note.dup + note_new.project = @project_new + note_new.noteable = @issue_new + note_new.save! + end end def close_old_issue - add_note_moved_to end def notify_participants end - def add_note_moved_from + def add_moved_from_note SystemNoteService.noteable_moved(:from, @issue_new, @project_new, @issue_old, @current_user) end - def add_note_moved_to + def add_moved_to_note SystemNoteService.noteable_moved(:to, @issue_old, @project_old, @issue_new, @current_user) end end diff --git a/spec/services/issues/move_service_spec.rb b/spec/services/issues/move_service_spec.rb index 412a817208e..6667840ec6e 100644 --- a/spec/services/issues/move_service_spec.rb +++ b/spec/services/issues/move_service_spec.rb @@ -2,15 +2,15 @@ require 'spec_helper' describe Issues::MoveService, services: true do let(:user) { create(:user) } - let(:issue) { create(:issue, title: 'Some issue', description: 'Some issue description') } - let(:current_project) { issue.project } + let(:title) { 'Some issue' } + let(:description) { 'Some issue description' } + let(:old_issue) { create(:issue, title: title, description: description) } + let(:current_project) { old_issue.project } let(:new_project) { create(:project) } let(:move_params) { { 'move_to_project_id' => new_project.id } } - let(:move_service) { described_class.new(current_project, user, move_params, issue) } + let(:move_service) { described_class.new(current_project, user, move_params, old_issue) } - before do - current_project.team << [user, :master] - end + before { current_project.team << [user, :master] } context 'issue movable' do describe '#move?' do @@ -19,18 +19,57 @@ describe Issues::MoveService, services: true do end describe '#execute' do - let!(:new_issue) { move_service.execute } - - it 'should create a new issue in a new project' do - expect(new_issue.project).to eq new_project + shared_context 'issue move executed' do + let!(:new_issue) { move_service.execute } end - it 'should add system note to old issue' do - expect(issue.notes.last.note).to match /^Moved to/ + context 'generic issue' do + include_context 'issue move executed' + + it 'creates a new issue in a new project' do + expect(new_issue.project).to eq new_project + end + + it 'rewrites issue title' do + expect(new_issue.title).to eq title + end + + it 'rewrites issue description' do + expect(new_issue.description).to include description + end + + it 'adds system note to old issue at the end' do + expect(old_issue.notes.last.note).to match /^Moved to/ + end + + it 'adds system note to new issue at the end' do + expect(new_issue.notes.last.note).to match /^Moved from/ + end end - it 'should add system note to new issue' do - expect(new_issue.notes.last.note).to match /^Moved from/ + context 'notes exist' do + let(:note_contents) do + ['Some system note 1', 'Some comment', 'Some system note 2'] + end + + before do + note_params = { noteable: old_issue, project: current_project, author: user} + create(:system_note, note_params.merge(note: note_contents.first)) + create(:note, note_params.merge(note: note_contents.second)) + create(:system_note, note_params.merge(note: note_contents.third)) + end + + include_context 'issue move executed' + + let(:new_notes) { new_issue.notes.order('id ASC').pluck(:note) } + + it 'rewrites existing system notes in valid order' do + expect(new_notes.first(3)).to eq note_contents + end + + it 'adds a system note about move after rewritten notes' do + expect(new_notes.last).to match /^Moved from/ + end end end end -- cgit v1.2.1 From 8d1b7f950708a0b759026816fcadb6324b29a208 Mon Sep 17 00:00:00 2001 From: Grzegorz Bizon Date: Mon, 22 Feb 2016 12:41:33 +0100 Subject: Add issue move ability and use it in move service --- app/models/ability.rb | 3 +- app/services/issues/move_service.rb | 9 ++-- spec/services/issues/move_service_spec.rb | 70 ++++++++++++++++++++++++++----- 3 files changed, 65 insertions(+), 17 deletions(-) diff --git a/app/models/ability.rb b/app/models/ability.rb index fe9e0aab717..5dff01a4e5d 100644 --- a/app/models/ability.rb +++ b/app/models/ability.rb @@ -222,7 +222,8 @@ class Ability :admin_wiki, :admin_project, :admin_commit_status, - :admin_build + :admin_build, + :move_issue ] end diff --git a/app/services/issues/move_service.rb b/app/services/issues/move_service.rb index 2cba6147429..8a39e2d5f4d 100644 --- a/app/services/issues/move_service.rb +++ b/app/services/issues/move_service.rb @@ -34,17 +34,14 @@ module Issues end def move? - return false unless @project_new - return false unless @issue_new - return false unless can_move? - - true + @project_new && can_move? end private def can_move? - true + can?(@current_user, :move_issue, @project_old) && + can?(@current_user, :move_issue, @project_new) end def open_new_issue diff --git a/spec/services/issues/move_service_spec.rb b/spec/services/issues/move_service_spec.rb index 6667840ec6e..d8357f89172 100644 --- a/spec/services/issues/move_service_spec.rb +++ b/spec/services/issues/move_service_spec.rb @@ -5,14 +5,25 @@ describe Issues::MoveService, services: true do let(:title) { 'Some issue' } let(:description) { 'Some issue description' } let(:old_issue) { create(:issue, title: title, description: description) } - let(:current_project) { old_issue.project } + let(:old_project) { old_issue.project } let(:new_project) { create(:project) } - let(:move_params) { { 'move_to_project_id' => new_project.id } } - let(:move_service) { described_class.new(current_project, user, move_params, old_issue) } + let(:move_service) { described_class.new(old_project, user, move_params, old_issue) } - before { current_project.team << [user, :master] } + shared_context 'issue move requested' do + let(:move_params) { { 'move_to_project_id' => new_project.id } } + end + shared_context 'user can move issue' do + before do + old_project.team << [user, :master] + new_project.team << [user, :master] + end + end + context 'issue movable' do + include_context 'issue move requested' + include_context 'user can move issue' + describe '#move?' do subject { move_service.move? } it { is_expected.to be_truthy } @@ -53,7 +64,7 @@ describe Issues::MoveService, services: true do end before do - note_params = { noteable: old_issue, project: current_project, author: user} + note_params = { noteable: old_issue, project: old_project, author: user} create(:system_note, note_params.merge(note: note_contents.first)) create(:note, note_params.merge(note: note_contents.second)) create(:system_note, note_params.merge(note: note_contents.third)) @@ -74,12 +85,51 @@ describe Issues::MoveService, services: true do end end - context 'issue not movable' do - context 'move not requested' do - let(:move_params) { {} } + context 'issue move not requested' do + let(:move_params) { {} } + + describe '#move?' do + subject { move_service.move? } + + context 'user do not have permissions to move issue' do + it { is_expected.to be_falsey } + end + + context 'user has permissions to move issue' do + include_context 'user can move issue' + it { is_expected.to be_falsey } + end + end + end + + + describe 'move permissions' do + include_context 'issue move requested' + + describe '#move?' do + subject { move_service.move? } + + context 'user is master in both projects' do + include_context 'user can move issue' + it { is_expected.to be_truthy } + end + + context 'user is master only in new project' do + before { new_project.team << [user, :master] } + it { is_expected.to be_falsey } + end + + context 'user is master only in old project' do + before { old_project.team << [user, :master] } + it { is_expected.to be_falsey } + end + + context 'user is master in one project and developer in another' do + before do + new_project.team << [user, :developer] + old_project.team << [user, :master] + end - describe '#move?' do - subject { move_service.move? } it { is_expected.to be_falsey } end end -- cgit v1.2.1 From 8a02449440b0491a056106d693d502252121740f Mon Sep 17 00:00:00 2001 From: Grzegorz Bizon Date: Mon, 22 Feb 2016 13:54:21 +0100 Subject: Do not show issue move form unless user can move --- app/views/shared/issuable/_form.html.haml | 2 +- spec/services/issues/move_service_spec.rb | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/app/views/shared/issuable/_form.html.haml b/app/views/shared/issuable/_form.html.haml index 22540cbfe69..bb0252ba3de 100644 --- a/app/views/shared/issuable/_form.html.haml +++ b/app/views/shared/issuable/_form.html.haml @@ -67,7 +67,7 @@ - if can? current_user, :admin_label, issuable.project = link_to 'Create new label', new_namespace_project_label_path(issuable.project.namespace, issuable.project), target: :blank -- if issuable.is_a?(Issue) +- if issuable.is_a?(Issue) && can?(current_user, :move_issue, issuable.project) %hr .form-group = f.label :move_to_project_id, 'Move', class: 'control-label' diff --git a/spec/services/issues/move_service_spec.rb b/spec/services/issues/move_service_spec.rb index d8357f89172..6fda8bc6716 100644 --- a/spec/services/issues/move_service_spec.rb +++ b/spec/services/issues/move_service_spec.rb @@ -64,7 +64,7 @@ describe Issues::MoveService, services: true do end before do - note_params = { noteable: old_issue, project: old_project, author: user} + note_params = { noteable: old_issue, project: old_project, author: user } create(:system_note, note_params.merge(note: note_contents.first)) create(:note, note_params.merge(note: note_contents.second)) create(:system_note, note_params.merge(note: note_contents.third)) -- cgit v1.2.1 From 5ed8a1e107b9b6f91257e5afdd370f0552078786 Mon Sep 17 00:00:00 2001 From: Grzegorz Bizon Date: Mon, 22 Feb 2016 15:18:39 +0100 Subject: Silently close old issue when it has been moved --- app/services/issues/move_service.rb | 12 +++++------- spec/services/issues/move_service_spec.rb | 9 +++++++++ 2 files changed, 14 insertions(+), 7 deletions(-) diff --git a/app/services/issues/move_service.rb b/app/services/issues/move_service.rb index 8a39e2d5f4d..d818ed86020 100644 --- a/app/services/issues/move_service.rb +++ b/app/services/issues/move_service.rb @@ -23,8 +23,8 @@ module Issues # Old issue tasks # - close_old_issue add_moved_to_note + close_old_issue # Notifications # @@ -45,20 +45,18 @@ module Issues end def open_new_issue - @issue_new.project = @project_new - @issue_new.save! + @issue_new.update(project: @project_new) end def rewrite_notes @issue_old.notes.find_each do |note| - note_new = note.dup - note_new.project = @project_new - note_new.noteable = @issue_new - note_new.save! + new_note = note.dup + new_note.update(project: @project_new, noteable: @issue_new) end end def close_old_issue + @issue_old.update(state: :closed) end def notify_participants diff --git a/spec/services/issues/move_service_spec.rb b/spec/services/issues/move_service_spec.rb index 6fda8bc6716..3b81cc90c61 100644 --- a/spec/services/issues/move_service_spec.rb +++ b/spec/services/issues/move_service_spec.rb @@ -56,6 +56,15 @@ describe Issues::MoveService, services: true do it 'adds system note to new issue at the end' do expect(new_issue.notes.last.note).to match /^Moved from/ end + + it 'closes old issue' do + expect(old_issue.closed?).to be true + end + + it 'persists changes to old and new issue' do + expect(new_issue.changed?).to be false + expect(old_issue.changed?).to be false + end end context 'notes exist' do -- cgit v1.2.1 From 4cbe87d50ecfad9b97ba76f05935124676c96052 Mon Sep 17 00:00:00 2001 From: Grzegorz Bizon Date: Tue, 23 Feb 2016 14:18:54 +0100 Subject: Rewrite references in issue description when moving it --- app/services/issues/move_service.rb | 17 ++++++++++++++++- spec/services/issues/move_service_spec.rb | 25 +++++++++++++++++++------ 2 files changed, 35 insertions(+), 7 deletions(-) diff --git a/app/services/issues/move_service.rb b/app/services/issues/move_service.rb index d818ed86020..bba972382d9 100644 --- a/app/services/issues/move_service.rb +++ b/app/services/issues/move_service.rb @@ -45,7 +45,8 @@ module Issues end def open_new_issue - @issue_new.update(project: @project_new) + new_description = rewrite_references(@issue_old, @issue_old.description) + @issue_new.update(project: @project_new, description: new_description) end def rewrite_notes @@ -69,5 +70,19 @@ module Issues def add_moved_to_note SystemNoteService.noteable_moved(:to, @issue_old, @project_old, @issue_new, @current_user) end + + def rewrite_references(mentionable, text) + new_content = text.dup + references = mentionable.all_references + + [:issues, :merge_requests, :milestones].each do |type| + references.public_send(type).each do |mentioned| + new_content.gsub!(mentioned.to_reference, + mentioned.to_reference(@project_new)) + end + end + + new_content + end end end diff --git a/spec/services/issues/move_service_spec.rb b/spec/services/issues/move_service_spec.rb index 3b81cc90c61..8d9cc09ffc7 100644 --- a/spec/services/issues/move_service_spec.rb +++ b/spec/services/issues/move_service_spec.rb @@ -4,8 +4,8 @@ describe Issues::MoveService, services: true do let(:user) { create(:user) } let(:title) { 'Some issue' } let(:description) { 'Some issue description' } - let(:old_issue) { create(:issue, title: title, description: description) } - let(:old_project) { old_issue.project } + let(:old_project) { create(:project) } + let(:old_issue) { create(:issue, title: title, description: description, project: old_project) } let(:new_project) { create(:project) } let(:move_service) { described_class.new(old_project, user, move_params, old_issue) } @@ -61,13 +61,12 @@ describe Issues::MoveService, services: true do expect(old_issue.closed?).to be true end - it 'persists changes to old and new issue' do - expect(new_issue.changed?).to be false - expect(old_issue.changed?).to be false + it 'persists new issue' do + expect(new_issue.persisted?).to be true end end - context 'notes exist' do + context 'issue with notes' do let(:note_contents) do ['Some system note 1', 'Some comment', 'Some system note 2'] end @@ -91,6 +90,20 @@ describe Issues::MoveService, services: true do expect(new_notes.last).to match /^Moved from/ end end + + describe 'rewritting references' do + include_context 'issue move executed' + + context 'issue reference' do + let(:another_issue) { create(:issue, project: old_project) } + let(:description) { "Some description #{another_issue.to_reference}" } + + it 'rewrites referenced issues creating cross project reference' do + expect(new_issue.description) + .to eq "Some description #{old_project.to_reference}#{another_issue.to_reference}" + end + end + end end end -- cgit v1.2.1 From 57eb39548879109dff3813129fca7acbcca23f71 Mon Sep 17 00:00:00 2001 From: Grzegorz Bizon Date: Wed, 24 Feb 2016 11:52:02 +0100 Subject: Do not pass unsanitized params to issue move service --- app/controllers/projects/issues_controller.rb | 3 ++- app/services/issues/move_service.rb | 7 ++----- spec/services/issues/move_service_spec.rb | 10 +++++++--- 3 files changed, 11 insertions(+), 9 deletions(-) diff --git a/app/controllers/projects/issues_controller.rb b/app/controllers/projects/issues_controller.rb index e3486f576c0..9ec342a7b2a 100644 --- a/app/controllers/projects/issues_controller.rb +++ b/app/controllers/projects/issues_controller.rb @@ -88,7 +88,8 @@ class Projects::IssuesController < Projects::ApplicationController def update @issue = Issues::UpdateService.new(project, current_user, issue_params).execute(issue) - move_service = Issues::MoveService.new(project, current_user, params.require(:issue).permit!, @issue) + move_service = Issues::MoveService.new(project, current_user, issue_params, + @issue, params['move_to_project_id']) if move_service.move? @issue = move_service.execute diff --git a/app/services/issues/move_service.rb b/app/services/issues/move_service.rb index bba972382d9..55239d566f1 100644 --- a/app/services/issues/move_service.rb +++ b/app/services/issues/move_service.rb @@ -1,15 +1,12 @@ module Issues class MoveService < Issues::BaseService - def initialize(project, current_user, params, issue) + def initialize(project, current_user, params, issue, new_project_id) super(project, current_user, params) @issue_old = issue @issue_new = @issue_old.dup @project_old = @project - - if params['move_to_project_id'] - @project_new = Project.find(params['move_to_project_id']) - end + @project_new = Project.find(new_project_id) if new_project_id end def execute diff --git a/spec/services/issues/move_service_spec.rb b/spec/services/issues/move_service_spec.rb index 8d9cc09ffc7..931ba06f6a1 100644 --- a/spec/services/issues/move_service_spec.rb +++ b/spec/services/issues/move_service_spec.rb @@ -7,10 +7,14 @@ describe Issues::MoveService, services: true do let(:old_project) { create(:project) } let(:old_issue) { create(:issue, title: title, description: description, project: old_project) } let(:new_project) { create(:project) } - let(:move_service) { described_class.new(old_project, user, move_params, old_issue) } + let(:issue_params) { old_issue.serializable_hash } + + let(:move_service) do + described_class.new(old_project, user, issue_params, old_issue, new_project_id) + end shared_context 'issue move requested' do - let(:move_params) { { 'move_to_project_id' => new_project.id } } + let(:new_project_id) { new_project.id } end shared_context 'user can move issue' do @@ -108,7 +112,7 @@ describe Issues::MoveService, services: true do end context 'issue move not requested' do - let(:move_params) { {} } + let(:new_project_id) { nil } describe '#move?' do subject { move_service.move? } -- cgit v1.2.1 From fbb5a8b089d00fb66d8915b0f546dd07e76877e4 Mon Sep 17 00:00:00 2001 From: Grzegorz Bizon Date: Wed, 24 Feb 2016 12:18:46 +0100 Subject: Use a new issue create service when moving an issue --- app/services/issues/move_service.rb | 29 ++++++++++++++++++++++++----- spec/services/issues/move_service_spec.rb | 5 +++++ 2 files changed, 29 insertions(+), 5 deletions(-) diff --git a/app/services/issues/move_service.rb b/app/services/issues/move_service.rb index 55239d566f1..510a04e98f7 100644 --- a/app/services/issues/move_service.rb +++ b/app/services/issues/move_service.rb @@ -4,7 +4,7 @@ module Issues super(project, current_user, params) @issue_old = issue - @issue_new = @issue_old.dup + @issue_new = nil @project_old = @project @project_new = Project.find(new_project_id) if new_project_id end @@ -42,8 +42,18 @@ module Issues end def open_new_issue - new_description = rewrite_references(@issue_old, @issue_old.description) - @issue_new.update(project: @project_new, description: new_description) + create_service = CreateService.new(@project_new, current_user, new_issue_params) + @issue_new = create_service.execute + end + + def new_issue_params + new_params = { id: nil, iid: nil, milestone_id: nil, label_ids: [], + project_id: @project_new.id, + author_id: @issue_old.author_id, + description: rewrite_references(@issue_old), + updated_by_id: current_user.id } + + params.merge(new_params) end def rewrite_notes @@ -68,9 +78,9 @@ module Issues SystemNoteService.noteable_moved(:to, @issue_old, @project_old, @issue_new, @current_user) end - def rewrite_references(mentionable, text) - new_content = text.dup + def rewrite_references(mentionable) references = mentionable.all_references + new_content = mentionable_content(mentionable).dup [:issues, :merge_requests, :milestones].each do |type| references.public_send(type).each do |mentioned| @@ -81,5 +91,14 @@ module Issues new_content end + + def mentionable_content(mentionable) + case mentionable + when Issue then mentionable.description + when Note then mentionable.note + else + raise 'Unexpected mentionable while moving an issue' + end + end end end diff --git a/spec/services/issues/move_service_spec.rb b/spec/services/issues/move_service_spec.rb index 931ba06f6a1..ec913cdde2a 100644 --- a/spec/services/issues/move_service_spec.rb +++ b/spec/services/issues/move_service_spec.rb @@ -68,6 +68,11 @@ describe Issues::MoveService, services: true do it 'persists new issue' do expect(new_issue.persisted?).to be true end + + it 'persist all changes' do + expect(old_issue.changed?).to be false + expect(new_issue.changed?).to be false + end end context 'issue with notes' do -- cgit v1.2.1 From 8d3f072ec4f01017a4ec2e1d2082bafcf2b58188 Mon Sep 17 00:00:00 2001 From: Grzegorz Bizon Date: Wed, 24 Feb 2016 13:20:08 +0100 Subject: Take care about data being rewritten when moving issue --- app/services/issues/move_service.rb | 4 +--- spec/services/issues/move_service_spec.rb | 20 +++++++++++++++++++- 2 files changed, 20 insertions(+), 4 deletions(-) diff --git a/app/services/issues/move_service.rb b/app/services/issues/move_service.rb index 510a04e98f7..37aaeb28857 100644 --- a/app/services/issues/move_service.rb +++ b/app/services/issues/move_service.rb @@ -49,9 +49,7 @@ module Issues def new_issue_params new_params = { id: nil, iid: nil, milestone_id: nil, label_ids: [], project_id: @project_new.id, - author_id: @issue_old.author_id, - description: rewrite_references(@issue_old), - updated_by_id: current_user.id } + description: rewrite_references(@issue_old) } params.merge(new_params) end diff --git a/spec/services/issues/move_service_spec.rb b/spec/services/issues/move_service_spec.rb index ec913cdde2a..282dfd9187d 100644 --- a/spec/services/issues/move_service_spec.rb +++ b/spec/services/issues/move_service_spec.rb @@ -2,13 +2,18 @@ require 'spec_helper' describe Issues::MoveService, services: true do let(:user) { create(:user) } + let(:author) { create(:user) } let(:title) { 'Some issue' } let(:description) { 'Some issue description' } let(:old_project) { create(:project) } - let(:old_issue) { create(:issue, title: title, description: description, project: old_project) } let(:new_project) { create(:project) } let(:issue_params) { old_issue.serializable_hash } + let(:old_issue) do + create(:issue, title: title, description: description, + project: old_project, author: author) + end + let(:move_service) do described_class.new(old_project, user, issue_params, old_issue, new_project_id) end @@ -73,6 +78,19 @@ describe Issues::MoveService, services: true do expect(old_issue.changed?).to be false expect(new_issue.changed?).to be false end + + it 'changes author' do + expect(new_issue.author).to eq user + end + + it 'removes data that is invalid in new context' do + expect(new_issue.milestone).to be_nil + expect(new_issue.labels).to be_empty + end + + it 'creates a new internal id for issue' do + expect(new_issue.iid).to be 1 + end end context 'issue with notes' do -- cgit v1.2.1 From c2d4060f26d06d1ce5450345a8be2bbf9186745f Mon Sep 17 00:00:00 2001 From: Grzegorz Bizon Date: Wed, 24 Feb 2016 15:38:52 +0100 Subject: Rewrite references in notes when moving issue --- app/services/issues/move_service.rb | 9 +++-- spec/services/issues/move_service_spec.rb | 57 +++++++++++++++++++++++-------- 2 files changed, 49 insertions(+), 17 deletions(-) diff --git a/app/services/issues/move_service.rb b/app/services/issues/move_service.rb index 37aaeb28857..782b8b6a430 100644 --- a/app/services/issues/move_service.rb +++ b/app/services/issues/move_service.rb @@ -57,7 +57,10 @@ module Issues def rewrite_notes @issue_old.notes.find_each do |note| new_note = note.dup - new_note.update(project: @project_new, noteable: @issue_new) + new_params = { project: @project_new, noteable: @issue_new, + note: rewrite_references(new_note) } + + new_note.update(new_params) end end @@ -79,8 +82,10 @@ module Issues def rewrite_references(mentionable) references = mentionable.all_references new_content = mentionable_content(mentionable).dup + cross_project_refs = [:issues, :merge_requests, :milestones, + :snippets, :commits, :commit_ranges] - [:issues, :merge_requests, :milestones].each do |type| + cross_project_refs.each do |type| references.public_send(type).each do |mentioned| new_content.gsub!(mentioned.to_reference, mentioned.to_reference(@project_new)) diff --git a/spec/services/issues/move_service_spec.rb b/spec/services/issues/move_service_spec.rb index 282dfd9187d..596da74b572 100644 --- a/spec/services/issues/move_service_spec.rb +++ b/spec/services/issues/move_service_spec.rb @@ -94,27 +94,54 @@ describe Issues::MoveService, services: true do end context 'issue with notes' do - let(:note_contents) do - ['Some system note 1', 'Some comment', 'Some system note 2'] - end + context 'notes without references' do + let(:notes_params) do + [{ system: false, note: 'Some comment 1' }, + { system: true, note: 'Some system note' }, + { system: false, note: 'Some comment 2' }] + end - before do - note_params = { noteable: old_issue, project: old_project, author: user } - create(:system_note, note_params.merge(note: note_contents.first)) - create(:note, note_params.merge(note: note_contents.second)) - create(:system_note, note_params.merge(note: note_contents.third)) - end + before do + note_params = { noteable: old_issue, project: old_project, author: author } + notes_params.each do |note| + create(:note, note_params.merge(note)) + end + end - include_context 'issue move executed' + include_context 'issue move executed' - let(:new_notes) { new_issue.notes.order('id ASC').pluck(:note) } + let(:all_notes) { new_issue.notes.order('id ASC') } + let(:system_notes) { all_notes.system } + let(:user_notes) { all_notes.user } + + it 'rewrites existing notes in valid order' do + expect(all_notes.pluck(:note).first(3)) + .to eq notes_params.map { |n| n[:note] } + end - it 'rewrites existing system notes in valid order' do - expect(new_notes.first(3)).to eq note_contents + it 'adds a system note about move after rewritten notes' do + expect(system_notes.last.note).to match /^Moved from/ + end + + it 'preserves orignal author of comment' do + expect(user_notes.pluck(:author_id)).to all(eq(author.id)) + end end - it 'adds a system note about move after rewritten notes' do - expect(new_notes.last).to match /^Moved from/ + context 'notes with references' do + before do + create(:merge_request, source_project: old_project) + create(:note, noteable: old_issue, project: old_project, author: author, + note: 'Note with reference to merge request !1') + end + + include_context 'issue move executed' + let(:new_note) { new_issue.notes.first } + + it 'rewrites references using a cross reference to old project' do + expect(new_note.note) + .to eq "Note with reference to merge request #{old_project.to_reference}!1" + end end end -- cgit v1.2.1 From efd8251751c85285e6519fd3dd756f74579da15b Mon Sep 17 00:00:00 2001 From: Grzegorz Bizon Date: Wed, 24 Feb 2016 16:27:06 +0100 Subject: Minor refactoring of issue move service and specs --- app/services/issues/move_service.rb | 32 ++++++++++++++++--------------- spec/services/issues/move_service_spec.rb | 5 +++-- 2 files changed, 20 insertions(+), 17 deletions(-) diff --git a/app/services/issues/move_service.rb b/app/services/issues/move_service.rb index 782b8b6a430..a404a64cfe9 100644 --- a/app/services/issues/move_service.rb +++ b/app/services/issues/move_service.rb @@ -79,28 +79,30 @@ module Issues SystemNoteService.noteable_moved(:to, @issue_old, @project_old, @issue_new, @current_user) end - def rewrite_references(mentionable) - references = mentionable.all_references - new_content = mentionable_content(mentionable).dup - cross_project_refs = [:issues, :merge_requests, :milestones, - :snippets, :commits, :commit_ranges] - - cross_project_refs.each do |type| - references.public_send(type).each do |mentioned| - new_content.gsub!(mentioned.to_reference, - mentioned.to_reference(@project_new)) + def rewrite_references(noteable) + references = noteable.all_references + new_content = noteable_content(noteable).dup + cross_project_mentionables = [:issues, :merge_requests, :milestones, + :snippets, :commits, :commit_ranges] + + cross_project_mentionables.each do |type| + references.public_send(type).each do |mentionable| + new_content.gsub!(mentionable.to_reference, + mentionable.to_reference(@project_new)) end end new_content end - def mentionable_content(mentionable) - case mentionable - when Issue then mentionable.description - when Note then mentionable.note + def noteable_content(noteable) + case noteable + when Issue + noteable.description + when Note + noteable.note else - raise 'Unexpected mentionable while moving an issue' + raise 'Unexpected noteable while moving an issue' end end end diff --git a/spec/services/issues/move_service_spec.rb b/spec/services/issues/move_service_spec.rb index 596da74b572..cd051175612 100644 --- a/spec/services/issues/move_service_spec.rb +++ b/spec/services/issues/move_service_spec.rb @@ -101,6 +101,8 @@ describe Issues::MoveService, services: true do { system: false, note: 'Some comment 2' }] end + let(:notes_contents) { notes_params.map { |n| n[:note] } } + before do note_params = { noteable: old_issue, project: old_project, author: author } notes_params.each do |note| @@ -115,8 +117,7 @@ describe Issues::MoveService, services: true do let(:user_notes) { all_notes.user } it 'rewrites existing notes in valid order' do - expect(all_notes.pluck(:note).first(3)) - .to eq notes_params.map { |n| n[:note] } + expect(all_notes.pluck(:note).first(3)).to eq notes_contents end it 'adds a system note about move after rewritten notes' do -- cgit v1.2.1 From 3c493c24c70f7c8dc8e1f3bcf29e18d1ef0944a7 Mon Sep 17 00:00:00 2001 From: Grzegorz Bizon Date: Fri, 26 Feb 2016 12:04:29 +0100 Subject: Add reference unfold pipeline used when moving issue --- app/services/issues/move_service.rb | 11 ++-- lib/banzai/filter/reference_unfold_filter.rb | 46 +++++++++++++++++ lib/banzai/pipeline/reference_unfold_pipeline.rb | 9 ++++ .../pipeline/reference_unfold_pipeline_spec.rb | 59 ++++++++++++++++++++++ spec/services/issues/move_service_spec.rb | 4 +- 5 files changed, 123 insertions(+), 6 deletions(-) create mode 100644 lib/banzai/filter/reference_unfold_filter.rb create mode 100644 lib/banzai/pipeline/reference_unfold_pipeline.rb create mode 100644 spec/lib/banzai/pipeline/reference_unfold_pipeline_spec.rb diff --git a/app/services/issues/move_service.rb b/app/services/issues/move_service.rb index a404a64cfe9..cf935f30ce3 100644 --- a/app/services/issues/move_service.rb +++ b/app/services/issues/move_service.rb @@ -86,10 +86,13 @@ module Issues :snippets, :commits, :commit_ranges] cross_project_mentionables.each do |type| - references.public_send(type).each do |mentionable| - new_content.gsub!(mentionable.to_reference, - mentionable.to_reference(@project_new)) - end + referables = references.public_send(type) + + context = { objects: referables, project: @project_new, + pipeline: :reference_unfold } + + new_content = Banzai.render_result(new_content, context) + new_content = new_content[:output].to_s end new_content diff --git a/lib/banzai/filter/reference_unfold_filter.rb b/lib/banzai/filter/reference_unfold_filter.rb new file mode 100644 index 00000000000..a6145261651 --- /dev/null +++ b/lib/banzai/filter/reference_unfold_filter.rb @@ -0,0 +1,46 @@ +module Banzai + module Filter + ## + # Filter than unfolds local references. + # + # Replaces all local references with project cross reference version + # in all objects passed to this filter in context. + # + # Requires objects array with each element implementing `Referable`. + # + class ReferenceUnfoldFilter < ReferenceFilter + def initialize(*) + super + + @objects = context[:objects] + @project = context[:project] + + unless @objects.all? { |object| object.respond_to?(:to_reference) } + raise StandardError, "No `to_reference` method implemented in one of the objects !" + end + + unless @project.kind_of?(Project) + raise StandardError, 'No valid project passed in context!' + end + end + + def call + @objects.each do |object| + pattern = /#{Regexp.escape(object.to_reference)}/ + replace_text_nodes_matching(pattern) do |content| + content.gsub(pattern, object.to_reference(@project)) + end + end + + doc + end + + private + + def validate + needs :project + needs :objects + end + end + end +end diff --git a/lib/banzai/pipeline/reference_unfold_pipeline.rb b/lib/banzai/pipeline/reference_unfold_pipeline.rb new file mode 100644 index 00000000000..5b555d7c2d7 --- /dev/null +++ b/lib/banzai/pipeline/reference_unfold_pipeline.rb @@ -0,0 +1,9 @@ +module Banzai + module Pipeline + class ReferenceUnfoldPipeline < BasePipeline + def self.filters + [Filter::ReferenceUnfoldFilter] + end + end + end +end diff --git a/spec/lib/banzai/pipeline/reference_unfold_pipeline_spec.rb b/spec/lib/banzai/pipeline/reference_unfold_pipeline_spec.rb new file mode 100644 index 00000000000..af9d6f4ad0d --- /dev/null +++ b/spec/lib/banzai/pipeline/reference_unfold_pipeline_spec.rb @@ -0,0 +1,59 @@ +require 'spec_helper' + +describe Banzai::Pipeline::ReferenceUnfoldPipeline do + let(:text) { 'some text' } + let(:project) { create(:project) } + let(:objects) { [] } + + let(:result) do + described_class.to_html(text, project: project, objects: objects) + end + + context 'invalid initializers' do + subject { -> { result } } + + context 'project context is invalid' do + let(:project) { nil } + it { is_expected.to raise_error StandardError, /No valid project/ } + end + + context 'objects context is invalid' do + let(:objects) { ['issue'] } + it { is_expected.to raise_error StandardError, /No `to_reference` method/ } + end + end + + context 'multiple issues and merge requests referenced' do + subject { result } + + let(:main_project) { create(:project) } + + let(:issue_first) { create(:issue, project: main_project) } + let(:issue_second) { create(:issue, project: main_project) } + let(:merge_request) { create(:merge_request, source_project: main_project) } + + let(:objects) { [issue_first, issue_second, merge_request] } + + context 'plain text description' do + let(:text) { 'Description that references #1, #2 and !1' } + + it { is_expected.to include issue_first.to_reference(project) } + it { is_expected.to include issue_second.to_reference(project) } + it { is_expected.to include merge_request.to_reference(project) } + end + + context 'description with ignored elements' do + let(:text) do + <<-EOF + Hi. This references #1, but not `#2` +
and not !1
+ EOF + end + + + it { is_expected.to include issue_first.to_reference(project) } + it { is_expected.to_not include issue_second.to_reference(project) } + it { is_expected.to_not include merge_request.to_reference(project) } + end + end +end diff --git a/spec/services/issues/move_service_spec.rb b/spec/services/issues/move_service_spec.rb index cd051175612..d516bd4830a 100644 --- a/spec/services/issues/move_service_spec.rb +++ b/spec/services/issues/move_service_spec.rb @@ -11,7 +11,7 @@ describe Issues::MoveService, services: true do let(:old_issue) do create(:issue, title: title, description: description, - project: old_project, author: author) + project: old_project, author: author) end let(:move_service) do @@ -133,7 +133,7 @@ describe Issues::MoveService, services: true do before do create(:merge_request, source_project: old_project) create(:note, noteable: old_issue, project: old_project, author: author, - note: 'Note with reference to merge request !1') + note: 'Note with reference to merge request !1') end include_context 'issue move executed' -- cgit v1.2.1 From 9124ebce982981b74ae644793a680b2b6060ce21 Mon Sep 17 00:00:00 2001 From: Grzegorz Bizon Date: Mon, 29 Feb 2016 09:49:22 +0100 Subject: Use internal reference extractor in banzai unfold pipeline --- app/services/issues/move_service.rb | 26 ++++------ lib/banzai/filter/reference_unfold_filter.rb | 34 ++++++------- lib/banzai/pipeline/reference_unfold_pipeline.rb | 14 +++++- .../pipeline/reference_unfold_pipeline_spec.rb | 57 ++++++++++++---------- 4 files changed, 66 insertions(+), 65 deletions(-) diff --git a/app/services/issues/move_service.rb b/app/services/issues/move_service.rb index cf935f30ce3..a9448af4699 100644 --- a/app/services/issues/move_service.rb +++ b/app/services/issues/move_service.rb @@ -72,30 +72,22 @@ module Issues end def add_moved_from_note - SystemNoteService.noteable_moved(:from, @issue_new, @project_new, @issue_old, @current_user) + SystemNoteService.noteable_moved(:from, @issue_new, @project_new, + @issue_old, @current_user) end def add_moved_to_note - SystemNoteService.noteable_moved(:to, @issue_old, @project_old, @issue_new, @current_user) + SystemNoteService.noteable_moved(:to, @issue_old, @project_old, + @issue_new, @current_user) end def rewrite_references(noteable) - references = noteable.all_references - new_content = noteable_content(noteable).dup - cross_project_mentionables = [:issues, :merge_requests, :milestones, - :snippets, :commits, :commit_ranges] + content = noteable_content(noteable).dup + context = { pipeline: :reference_unfold, + project: @project_old, new_project: @project_new } - cross_project_mentionables.each do |type| - referables = references.public_send(type) - - context = { objects: referables, project: @project_new, - pipeline: :reference_unfold } - - new_content = Banzai.render_result(new_content, context) - new_content = new_content[:output].to_s - end - - new_content + new_content = Banzai.render_result(content, context) + new_content[:output].to_s end def noteable_content(noteable) diff --git a/lib/banzai/filter/reference_unfold_filter.rb b/lib/banzai/filter/reference_unfold_filter.rb index a6145261651..d8ed44a0e4e 100644 --- a/lib/banzai/filter/reference_unfold_filter.rb +++ b/lib/banzai/filter/reference_unfold_filter.rb @@ -1,45 +1,39 @@ +require 'html/pipeline/filter' + module Banzai module Filter ## # Filter than unfolds local references. # - # Replaces all local references with project cross reference version - # in all objects passed to this filter in context. - # - # Requires objects array with each element implementing `Referable`. # - class ReferenceUnfoldFilter < ReferenceFilter + class ReferenceUnfoldFilter < HTML::Pipeline::Filter def initialize(*) super - @objects = context[:objects] - @project = context[:project] - - unless @objects.all? { |object| object.respond_to?(:to_reference) } - raise StandardError, "No `to_reference` method implemented in one of the objects !" + unless result[:references].is_a?(Hash) + raise StandardError, 'References not processed!' end - unless @project.kind_of?(Project) - raise StandardError, 'No valid project passed in context!' - end + @text = context[:text].dup + @new_project = context[:new_project] + @referables = result[:references].values.flatten end def call - @objects.each do |object| - pattern = /#{Regexp.escape(object.to_reference)}/ - replace_text_nodes_matching(pattern) do |content| - content.gsub(pattern, object.to_reference(@project)) - end + @referables.each do |referable| + pattern = /#{Regexp.escape(referable.to_reference)}/ + @text.gsub!(pattern, referable.to_reference(@new_project)) end - doc + @text end private def validate needs :project - needs :objects + needs :new_project + needs :text end end end diff --git a/lib/banzai/pipeline/reference_unfold_pipeline.rb b/lib/banzai/pipeline/reference_unfold_pipeline.rb index 5b555d7c2d7..597bf7befd1 100644 --- a/lib/banzai/pipeline/reference_unfold_pipeline.rb +++ b/lib/banzai/pipeline/reference_unfold_pipeline.rb @@ -2,7 +2,19 @@ module Banzai module Pipeline class ReferenceUnfoldPipeline < BasePipeline def self.filters - [Filter::ReferenceUnfoldFilter] + FullPipeline.filters + + [Filter::ReferenceGathererFilter, + Filter::ReferenceUnfoldFilter] + end + + def self.call(text, context = {}) + context = context.merge(text: text) + super + end + + class << self + alias_method :to_document, :call + alias_method :to_html, :call end end end diff --git a/spec/lib/banzai/pipeline/reference_unfold_pipeline_spec.rb b/spec/lib/banzai/pipeline/reference_unfold_pipeline_spec.rb index af9d6f4ad0d..c02dc5a2c2c 100644 --- a/spec/lib/banzai/pipeline/reference_unfold_pipeline_spec.rb +++ b/spec/lib/banzai/pipeline/reference_unfold_pipeline_spec.rb @@ -2,58 +2,61 @@ require 'spec_helper' describe Banzai::Pipeline::ReferenceUnfoldPipeline do let(:text) { 'some text' } - let(:project) { create(:project) } - let(:objects) { [] } + let(:old_project) { create(:project) } + let(:new_project) { create(:project) } + let(:pipeline_context) do + { project: old_project, new_project: new_project } + end let(:result) do - described_class.to_html(text, project: project, objects: objects) + described_class.to_document(text, pipeline_context) end context 'invalid initializers' do subject { -> { result } } - context 'project context is invalid' do - let(:project) { nil } - it { is_expected.to raise_error StandardError, /No valid project/ } + context 'project context is missing' do + let(:pipeline_context) { { new_project: new_project } } + it { is_expected.to raise_error ArgumentError, /Missing context keys/ } end - context 'objects context is invalid' do - let(:objects) { ['issue'] } - it { is_expected.to raise_error StandardError, /No `to_reference` method/ } + context 'new project context is missing' do + let(:pipeline_context) { { project: old_project } } + it { is_expected.to raise_error ArgumentError, /Missing context keys/ } end end context 'multiple issues and merge requests referenced' do - subject { result } - - let(:main_project) { create(:project) } - - let(:issue_first) { create(:issue, project: main_project) } - let(:issue_second) { create(:issue, project: main_project) } - let(:merge_request) { create(:merge_request, source_project: main_project) } + subject { result[:output] } - let(:objects) { [issue_first, issue_second, merge_request] } + let!(:issue_first) { create(:issue, project: old_project) } + let!(:issue_second) { create(:issue, project: old_project) } + let!(:merge_request) { create(:merge_request, source_project: old_project) } context 'plain text description' do let(:text) { 'Description that references #1, #2 and !1' } - it { is_expected.to include issue_first.to_reference(project) } - it { is_expected.to include issue_second.to_reference(project) } - it { is_expected.to include merge_request.to_reference(project) } + it { is_expected.to include issue_first.to_reference(new_project) } + it { is_expected.to include issue_second.to_reference(new_project) } + it { is_expected.to include merge_request.to_reference(new_project) } end context 'description with ignored elements' do let(:text) do - <<-EOF - Hi. This references #1, but not `#2` -
and not !1
- EOF + "Hi. This references #1, but not `#2`\n" + + '
and not !1
' end + it { is_expected.to include issue_first.to_reference(new_project) } + it { is_expected.to_not include issue_second.to_reference(new_project) } + it { is_expected.to_not include merge_request.to_reference(new_project) } + end + + context 'description ambigous elements' do + let(:url) { 'http://gitlab.com/#1' } + let(:text) { "This references #1, but not #{url}" } - it { is_expected.to include issue_first.to_reference(project) } - it { is_expected.to_not include issue_second.to_reference(project) } - it { is_expected.to_not include merge_request.to_reference(project) } + it { is_expected.to include url } end end end -- cgit v1.2.1 From cd0f19450843b5b9245ae82302d3f9d9003cb899 Mon Sep 17 00:00:00 2001 From: Grzegorz Bizon Date: Tue, 1 Mar 2016 14:18:51 +0100 Subject: Do not unfold non-referables when moving an issue --- lib/banzai/filter/reference_unfold_filter.rb | 2 ++ 1 file changed, 2 insertions(+) diff --git a/lib/banzai/filter/reference_unfold_filter.rb b/lib/banzai/filter/reference_unfold_filter.rb index d8ed44a0e4e..70a485ba2ae 100644 --- a/lib/banzai/filter/reference_unfold_filter.rb +++ b/lib/banzai/filter/reference_unfold_filter.rb @@ -21,6 +21,8 @@ module Banzai def call @referables.each do |referable| + next unless referable.respond_to?(:to_reference) + pattern = /#{Regexp.escape(referable.to_reference)}/ @text.gsub!(pattern, referable.to_reference(@new_project)) end -- cgit v1.2.1 From fd8394faae25b54c4d9ac485a0ce746cffec3a0f Mon Sep 17 00:00:00 2001 From: Grzegorz Bizon Date: Mon, 7 Mar 2016 13:09:53 +0100 Subject: Move reference unfolder for GFM to separate class --- app/services/issues/move_service.rb | 7 +-- lib/banzai/filter/reference_unfold_filter.rb | 42 --------------- lib/banzai/pipeline/reference_unfold_pipeline.rb | 21 -------- lib/gitlab/gfm/reference_unfolder.rb | 31 +++++++++++ lib/gitlab/reference_extractor.rb | 8 ++- .../pipeline/reference_unfold_pipeline_spec.rb | 62 ---------------------- spec/lib/gitlab/gfm/reference_unfolder_spec.rb | 43 +++++++++++++++ spec/lib/gitlab/reference_extractor_spec.rb | 12 +++++ 8 files changed, 95 insertions(+), 131 deletions(-) delete mode 100644 lib/banzai/filter/reference_unfold_filter.rb delete mode 100644 lib/banzai/pipeline/reference_unfold_pipeline.rb create mode 100644 lib/gitlab/gfm/reference_unfolder.rb delete mode 100644 spec/lib/banzai/pipeline/reference_unfold_pipeline_spec.rb create mode 100644 spec/lib/gitlab/gfm/reference_unfolder_spec.rb diff --git a/app/services/issues/move_service.rb b/app/services/issues/move_service.rb index a9448af4699..de12ea5c172 100644 --- a/app/services/issues/move_service.rb +++ b/app/services/issues/move_service.rb @@ -83,11 +83,8 @@ module Issues def rewrite_references(noteable) content = noteable_content(noteable).dup - context = { pipeline: :reference_unfold, - project: @project_old, new_project: @project_new } - - new_content = Banzai.render_result(content, context) - new_content[:output].to_s + unfolder = Gitlab::Gfm::ReferenceUnfolder.new(content, @project_old) + unfolder.unfold(@project_new) end def noteable_content(noteable) diff --git a/lib/banzai/filter/reference_unfold_filter.rb b/lib/banzai/filter/reference_unfold_filter.rb deleted file mode 100644 index 70a485ba2ae..00000000000 --- a/lib/banzai/filter/reference_unfold_filter.rb +++ /dev/null @@ -1,42 +0,0 @@ -require 'html/pipeline/filter' - -module Banzai - module Filter - ## - # Filter than unfolds local references. - # - # - class ReferenceUnfoldFilter < HTML::Pipeline::Filter - def initialize(*) - super - - unless result[:references].is_a?(Hash) - raise StandardError, 'References not processed!' - end - - @text = context[:text].dup - @new_project = context[:new_project] - @referables = result[:references].values.flatten - end - - def call - @referables.each do |referable| - next unless referable.respond_to?(:to_reference) - - pattern = /#{Regexp.escape(referable.to_reference)}/ - @text.gsub!(pattern, referable.to_reference(@new_project)) - end - - @text - end - - private - - def validate - needs :project - needs :new_project - needs :text - end - end - end -end diff --git a/lib/banzai/pipeline/reference_unfold_pipeline.rb b/lib/banzai/pipeline/reference_unfold_pipeline.rb deleted file mode 100644 index 597bf7befd1..00000000000 --- a/lib/banzai/pipeline/reference_unfold_pipeline.rb +++ /dev/null @@ -1,21 +0,0 @@ -module Banzai - module Pipeline - class ReferenceUnfoldPipeline < BasePipeline - def self.filters - FullPipeline.filters + - [Filter::ReferenceGathererFilter, - Filter::ReferenceUnfoldFilter] - end - - def self.call(text, context = {}) - context = context.merge(text: text) - super - end - - class << self - alias_method :to_document, :call - alias_method :to_html, :call - end - end - end -end diff --git a/lib/gitlab/gfm/reference_unfolder.rb b/lib/gitlab/gfm/reference_unfolder.rb new file mode 100644 index 00000000000..4969e8ebe85 --- /dev/null +++ b/lib/gitlab/gfm/reference_unfolder.rb @@ -0,0 +1,31 @@ +module Gitlab + module Gfm + ## + # Class than unfolds local references in text. + # + # + class ReferenceUnfolder + def initialize(text, project) + @text = text + @project = project + end + + def unfold(from_project) + referables.each_with_object(@text.dup) do |referable, text| + next unless referable.respond_to?(:to_reference) + + pattern = /#{Regexp.escape(referable.to_reference)}/ + text.gsub!(pattern, referable.to_reference(from_project)) + end + end + + private + + def referables + extractor = Gitlab::ReferenceExtractor.new(@project) + extractor.analyze(@text) + extractor.all + end + end + end +end diff --git a/lib/gitlab/reference_extractor.rb b/lib/gitlab/reference_extractor.rb index 4d830aa45e1..8c698d43bef 100644 --- a/lib/gitlab/reference_extractor.rb +++ b/lib/gitlab/reference_extractor.rb @@ -1,6 +1,7 @@ module Gitlab # Extract possible GFM references from an arbitrary String for further processing. class ReferenceExtractor < Banzai::ReferenceExtractor + REFERABLES = %i(user issue label milestone merge_request snippet commit commit_range) attr_accessor :project, :current_user, :author def initialize(project, current_user = nil, author = nil) @@ -17,7 +18,7 @@ module Gitlab super(text, context.merge(project: project)) end - %i(user label milestone merge_request snippet commit commit_range).each do |type| + REFERABLES.each do |type| define_method("#{type}s") do @references[type] ||= references(type, reference_context) end @@ -31,6 +32,11 @@ module Gitlab end end + def all + REFERABLES.each { |referable| send(referable.to_s.pluralize) } + @references.values.flatten + end + private def reference_context diff --git a/spec/lib/banzai/pipeline/reference_unfold_pipeline_spec.rb b/spec/lib/banzai/pipeline/reference_unfold_pipeline_spec.rb deleted file mode 100644 index c02dc5a2c2c..00000000000 --- a/spec/lib/banzai/pipeline/reference_unfold_pipeline_spec.rb +++ /dev/null @@ -1,62 +0,0 @@ -require 'spec_helper' - -describe Banzai::Pipeline::ReferenceUnfoldPipeline do - let(:text) { 'some text' } - let(:old_project) { create(:project) } - let(:new_project) { create(:project) } - let(:pipeline_context) do - { project: old_project, new_project: new_project } - end - - let(:result) do - described_class.to_document(text, pipeline_context) - end - - context 'invalid initializers' do - subject { -> { result } } - - context 'project context is missing' do - let(:pipeline_context) { { new_project: new_project } } - it { is_expected.to raise_error ArgumentError, /Missing context keys/ } - end - - context 'new project context is missing' do - let(:pipeline_context) { { project: old_project } } - it { is_expected.to raise_error ArgumentError, /Missing context keys/ } - end - end - - context 'multiple issues and merge requests referenced' do - subject { result[:output] } - - let!(:issue_first) { create(:issue, project: old_project) } - let!(:issue_second) { create(:issue, project: old_project) } - let!(:merge_request) { create(:merge_request, source_project: old_project) } - - context 'plain text description' do - let(:text) { 'Description that references #1, #2 and !1' } - - it { is_expected.to include issue_first.to_reference(new_project) } - it { is_expected.to include issue_second.to_reference(new_project) } - it { is_expected.to include merge_request.to_reference(new_project) } - end - - context 'description with ignored elements' do - let(:text) do - "Hi. This references #1, but not `#2`\n" + - '
and not !1
' - end - - it { is_expected.to include issue_first.to_reference(new_project) } - it { is_expected.to_not include issue_second.to_reference(new_project) } - it { is_expected.to_not include merge_request.to_reference(new_project) } - end - - context 'description ambigous elements' do - let(:url) { 'http://gitlab.com/#1' } - let(:text) { "This references #1, but not #{url}" } - - it { is_expected.to include url } - end - end -end diff --git a/spec/lib/gitlab/gfm/reference_unfolder_spec.rb b/spec/lib/gitlab/gfm/reference_unfolder_spec.rb new file mode 100644 index 00000000000..4f8b960350c --- /dev/null +++ b/spec/lib/gitlab/gfm/reference_unfolder_spec.rb @@ -0,0 +1,43 @@ +require 'spec_helper' + +describe Gitlab::Gfm::ReferenceUnfolder do + let(:text) { 'some text' } + let(:old_project) { create(:project) } + let(:new_project) { create(:project) } + + describe '#unfold' do + subject { described_class.new(text, old_project).unfold(new_project) } + + context 'multiple issues and merge requests referenced' do + let!(:issue_first) { create(:issue, project: old_project) } + let!(:issue_second) { create(:issue, project: old_project) } + let!(:merge_request) { create(:merge_request, source_project: old_project) } + + context 'plain text description' do + let(:text) { 'Description that references #1, #2 and !1' } + + it { is_expected.to include issue_first.to_reference(new_project) } + it { is_expected.to include issue_second.to_reference(new_project) } + it { is_expected.to include merge_request.to_reference(new_project) } + end + + context 'description with ignored elements' do + let(:text) do + "Hi. This references #1, but not `#2`\n" + + '
and not !1
' + end + + it { is_expected.to include issue_first.to_reference(new_project) } + it { is_expected.to_not include issue_second.to_reference(new_project) } + it { is_expected.to_not include merge_request.to_reference(new_project) } + end + + context 'description ambigous elements' do + let(:url) { 'http://gitlab.com/#1' } + let(:text) { "This references #1, but not #{url}" } + + it { is_expected.to include url } + end + end + end +end diff --git a/spec/lib/gitlab/reference_extractor_spec.rb b/spec/lib/gitlab/reference_extractor_spec.rb index 7d963795e17..78ab162e5d6 100644 --- a/spec/lib/gitlab/reference_extractor_spec.rb +++ b/spec/lib/gitlab/reference_extractor_spec.rb @@ -122,4 +122,16 @@ describe Gitlab::ReferenceExtractor, lib: true do expect(extracted).to match_array([issue]) end end + + describe '#all' do + let(:issue) { create(:issue, project: project) } + let(:label) { create(:label, project: project) } + let(:text) { "Ref. #{issue.to_reference} and #{label.to_reference}" } + + before { subject.analyze(text) } + + it 'returns all referables' do + expect(subject.all).to match_array([issue, label]) + end + end end -- cgit v1.2.1 From 4354bfaba55c9238d5245fe2f5823665790c9817 Mon Sep 17 00:00:00 2001 From: Grzegorz Bizon Date: Fri, 11 Mar 2016 11:49:04 +0100 Subject: Add implementation of reference unfolder using banzai --- lib/banzai/filter/reference_filter.rb | 1 + lib/gitlab/gfm/reference_unfolder.rb | 53 +++++++++++++++++++++++--- spec/lib/gitlab/gfm/reference_unfolder_spec.rb | 32 ++++++++++++++-- 3 files changed, 78 insertions(+), 8 deletions(-) diff --git a/lib/banzai/filter/reference_filter.rb b/lib/banzai/filter/reference_filter.rb index 3637b1bac94..132f0a4bd93 100644 --- a/lib/banzai/filter/reference_filter.rb +++ b/lib/banzai/filter/reference_filter.rb @@ -47,6 +47,7 @@ module Banzai # Returns a String def data_attribute(attributes = {}) attributes[:reference_filter] = self.class.name.demodulize + attributes.delete(:original) if context[:no_original_data] attributes.map { |key, value| %Q(data-#{key.to_s.dasherize}="#{escape_once(value)}") }.join(" ") end diff --git a/lib/gitlab/gfm/reference_unfolder.rb b/lib/gitlab/gfm/reference_unfolder.rb index 4969e8ebe85..57871c36eb4 100644 --- a/lib/gitlab/gfm/reference_unfolder.rb +++ b/lib/gitlab/gfm/reference_unfolder.rb @@ -8,23 +8,66 @@ module Gitlab def initialize(text, project) @text = text @project = project + @original = markdown(text) end def unfold(from_project) - referables.each_with_object(@text.dup) do |referable, text| - next unless referable.respond_to?(:to_reference) + return @text unless @text =~ references_pattern - pattern = /#{Regexp.escape(referable.to_reference)}/ - text.gsub!(pattern, referable.to_reference(from_project)) + unfolded = @text.gsub(references_pattern) do |reference| + unfold_reference(reference, Regexp.last_match, from_project) end + + unless substitution_valid?(unfolded) + raise StandardError, 'Invalid references unfolding!' + end + + unfolded end private + def unfold_reference(reference, match, from_project) + before = @text[0...match.begin(0)] + after = @text[match.end(0)...@text.length] + referable = find_referable(reference) + + return reference unless referable + cross_reference = referable.to_reference(from_project) + new_text = before + cross_reference + after + + substitution_valid?(new_text) ? cross_reference : reference + end + + def references_pattern + return @pattern if @pattern + + patterns = Gitlab::ReferenceExtractor::REFERABLES.map do |ref| + ref.to_s.classify.constantize.try(:reference_pattern) + end + + @pattern = Regexp.union(patterns.compact) + end + def referables + return @referables if @referables + extractor = Gitlab::ReferenceExtractor.new(@project) extractor.analyze(@text) - extractor.all + @referables = extractor.all + end + + def find_referable(reference) + referables.find { |ref| ref.to_reference == reference } + end + + def substitution_valid?(substituted) + @original == markdown(substituted) + end + + def markdown(text) + helper = Class.new.extend(GitlabMarkdownHelper) + helper.markdown(text, project: @project, no_original_data: true) end end end diff --git a/spec/lib/gitlab/gfm/reference_unfolder_spec.rb b/spec/lib/gitlab/gfm/reference_unfolder_spec.rb index 4f8b960350c..40cdb7e1452 100644 --- a/spec/lib/gitlab/gfm/reference_unfolder_spec.rb +++ b/spec/lib/gitlab/gfm/reference_unfolder_spec.rb @@ -33,10 +33,36 @@ describe Gitlab::Gfm::ReferenceUnfolder do end context 'description ambigous elements' do - let(:url) { 'http://gitlab.com/#1' } - let(:text) { "This references #1, but not #{url}" } + context 'url' do + let(:url) { 'http://gitlab.com/#1' } + let(:text) { "This references #1, but not #{url}" } - it { is_expected.to include url } + it { is_expected.to include url } + end + + context 'code' do + let(:text) { "#1, but not `[#1]`" } + it { is_expected.to eq "#{issue_first.to_reference(new_project)}, but not `[#1]`" } + end + + context 'code reverse' do + let(:text) { "not `#1`, but #1" } + it { is_expected.to eq "not `#1`, but #{issue_first.to_reference(new_project)}" } + end + + context 'code in random order' do + let(:text) { "#1, `#1`, #1, `#1`" } + let(:ref) { issue_first.to_reference(new_project) } + + it { is_expected.to eq "#{ref}, `#1`, #{ref}, `#1`" } + end + end + + context 'reference contains milestone' do + let(:milestone) { create(:milestone) } + let(:text) { "milestone ref: #{milestone.to_reference}" } + + it { is_expected.to eq text } end end end -- cgit v1.2.1 From 310b417b0f2336e5e7a9b44da2fabf5d4abb0cb4 Mon Sep 17 00:00:00 2001 From: Grzegorz Bizon Date: Mon, 14 Mar 2016 08:25:37 +0100 Subject: Preserve original author when moving issue This also wrapps entire process into transation, as rewriting references may have large memory footprint. --- app/services/issues/move_service.rb | 40 +++++++++++++++---------------- spec/services/issues/move_service_spec.rb | 8 +++---- 2 files changed, 23 insertions(+), 25 deletions(-) diff --git a/app/services/issues/move_service.rb b/app/services/issues/move_service.rb index de12ea5c172..d1bded5c99f 100644 --- a/app/services/issues/move_service.rb +++ b/app/services/issues/move_service.rb @@ -4,7 +4,7 @@ module Issues super(project, current_user, params) @issue_old = issue - @issue_new = nil + @issue_new = issue.dup @project_old = @project @project_new = Project.find(new_project_id) if new_project_id end @@ -12,16 +12,18 @@ module Issues def execute return unless move? - # New issue tasks - # - open_new_issue - rewrite_notes - add_moved_from_note - - # Old issue tasks - # - add_moved_to_note - close_old_issue + ActiveRecord::Base.transaction do + # New issue tasks + # + open_new_issue + rewrite_notes + add_moved_from_note + + # Old issue tasks + # + add_moved_to_note + close_old_issue + end # Notifications # @@ -42,16 +44,12 @@ module Issues end def open_new_issue - create_service = CreateService.new(@project_new, current_user, new_issue_params) - @issue_new = create_service.execute - end - - def new_issue_params - new_params = { id: nil, iid: nil, milestone_id: nil, label_ids: [], - project_id: @project_new.id, - description: rewrite_references(@issue_old) } - - params.merge(new_params) + @issue_new.iid = nil + @issue_new.project = @project_new + @issue_new.labels = [] + @issue_new.milestone = nil + @issue_new.description = rewrite_references(@issue_old) + @issue_new.save! end def rewrite_notes diff --git a/spec/services/issues/move_service_spec.rb b/spec/services/issues/move_service_spec.rb index d516bd4830a..4fcfceaddd4 100644 --- a/spec/services/issues/move_service_spec.rb +++ b/spec/services/issues/move_service_spec.rb @@ -74,13 +74,13 @@ describe Issues::MoveService, services: true do expect(new_issue.persisted?).to be true end - it 'persist all changes' do + it 'persists all changes' do expect(old_issue.changed?).to be false - expect(new_issue.changed?).to be false + expect(new_issue.persisted?).to be true end - it 'changes author' do - expect(new_issue.author).to eq user + it 'preserves author' do + expect(new_issue.author).to eq author end it 'removes data that is invalid in new context' do -- cgit v1.2.1 From a91101b19a44d98005dd21b06d8509abcd82ddfc Mon Sep 17 00:00:00 2001 From: Grzegorz Bizon Date: Mon, 14 Mar 2016 08:30:37 +0100 Subject: Make it possible to move issue if user is a reporter Discussed it here: https://gitlab.com/gitlab-org/gitlab-ce/merge_requests/2831#note_4190228 --- app/models/ability.rb | 1 - app/services/issues/move_service.rb | 4 ++-- app/views/shared/issuable/_form.html.haml | 2 +- spec/services/issues/move_service_spec.rb | 20 ++++++++++---------- 4 files changed, 13 insertions(+), 14 deletions(-) diff --git a/app/models/ability.rb b/app/models/ability.rb index 5dff01a4e5d..ae1cf0c0f6b 100644 --- a/app/models/ability.rb +++ b/app/models/ability.rb @@ -223,7 +223,6 @@ class Ability :admin_project, :admin_commit_status, :admin_build, - :move_issue ] end diff --git a/app/services/issues/move_service.rb b/app/services/issues/move_service.rb index d1bded5c99f..d9f4692d377 100644 --- a/app/services/issues/move_service.rb +++ b/app/services/issues/move_service.rb @@ -39,8 +39,8 @@ module Issues private def can_move? - can?(@current_user, :move_issue, @project_old) && - can?(@current_user, :move_issue, @project_new) + can?(@current_user, :admin_issue, @project_old) && + can?(@current_user, :admin_issue, @project_new) end def open_new_issue diff --git a/app/views/shared/issuable/_form.html.haml b/app/views/shared/issuable/_form.html.haml index bb0252ba3de..5b3c0ee2734 100644 --- a/app/views/shared/issuable/_form.html.haml +++ b/app/views/shared/issuable/_form.html.haml @@ -67,7 +67,7 @@ - if can? current_user, :admin_label, issuable.project = link_to 'Create new label', new_namespace_project_label_path(issuable.project.namespace, issuable.project), target: :blank -- if issuable.is_a?(Issue) && can?(current_user, :move_issue, issuable.project) +- if issuable.is_a?(Issue) && can?(current_user, :admin_issue, issuable.project) %hr .form-group = f.label :move_to_project_id, 'Move', class: 'control-label' diff --git a/spec/services/issues/move_service_spec.rb b/spec/services/issues/move_service_spec.rb index 4fcfceaddd4..38fa5707cf7 100644 --- a/spec/services/issues/move_service_spec.rb +++ b/spec/services/issues/move_service_spec.rb @@ -24,8 +24,8 @@ describe Issues::MoveService, services: true do shared_context 'user can move issue' do before do - old_project.team << [user, :master] - new_project.team << [user, :master] + old_project.team << [user, :reporter] + new_project.team << [user, :reporter] end end @@ -186,25 +186,25 @@ describe Issues::MoveService, services: true do describe '#move?' do subject { move_service.move? } - context 'user is master in both projects' do + context 'user is reporter in both projects' do include_context 'user can move issue' it { is_expected.to be_truthy } end - context 'user is master only in new project' do - before { new_project.team << [user, :master] } + context 'user is reporter only in new project' do + before { new_project.team << [user, :reporter] } it { is_expected.to be_falsey } end - context 'user is master only in old project' do - before { old_project.team << [user, :master] } + context 'user is reporter only in old project' do + before { old_project.team << [user, :reporter] } it { is_expected.to be_falsey } end - context 'user is master in one project and developer in another' do + context 'user is reporter in one project and guest in another' do before do - new_project.team << [user, :developer] - old_project.team << [user, :master] + new_project.team << [user, :guest] + old_project.team << [user, :reporter] end it { is_expected.to be_falsey } -- cgit v1.2.1 From bcd5806960f3f74e27bc8e7c8a6fab3a044c181a Mon Sep 17 00:00:00 2001 From: Grzegorz Bizon Date: Mon, 14 Mar 2016 11:25:04 +0100 Subject: Add access-level filter support for projects select This also refactores ProjectSelect adding some decorator-like functions. --- app/assets/javascripts/project_select.js.coffee | 56 +++++++++++++++++-------- app/models/user.rb | 2 +- app/services/issues/move_service.rb | 4 +- app/views/shared/issuable/_form.html.haml | 3 +- 4 files changed, 43 insertions(+), 22 deletions(-) diff --git a/app/assets/javascripts/project_select.js.coffee b/app/assets/javascripts/project_select.js.coffee index 5b6a71714b2..c794d7ef397 100644 --- a/app/assets/javascripts/project_select.js.coffee +++ b/app/assets/javascripts/project_select.js.coffee @@ -1,10 +1,11 @@ class @ProjectSelect constructor: -> - $('.ajax-project-select').each (i, select) -> + $('.ajax-project-select').each (i, select) => @groupId = $(select).data('group-id') @includeGroups = $(select).data('include-groups') @orderBy = $(select).data('order-by') || 'id' @selectId = $(select).data('select-id') || 'web_url' + @accessLevel = $(select).data('access-level') placeholder = "Search for project" placeholder += " or group" if @includeGroups @@ -12,25 +13,11 @@ class @ProjectSelect $(select).select2 placeholder: placeholder minimumInputLength: 0 - query: (query) => - finalCallback = (projects) -> - data = { results: projects } - query.callback(data) - - if @includeGroups - projectsCallback = (projects) -> - groupsCallback = (groups) -> - data = groups.concat(projects) - finalCallback(data) - - Api.groups query.term, false, groupsCallback - else - projectsCallback = finalCallback - + query: (options) => if @groupId - Api.groupProjects @groupId, query.term, projectsCallback + Api.groupProjects @groupId, options.term, @createCallback(options) else - Api.projects query.term, @orderBy, projectsCallback + Api.projects options.term, @orderBy, @createCallback(options) id: (project) => project[@selectId] @@ -39,3 +26,36 @@ class @ProjectSelect project.name_with_namespace || project.name dropdownCssClass: "ajax-project-dropdown" + + createCallback: (options) => + finalCallback = (projects) -> + options.callback({ results: projects }) + + @accessLevelCallbackDecorator( + @groupsCallbackDecorator( + finalCallback + ) + ) + + groupsCallbackDecorator: (callback) => + return callback unless @includeGroups + + (projects) => + Api.groups options.term, false, (groups) => + data = groups.concat(projects) + callback(data) + + accessLevelCallbackDecorator: (callback) => + return callback unless @accessLevel + + ## + # Requires ECMAScript >= 5 + # + (projects) => + data = projects.filter (i) => + max = Math.max(i.permissions.group_access?.access_level ? 0, + i.permissions.project_access?.access_level ? 0) + + max >= @accessLevel + + callback(data) diff --git a/app/models/user.rb b/app/models/user.rb index 68b242888aa..5ba72dc0ae9 100644 --- a/app/models/user.rb +++ b/app/models/user.rb @@ -429,7 +429,7 @@ class User < ActiveRecord::Base Group.where("namespaces.id IN (#{union.to_sql})") end - # Returns the groups a user is authorized to access. + # Returns projects user is authorized to access. def authorized_projects Project.where("projects.id IN (#{projects_union.to_sql})") end diff --git a/app/services/issues/move_service.rb b/app/services/issues/move_service.rb index d9f4692d377..25f970f0801 100644 --- a/app/services/issues/move_service.rb +++ b/app/services/issues/move_service.rb @@ -15,7 +15,7 @@ module Issues ActiveRecord::Base.transaction do # New issue tasks # - open_new_issue + create_new_issue rewrite_notes add_moved_from_note @@ -43,7 +43,7 @@ module Issues can?(@current_user, :admin_issue, @project_new) end - def open_new_issue + def create_new_issue @issue_new.iid = nil @issue_new.project = @project_new @issue_new.labels = [] diff --git a/app/views/shared/issuable/_form.html.haml b/app/views/shared/issuable/_form.html.haml index 5b3c0ee2734..3d64738095f 100644 --- a/app/views/shared/issuable/_form.html.haml +++ b/app/views/shared/issuable/_form.html.haml @@ -73,7 +73,8 @@ = f.label :move_to_project_id, 'Move', class: 'control-label' .col-sm-10 = project_select_tag("#{issuable.class.model_name.param_key}[move_to_project_id]", - placeholder: 'Select project', class: 'custom-form-control', data: { 'select-id' => 'id' }) + placeholder: 'Select project', class: 'custom-form-control', + data: { 'select-id' => 'id', 'access-level' => Gitlab::Access::REPORTER }) - if issuable.is_a?(MergeRequest) %hr -- cgit v1.2.1 From 58df72fac1252e09ca6043ac8d78548882c062be Mon Sep 17 00:00:00 2001 From: Grzegorz Bizon Date: Mon, 14 Mar 2016 11:51:26 +0100 Subject: Make it possible to exclude project in projects select --- app/assets/javascripts/project_select.js.coffee | 27 +++++++++++++++++++------ app/views/shared/issuable/_form.html.haml | 6 +++--- 2 files changed, 24 insertions(+), 9 deletions(-) diff --git a/app/assets/javascripts/project_select.js.coffee b/app/assets/javascripts/project_select.js.coffee index c794d7ef397..4df61eb6ed6 100644 --- a/app/assets/javascripts/project_select.js.coffee +++ b/app/assets/javascripts/project_select.js.coffee @@ -6,6 +6,7 @@ class @ProjectSelect @orderBy = $(select).data('order-by') || 'id' @selectId = $(select).data('select-id') || 'web_url' @accessLevel = $(select).data('access-level') + @withoutId = $(select).data('without-id') placeholder = "Search for project" placeholder += " or group" if @includeGroups @@ -31,9 +32,11 @@ class @ProjectSelect finalCallback = (projects) -> options.callback({ results: projects }) - @accessLevelCallbackDecorator( - @groupsCallbackDecorator( - finalCallback + @withoutIdCallbackDecorator( + @accessLevelCallbackDecorator( + @groupsCallbackDecorator( + finalCallback + ) ) ) @@ -52,10 +55,22 @@ class @ProjectSelect # Requires ECMAScript >= 5 # (projects) => - data = projects.filter (i) => - max = Math.max(i.permissions.group_access?.access_level ? 0, - i.permissions.project_access?.access_level ? 0) + data = projects.filter (p) => + max = Math.max(p.permissions.group_access?.access_level ? 0, + p.permissions.project_access?.access_level ? 0) max >= @accessLevel callback(data) + + withoutIdCallbackDecorator: (callback) => + return callback unless @withoutId + + ## + # Requires ECMAScript >= 5 + # + (projects) => + data = projects.filter (p) => + p.id != @withoutId + + callback(data) diff --git a/app/views/shared/issuable/_form.html.haml b/app/views/shared/issuable/_form.html.haml index 3d64738095f..f9160a46093 100644 --- a/app/views/shared/issuable/_form.html.haml +++ b/app/views/shared/issuable/_form.html.haml @@ -72,9 +72,9 @@ .form-group = f.label :move_to_project_id, 'Move', class: 'control-label' .col-sm-10 - = project_select_tag("#{issuable.class.model_name.param_key}[move_to_project_id]", - placeholder: 'Select project', class: 'custom-form-control', - data: { 'select-id' => 'id', 'access-level' => Gitlab::Access::REPORTER }) + = project_select_tag(:move_to_project_id, placeholder: 'Select project', + class: 'custom-form-control', data: { 'select-id' => 'id', + 'access-level' => Gitlab::Access::REPORTER, 'without-id' => issuable.project.id }) - if issuable.is_a?(MergeRequest) %hr -- cgit v1.2.1 From 93812f2594ab871d613e29ff33ef4eefe298aeaa Mon Sep 17 00:00:00 2001 From: Grzegorz Bizon Date: Mon, 14 Mar 2016 13:24:56 +0100 Subject: Add initial notifications and hooks for issue move --- app/assets/javascripts/project_select.js.coffee | 6 ---- app/services/issues/move_service.rb | 37 ++++++++++++++++++++----- app/services/notification_service.rb | 8 ++++++ 3 files changed, 38 insertions(+), 13 deletions(-) diff --git a/app/assets/javascripts/project_select.js.coffee b/app/assets/javascripts/project_select.js.coffee index 4df61eb6ed6..9738a66ecb9 100644 --- a/app/assets/javascripts/project_select.js.coffee +++ b/app/assets/javascripts/project_select.js.coffee @@ -51,9 +51,6 @@ class @ProjectSelect accessLevelCallbackDecorator: (callback) => return callback unless @accessLevel - ## - # Requires ECMAScript >= 5 - # (projects) => data = projects.filter (p) => max = Math.max(p.permissions.group_access?.access_level ? 0, @@ -66,9 +63,6 @@ class @ProjectSelect withoutIdCallbackDecorator: (callback) => return callback unless @withoutId - ## - # Requires ECMAScript >= 5 - # (projects) => data = projects.filter (p) => p.id != @withoutId diff --git a/app/services/issues/move_service.rb b/app/services/issues/move_service.rb index 25f970f0801..646ff876211 100644 --- a/app/services/issues/move_service.rb +++ b/app/services/issues/move_service.rb @@ -6,7 +6,7 @@ module Issues @issue_old = issue @issue_new = issue.dup @project_old = @project - @project_new = Project.find(new_project_id) if new_project_id + @project_new = Project.find(new_project_id) end def execute @@ -25,9 +25,10 @@ module Issues close_old_issue end - # Notifications + # Notifications and hooks # - notify_participants + # notify_participants + # trigger_hooks_and_events @issue_new end @@ -44,10 +45,18 @@ module Issues end def create_new_issue - @issue_new.iid = nil @issue_new.project = @project_new + + # Reset internal ID, will be regenerated before save + # + @issue_new.iid = nil + + # Reset labels and milestones, as those are not valid in context + # of a new project + # @issue_new.labels = [] @issue_new.milestone = nil + @issue_new.description = rewrite_references(@issue_old) @issue_new.save! end @@ -66,9 +75,6 @@ module Issues @issue_old.update(state: :closed) end - def notify_participants - end - def add_moved_from_note SystemNoteService.noteable_moved(:from, @issue_new, @project_new, @issue_old, @current_user) @@ -95,5 +101,22 @@ module Issues raise 'Unexpected noteable while moving an issue' end end + + def trigger_hooks_and_events + event_service.close_issue(@issue_old, @current_user) + event_service.open_issue(@issue_new, @current_user) + + @issue_new.create_cross_references!(@current_user) + + execute_hooks(@issue_old, 'close') + execute_hooks(@issue_new, 'open') + end + + def notify_participants + todo_service.close_issue(@issue_old, @current_user) + todo_service.open_issue(@issue_new, @current_user) + + notification_service.issue_moved(@issue_old, @issue_new, @current_user) + end end end diff --git a/app/services/notification_service.rb b/app/services/notification_service.rb index 19a6779dea9..d90e630ff50 100644 --- a/app/services/notification_service.rb +++ b/app/services/notification_service.rb @@ -236,6 +236,14 @@ class NotificationService end end + def issue_moved(issue, old_project, new_project, current_user) + recipients = build_recipients(issue, old_project, current_user) + + recipients.each do |recipient| + mailer.send('issue_moved', recipient.id, issue.id, current_user.id).deliver_later + end + end + protected # Get project users with WATCH notification level -- cgit v1.2.1 From a23f0e8c9ebd3a7922786d2fc4f3450c1fdecad6 Mon Sep 17 00:00:00 2001 From: Grzegorz Bizon Date: Tue, 15 Mar 2016 10:14:39 +0100 Subject: Reuse existing issue services when moving issue --- app/services/issues/close_service.rb | 6 ++-- app/services/issues/create_service.rb | 4 +-- app/services/issues/move_service.rb | 58 +++++++++++-------------------- app/services/notification_service.rb | 8 ----- spec/services/issues/move_service_spec.rb | 2 +- 5 files changed, 26 insertions(+), 52 deletions(-) diff --git a/app/services/issues/close_service.rb b/app/services/issues/close_service.rb index 78254b49af3..6866d3fcfb9 100644 --- a/app/services/issues/close_service.rb +++ b/app/services/issues/close_service.rb @@ -1,6 +1,6 @@ module Issues class CloseService < Issues::BaseService - def execute(issue, commit = nil) + def execute(issue, commit = nil, notifications: true, system_note: true) if project.jira_tracker? && project.jira_service.active project.jira_service.execute(commit, issue) todo_service.close_issue(issue, current_user) @@ -9,8 +9,8 @@ module Issues if project.default_issues_tracker? && issue.close event_service.close_issue(issue, current_user) - create_note(issue, commit) - notification_service.close_issue(issue, current_user) + create_note(issue, commit) if system_note + notification_service.close_issue(issue, current_user) if notifications todo_service.close_issue(issue, current_user) execute_hooks(issue, 'close') end diff --git a/app/services/issues/create_service.rb b/app/services/issues/create_service.rb index 10787e8873c..8af1a9b8d3b 100644 --- a/app/services/issues/create_service.rb +++ b/app/services/issues/create_service.rb @@ -1,10 +1,10 @@ module Issues class CreateService < Issues::BaseService - def execute + def execute(set_author: true) filter_params label_params = params[:label_ids] issue = project.issues.new(params.except(:label_ids)) - issue.author = current_user + issue.author = current_user if set_author if issue.save issue.update_attributes(label_ids: label_params) diff --git a/app/services/issues/move_service.rb b/app/services/issues/move_service.rb index 646ff876211..e2089e22bcb 100644 --- a/app/services/issues/move_service.rb +++ b/app/services/issues/move_service.rb @@ -4,14 +4,20 @@ module Issues super(project, current_user, params) @issue_old = issue - @issue_new = issue.dup + @issue_new = nil @project_old = @project - @project_new = Project.find(new_project_id) + + if new_project_id + @project_new = Project.find(new_project_id) + end end def execute return unless move? + # Using trasaction because of a high footprint on + # rewriting notes (unfolding references) + # ActiveRecord::Base.transaction do # New issue tasks # @@ -25,10 +31,7 @@ module Issues close_old_issue end - # Notifications and hooks - # - # notify_participants - # trigger_hooks_and_events + notify_participants @issue_new end @@ -45,20 +48,14 @@ module Issues end def create_new_issue - @issue_new.project = @project_new - - # Reset internal ID, will be regenerated before save - # - @issue_new.iid = nil + new_params = { id: nil, iid: nil, milestone: nil, label_ids: [], + project: @project_new, author: @issue_old.author, + description: rewrite_references(@issue_old) } - # Reset labels and milestones, as those are not valid in context - # of a new project - # - @issue_new.labels = [] - @issue_new.milestone = nil + create_service = CreateService.new(@project_new, @current_user, + params.merge(new_params)) - @issue_new.description = rewrite_references(@issue_old) - @issue_new.save! + @issue_new = create_service.execute(set_author: false) end def rewrite_notes @@ -72,7 +69,8 @@ module Issues end def close_old_issue - @issue_old.update(state: :closed) + close_service = CloseService.new(@project_new, @current_user) + close_service.execute(@issue_old, notifications: false, system_note: false) end def add_moved_from_note @@ -93,30 +91,14 @@ module Issues def noteable_content(noteable) case noteable - when Issue - noteable.description - when Note - noteable.note + when Issue then noteable.description + when Note then noteable.note else - raise 'Unexpected noteable while moving an issue' + raise 'Unexpected noteable while moving an issue!' end end - def trigger_hooks_and_events - event_service.close_issue(@issue_old, @current_user) - event_service.open_issue(@issue_new, @current_user) - - @issue_new.create_cross_references!(@current_user) - - execute_hooks(@issue_old, 'close') - execute_hooks(@issue_new, 'open') - end - def notify_participants - todo_service.close_issue(@issue_old, @current_user) - todo_service.open_issue(@issue_new, @current_user) - - notification_service.issue_moved(@issue_old, @issue_new, @current_user) end end end diff --git a/app/services/notification_service.rb b/app/services/notification_service.rb index d90e630ff50..19a6779dea9 100644 --- a/app/services/notification_service.rb +++ b/app/services/notification_service.rb @@ -236,14 +236,6 @@ class NotificationService end end - def issue_moved(issue, old_project, new_project, current_user) - recipients = build_recipients(issue, old_project, current_user) - - recipients.each do |recipient| - mailer.send('issue_moved', recipient.id, issue.id, current_user.id).deliver_later - end - end - protected # Get project users with WATCH notification level diff --git a/spec/services/issues/move_service_spec.rb b/spec/services/issues/move_service_spec.rb index 38fa5707cf7..69f4748ae67 100644 --- a/spec/services/issues/move_service_spec.rb +++ b/spec/services/issues/move_service_spec.rb @@ -76,7 +76,7 @@ describe Issues::MoveService, services: true do it 'persists all changes' do expect(old_issue.changed?).to be false - expect(new_issue.persisted?).to be true + expect(new_issue.changed?).to be false end it 'preserves author' do -- cgit v1.2.1 From 15d32b6a11380c257180dbc8e2691bc12e2792bc Mon Sep 17 00:00:00 2001 From: Grzegorz Bizon Date: Tue, 15 Mar 2016 11:35:40 +0100 Subject: Add new notifications for issue move action [ci skip] --- app/mailers/emails/issues.rb | 8 ++++++++ app/services/issues/move_service.rb | 9 +++++++-- app/services/notification_service.rb | 10 ++++++++++ app/views/notify/issue_moved_email.html.haml | 6 ++++++ app/views/notify/issue_moved_email.text.erb | 4 ++++ spec/mailers/notify_spec.rb | 27 +++++++++++++++++++++++++++ spec/services/issues/move_service_spec.rb | 2 +- 7 files changed, 63 insertions(+), 3 deletions(-) create mode 100644 app/views/notify/issue_moved_email.html.haml create mode 100644 app/views/notify/issue_moved_email.text.erb diff --git a/app/mailers/emails/issues.rb b/app/mailers/emails/issues.rb index 5f9adb32e00..6f54c42146c 100644 --- a/app/mailers/emails/issues.rb +++ b/app/mailers/emails/issues.rb @@ -36,6 +36,14 @@ module Emails mail_answer_thread(@issue, issue_thread_options(updated_by_user_id, recipient_id)) end + def issue_moved_email(recipient, issue, new_issue, updated_by_user) + setup_issue_mail(issue.id, recipient.id) + + @new_issue = new_issue + @new_project = new_issue.project + mail_answer_thread(issue, issue_thread_options(updated_by_user.id, recipient.id)) + end + private def setup_issue_mail(issue_id, recipient_id) diff --git a/app/services/issues/move_service.rb b/app/services/issues/move_service.rb index e2089e22bcb..4abcd203407 100644 --- a/app/services/issues/move_service.rb +++ b/app/services/issues/move_service.rb @@ -10,13 +10,17 @@ module Issues if new_project_id @project_new = Project.find(new_project_id) end + + if @project_new == @project_old + raise StandardError, 'Cannot move issue to project it originates from!' + end end def execute return unless move? - # Using trasaction because of a high footprint on - # rewriting notes (unfolding references) + # Using trasaction because of a high resources footprint + # on rewriting notes (unfolding references) # ActiveRecord::Base.transaction do # New issue tasks @@ -99,6 +103,7 @@ module Issues end def notify_participants + notification_service.issue_moved(@issue_old, @issue_new, @current_user) end end end diff --git a/app/services/notification_service.rb b/app/services/notification_service.rb index 19a6779dea9..3bdf00a8291 100644 --- a/app/services/notification_service.rb +++ b/app/services/notification_service.rb @@ -236,6 +236,16 @@ class NotificationService end end + def issue_moved(issue, new_issue, current_user) + recipients = build_recipients(issue, issue.project, current_user) + + recipients.map do |recipient| + email = mailer.issue_moved_email(recipient, issue, new_issue, current_user) + email.deliver_later + email + end + end + protected # Get project users with WATCH notification level diff --git a/app/views/notify/issue_moved_email.html.haml b/app/views/notify/issue_moved_email.html.haml new file mode 100644 index 00000000000..40f7d61fe19 --- /dev/null +++ b/app/views/notify/issue_moved_email.html.haml @@ -0,0 +1,6 @@ +%p + Issue was moved to another project. +%p + New issue: + = link_to namespace_project_issue_url(@new_project.namespace, @new_project, @new_issue) do + = @new_issue.title diff --git a/app/views/notify/issue_moved_email.text.erb b/app/views/notify/issue_moved_email.text.erb new file mode 100644 index 00000000000..b3bd43c2055 --- /dev/null +++ b/app/views/notify/issue_moved_email.text.erb @@ -0,0 +1,4 @@ +Issue was moved to another project. + +New issue location: +<%= namespace_project_issue_url(@new_project.namespace, @new_project, @new_issue) %> diff --git a/spec/mailers/notify_spec.rb b/spec/mailers/notify_spec.rb index f910424d85b..9b47acfe0cd 100644 --- a/spec/mailers/notify_spec.rb +++ b/spec/mailers/notify_spec.rb @@ -158,6 +158,33 @@ describe Notify do is_expected.to have_body_text /#{namespace_project_issue_path project.namespace, project, issue}/ end end + + describe 'moved to another project' do + let(:new_issue) { create(:issue) } + subject { Notify.issue_moved_email(recipient, issue, new_issue, current_user) } + + it_behaves_like 'an answer to an existing thread', 'issue' + it_behaves_like 'it should show Gmail Actions View Issue link' + it_behaves_like 'an unsubscribeable thread' + + it 'contains description about action taken' do + is_expected.to have_body_text 'Issue was moved to another project' + end + + it 'has the correct subject' do + is_expected.to have_subject /#{issue.title} \(##{issue.iid}\)/i + end + + it 'contains link to new issue' do + new_issue_url = namespace_project_issue_path(new_issue.project.namespace, + new_issue.project, new_issue) + is_expected.to have_body_text new_issue_url + end + + it 'contains a link to the original issue' do + is_expected.to have_body_text /#{namespace_project_issue_path project.namespace, project, issue}/ + end + end end context 'for merge requests' do diff --git a/spec/services/issues/move_service_spec.rb b/spec/services/issues/move_service_spec.rb index 69f4748ae67..b71ce311afd 100644 --- a/spec/services/issues/move_service_spec.rb +++ b/spec/services/issues/move_service_spec.rb @@ -55,7 +55,7 @@ describe Issues::MoveService, services: true do end it 'rewrites issue description' do - expect(new_issue.description).to include description + expect(new_issue.description).to eq description end it 'adds system note to old issue at the end' do -- cgit v1.2.1 From 414558939339bd636a3549866ce532fa25500bc5 Mon Sep 17 00:00:00 2001 From: Grzegorz Bizon Date: Tue, 15 Mar 2016 14:59:38 +0100 Subject: Revert changes in js used to create project select This will preserve those changes in history, we may need them in future. Currently we will use a issues helpers to create options to choose project whem moving issue to it. --- app/assets/javascripts/project_select.js.coffee | 70 +++++++------------------ 1 file changed, 20 insertions(+), 50 deletions(-) diff --git a/app/assets/javascripts/project_select.js.coffee b/app/assets/javascripts/project_select.js.coffee index 9738a66ecb9..be8ab9b428d 100644 --- a/app/assets/javascripts/project_select.js.coffee +++ b/app/assets/javascripts/project_select.js.coffee @@ -1,12 +1,9 @@ class @ProjectSelect constructor: -> - $('.ajax-project-select').each (i, select) => + $('.ajax-project-select').each (i, select) -> @groupId = $(select).data('group-id') @includeGroups = $(select).data('include-groups') @orderBy = $(select).data('order-by') || 'id' - @selectId = $(select).data('select-id') || 'web_url' - @accessLevel = $(select).data('access-level') - @withoutId = $(select).data('without-id') placeholder = "Search for project" placeholder += " or group" if @includeGroups @@ -14,57 +11,30 @@ class @ProjectSelect $(select).select2 placeholder: placeholder minimumInputLength: 0 - query: (options) => + query: (query) => + finalCallback = (projects) -> + data = { results: projects } + query.callback(data) + + if @includeGroups + projectsCallback = (projects) -> + groupsCallback = (groups) -> + data = groups.concat(projects) + finalCallback(data) + + Api.groups query.term, false, groupsCallback + else + projectsCallback = finalCallback + if @groupId - Api.groupProjects @groupId, options.term, @createCallback(options) + Api.groupProjects @groupId, query.term, projectsCallback else - Api.projects options.term, @orderBy, @createCallback(options) + Api.projects query.term, @orderBy, projectsCallback - id: (project) => - project[@selectId] + id: (project) -> + project.web_url text: (project) -> project.name_with_namespace || project.name dropdownCssClass: "ajax-project-dropdown" - - createCallback: (options) => - finalCallback = (projects) -> - options.callback({ results: projects }) - - @withoutIdCallbackDecorator( - @accessLevelCallbackDecorator( - @groupsCallbackDecorator( - finalCallback - ) - ) - ) - - groupsCallbackDecorator: (callback) => - return callback unless @includeGroups - - (projects) => - Api.groups options.term, false, (groups) => - data = groups.concat(projects) - callback(data) - - accessLevelCallbackDecorator: (callback) => - return callback unless @accessLevel - - (projects) => - data = projects.filter (p) => - max = Math.max(p.permissions.group_access?.access_level ? 0, - p.permissions.project_access?.access_level ? 0) - - max >= @accessLevel - - callback(data) - - withoutIdCallbackDecorator: (callback) => - return callback unless @withoutId - - (projects) => - data = projects.filter (p) => - p.id != @withoutId - - callback(data) -- cgit v1.2.1 From 1dd279d83335de71c69d0acfdcdd7eb0ebe7f3dd Mon Sep 17 00:00:00 2001 From: Grzegorz Bizon Date: Tue, 15 Mar 2016 15:01:26 +0100 Subject: Use helper to create list of projects issue can be moved to This also adds confirmation message if issue move has been requested. --- app/assets/javascripts/issuable_form.js.coffee | 11 ++++++++++- app/helpers/issues_helper.rb | 11 +++++++++++ app/views/shared/issuable/_form.html.haml | 6 +++--- spec/services/issues/move_service_spec.rb | 15 +++++++++++++-- 4 files changed, 37 insertions(+), 6 deletions(-) diff --git a/app/assets/javascripts/issuable_form.js.coffee b/app/assets/javascripts/issuable_form.js.coffee index 48c249943f2..9b939985b33 100644 --- a/app/assets/javascripts/issuable_form.js.coffee +++ b/app/assets/javascripts/issuable_form.js.coffee @@ -1,4 +1,6 @@ class @IssuableForm + ISSUE_MOVE_CONFIRM_MSG = 'Are you sure you want to move this issue to another project?' + constructor: (@form) -> GitLab.GfmAutoComplete.setup() new UsersSelect() @@ -6,12 +8,13 @@ class @IssuableForm @titleField = @form.find("input[name*='[title]']") @descriptionField = @form.find("textarea[name*='[description]']") + @issueMoveField = @form.find("#move_to_project_id") return unless @titleField.length && @descriptionField.length @initAutosave() - @form.on "submit", @resetAutosave + @form.on "submit", @handleSubmit @form.on "click", ".btn-cancel", @resetAutosave initAutosave: -> @@ -27,6 +30,12 @@ class @IssuableForm "description" ] + handleSubmit: (e) => + @resetAutosave + + if (parseInt(@issueMoveField?.val()) ? 0) > 0 + e.preventDefault() unless confirm(ISSUE_MOVE_CONFIRM_MSG) + resetAutosave: => @titleField.data("autosave").reset() @descriptionField.data("autosave").reset() diff --git a/app/helpers/issues_helper.rb b/app/helpers/issues_helper.rb index ae4ebc0854a..62479b8d1ce 100644 --- a/app/helpers/issues_helper.rb +++ b/app/helpers/issues_helper.rb @@ -57,6 +57,17 @@ module IssuesHelper options_from_collection_for_select(milestones, 'id', 'title', object.milestone_id) end + def project_options(issuable, current_user, ability: :read_project) + projects = current_user.authorized_projects + projects = projects.select do |project| + current_user.can?(ability, project) && project != issuable.project + end + + projects.unshift(OpenStruct.new(id: 0, name_with_namespace: 'No project')) + + options_from_collection_for_select(projects, :id, :name_with_namespace, 0) + end + def status_box_class(item) if item.respond_to?(:expired?) && item.expired? 'status-box-expired' diff --git a/app/views/shared/issuable/_form.html.haml b/app/views/shared/issuable/_form.html.haml index f9160a46093..11b8b9d44af 100644 --- a/app/views/shared/issuable/_form.html.haml +++ b/app/views/shared/issuable/_form.html.haml @@ -72,9 +72,9 @@ .form-group = f.label :move_to_project_id, 'Move', class: 'control-label' .col-sm-10 - = project_select_tag(:move_to_project_id, placeholder: 'Select project', - class: 'custom-form-control', data: { 'select-id' => 'id', - 'access-level' => Gitlab::Access::REPORTER, 'without-id' => issuable.project.id }) + - projects = project_options(issuable, current_user, ability: :admin_issue) + = select_tag(:move_to_project_id, projects, include_blank: true, + class: 'select2', data: { placeholder: 'Select project' }) - if issuable.is_a?(MergeRequest) %hr diff --git a/spec/services/issues/move_service_spec.rb b/spec/services/issues/move_service_spec.rb index b71ce311afd..cb02b8721ac 100644 --- a/spec/services/issues/move_service_spec.rb +++ b/spec/services/issues/move_service_spec.rb @@ -28,7 +28,7 @@ describe Issues::MoveService, services: true do new_project.team << [user, :reporter] end end - + context 'issue movable' do include_context 'issue move requested' include_context 'user can move issue' @@ -162,6 +162,18 @@ describe Issues::MoveService, services: true do end end + context 'moving to same project' do + let(:new_project) { old_project } + + include_context 'issue move requested' + include_context 'user can move issue' + + it 'raises error' do + expect { move_service } + .to raise_error(StandardError, /Cannot move issue/) + end + end + context 'issue move not requested' do let(:new_project_id) { nil } @@ -179,7 +191,6 @@ describe Issues::MoveService, services: true do end end - describe 'move permissions' do include_context 'issue move requested' -- cgit v1.2.1 From 5e3c9475a9332d7104a791f7bdfef30e069dd848 Mon Sep 17 00:00:00 2001 From: Grzegorz Bizon Date: Wed, 16 Mar 2016 10:24:44 +0100 Subject: Add minor improvements in code related to issue move --- app/assets/javascripts/issuable_form.js.coffee | 8 +++---- app/helpers/issues_helper.rb | 8 ++++--- app/services/issues/move_service.rb | 31 +++++++++----------------- app/services/system_note_service.rb | 10 ++++----- app/views/shared/issuable/_form.html.haml | 2 +- lib/gitlab/gfm/reference_unfolder.rb | 27 +++++++++++++++++++--- spec/services/system_note_service_spec.rb | 2 +- 7 files changed, 50 insertions(+), 38 deletions(-) diff --git a/app/assets/javascripts/issuable_form.js.coffee b/app/assets/javascripts/issuable_form.js.coffee index 9b939985b33..d60832f90ed 100644 --- a/app/assets/javascripts/issuable_form.js.coffee +++ b/app/assets/javascripts/issuable_form.js.coffee @@ -30,11 +30,11 @@ class @IssuableForm "description" ] - handleSubmit: (e) => - @resetAutosave - + handleSubmit: => if (parseInt(@issueMoveField?.val()) ? 0) > 0 - e.preventDefault() unless confirm(ISSUE_MOVE_CONFIRM_MSG) + return false unless confirm(ISSUE_MOVE_CONFIRM_MSG) + + @resetAutosave() resetAutosave: => @titleField.data("autosave").reset() diff --git a/app/helpers/issues_helper.rb b/app/helpers/issues_helper.rb index 62479b8d1ce..b67057ebc7c 100644 --- a/app/helpers/issues_helper.rb +++ b/app/helpers/issues_helper.rb @@ -60,12 +60,14 @@ module IssuesHelper def project_options(issuable, current_user, ability: :read_project) projects = current_user.authorized_projects projects = projects.select do |project| - current_user.can?(ability, project) && project != issuable.project + current_user.can?(ability, project) end - projects.unshift(OpenStruct.new(id: 0, name_with_namespace: 'No project')) + no_project = OpenStruct.new(id: 0, name_with_namespace: 'No project') + projects.unshift(no_project) + projects.delete(issuable.project) - options_from_collection_for_select(projects, :id, :name_with_namespace, 0) + options_from_collection_for_select(projects, :id, :name_with_namespace) end def status_box_class(item) diff --git a/app/services/issues/move_service.rb b/app/services/issues/move_service.rb index 4abcd203407..ce31830f2d0 100644 --- a/app/services/issues/move_service.rb +++ b/app/services/issues/move_service.rb @@ -7,7 +7,7 @@ module Issues @issue_new = nil @project_old = @project - if new_project_id + if new_project_id.to_i > 0 @project_new = Project.find(new_project_id) end @@ -19,7 +19,7 @@ module Issues def execute return unless move? - # Using trasaction because of a high resources footprint + # Using transaction because of a high resources footprint # on rewriting notes (unfolding references) # ActiveRecord::Base.transaction do @@ -54,10 +54,11 @@ module Issues def create_new_issue new_params = { id: nil, iid: nil, milestone: nil, label_ids: [], project: @project_new, author: @issue_old.author, - description: rewrite_references(@issue_old) } + description: unfold_references(@issue_old.description) } + new_params = @issue_old.serializable_hash.merge(new_params) create_service = CreateService.new(@project_new, @current_user, - params.merge(new_params)) + new_params) @issue_new = create_service.execute(set_author: false) end @@ -66,7 +67,7 @@ module Issues @issue_old.notes.find_each do |note| new_note = note.dup new_params = { project: @project_new, noteable: @issue_new, - note: rewrite_references(new_note) } + note: unfold_references(new_note.note) } new_note.update(new_params) end @@ -78,30 +79,20 @@ module Issues end def add_moved_from_note - SystemNoteService.noteable_moved(:from, @issue_new, @project_new, - @issue_old, @current_user) + SystemNoteService.noteable_moved(@issue_new, @project_new, + @issue_old, @current_user, direction: :from) end def add_moved_to_note - SystemNoteService.noteable_moved(:to, @issue_old, @project_old, - @issue_new, @current_user) + SystemNoteService.noteable_moved(@issue_old, @project_old, + @issue_new, @current_user, direction: :to) end - def rewrite_references(noteable) - content = noteable_content(noteable).dup + def unfold_references(content) unfolder = Gitlab::Gfm::ReferenceUnfolder.new(content, @project_old) unfolder.unfold(@project_new) end - def noteable_content(noteable) - case noteable - when Issue then noteable.description - when Note then noteable.note - else - raise 'Unexpected noteable while moving an issue!' - end - end - def notify_participants notification_service.issue_moved(@issue_old, @issue_new, @current_user) end diff --git a/app/services/system_note_service.rb b/app/services/system_note_service.rb index a8d6555dec3..c679891b07c 100644 --- a/app/services/system_note_service.rb +++ b/app/services/system_note_service.rb @@ -3,7 +3,6 @@ # Used for creating system notes (e.g., when a user references a merge request # from an issue, an issue's assignee changes, an issue is closed, etc.) class SystemNoteService - extend GitlabMarkdownHelper # Called when commits are added to a Merge Request # # noteable - Noteable object @@ -398,16 +397,15 @@ class SystemNoteService # # Example Note text: # - # "Moved to project_new/#11" + # "Moved to some_namespace/project_new#11" # # Returns the created Note object - def self.noteable_moved(direction, noteable, project, noteable_ref, author) + def self.noteable_moved(noteable, project, noteable_ref, author, direction:) unless [:to, :from].include?(direction) - raise StandardError, "Invalid direction `#{direction}`" + raise ArgumentError, "Invalid direction `#{direction}`" end - cross_reference = cross_project_reference(noteable_ref.project, noteable_ref) - + cross_reference = noteable_ref.to_reference(project) body = "Moved #{direction} #{cross_reference}" create_note(noteable: noteable, project: project, author: author, note: body) end diff --git a/app/views/shared/issuable/_form.html.haml b/app/views/shared/issuable/_form.html.haml index 11b8b9d44af..d3eabe9ea64 100644 --- a/app/views/shared/issuable/_form.html.haml +++ b/app/views/shared/issuable/_form.html.haml @@ -70,7 +70,7 @@ - if issuable.is_a?(Issue) && can?(current_user, :admin_issue, issuable.project) %hr .form-group - = f.label :move_to_project_id, 'Move', class: 'control-label' + = label_tag :move_to_project_id, 'Move', class: 'control-label' .col-sm-10 - projects = project_options(issuable, current_user, ability: :admin_issue) = select_tag(:move_to_project_id, projects, include_blank: true, diff --git a/lib/gitlab/gfm/reference_unfolder.rb b/lib/gitlab/gfm/reference_unfolder.rb index 57871c36eb4..0a68d6f977f 100644 --- a/lib/gitlab/gfm/reference_unfolder.rb +++ b/lib/gitlab/gfm/reference_unfolder.rb @@ -1,8 +1,30 @@ module Gitlab module Gfm ## - # Class than unfolds local references in text. + # Class that unfolds local references in text. # + # The initializer takes text in Markdown and project this text is valid + # in context of. + # + # `unfold` method tries to find all local references and unfold each of + # those local references to cross reference format. + # + # Examples: + # + # 'Hello, this issue is related to #123 and + # other issues labeled with ~"label"', will be converted to: + # + # 'Hello, this issue is related to gitlab-org/gitlab-ce#123 and + # other issue labeled with gitlab-org/gitlab-ce~"label"'. + # + # It does respect markdown lexical rules, so text in code block will not be + # replaced, see another example: + # + # 'Merge request for issue #1234, see also link: + # http://gitlab.com/some/link/#1234, and code `puts #1234`' => + # + # 'Merge request for issue gitlab-org/gitlab-ce#1234, se also link: + # http://gitlab.com/some/link/#1234, and code `puts #1234`' # class ReferenceUnfolder def initialize(text, project) @@ -66,8 +88,7 @@ module Gitlab end def markdown(text) - helper = Class.new.extend(GitlabMarkdownHelper) - helper.markdown(text, project: @project, no_original_data: true) + Banzai.render(text, project: @project, no_original_data: true) end end end diff --git a/spec/services/system_note_service_spec.rb b/spec/services/system_note_service_spec.rb index 7efb9f2ddba..7c93ce304f9 100644 --- a/spec/services/system_note_service_spec.rb +++ b/spec/services/system_note_service_spec.rb @@ -446,7 +446,7 @@ describe SystemNoteService, services: true do let(:new_noteable) { create(:issue, project: new_project) } subject do - described_class.noteable_moved(direction, noteable, project, new_noteable, author) + described_class.noteable_moved(noteable, project, new_noteable, author, direction: direction) end shared_examples 'cross project mentionable' do -- cgit v1.2.1 From e3d32ff036ab442b9613d4dbceb473f30da048af Mon Sep 17 00:00:00 2001 From: Grzegorz Bizon Date: Wed, 16 Mar 2016 15:11:07 +0100 Subject: Add feature specs for issue move --- app/models/ability.rb | 2 +- spec/features/issues/move_spec.rb | 77 +++++++++++++++++++++++++++++++++++++++ 2 files changed, 78 insertions(+), 1 deletion(-) create mode 100644 spec/features/issues/move_spec.rb diff --git a/app/models/ability.rb b/app/models/ability.rb index ae1cf0c0f6b..fe9e0aab717 100644 --- a/app/models/ability.rb +++ b/app/models/ability.rb @@ -222,7 +222,7 @@ class Ability :admin_wiki, :admin_project, :admin_commit_status, - :admin_build, + :admin_build ] end diff --git a/spec/features/issues/move_spec.rb b/spec/features/issues/move_spec.rb new file mode 100644 index 00000000000..6303b117c09 --- /dev/null +++ b/spec/features/issues/move_spec.rb @@ -0,0 +1,77 @@ +require 'rails_helper' + +feature 'issue move to another project' do + let(:user) { create(:user) } + let(:old_project) { create(:project) } + let(:text) { 'Some issue description' } + + let(:issue) do + create(:issue, description: text, project: old_project, author: user) + end + + background { login_as(user) } + + context 'user does not have permission to move issue' do + background do + old_project.team << [user, :guest] + + edit_issue(issue) + end + + scenario 'moving issue to another project not allowed' do + expect(page).to have_no_select('move_to_project_id') + end + end + + context 'user has permission to move issue' do + let!(:mr) { create(:merge_request, source_project: old_project) } + let(:new_project) { create(:project) } + let(:text) { 'Text with !1' } + let(:cross_reference) { old_project.to_reference } + + background do + old_project.team << [user, :reporter] + new_project.team << [user, :reporter] + + edit_issue(issue) + end + + scenario 'moving issue to another project', js: true do + find('#s2id_move_to_project_id').click + find('.select2-drop li', text: new_project.name_with_namespace).click + click_button('Save changes') + + expect(current_url).to include project_path(new_project) + + page.within('.issue') do + expect(page).to have_content("Text with #{cross_reference}!1") + expect(page).to have_content("Moved from #{cross_reference}#1") + expect(page).to have_content(issue.title) + end + end + + context 'projects user does not have permission to move issue to exist' do + let!(:private_project) { create(:project, :private) } + let(:another_project) { create(:project) } + background { another_project.team << [user, :guest] } + + scenario 'browsing projects in projects select' do + options = [ '', 'No project', new_project.name_with_namespace ] + expect(page).to have_select('move_to_project_id', options: options) + end + end + end + + def edit_issue(issue) + visit issue_path(issue) + page.within('.issuable-header') { click_link 'Edit' } + end + + def issue_path(issue) + namespace_project_issue_path(issue.project.namespace, issue.project, issue) + end + + def project_path(project) + namespace_project_path(new_project.namespace, new_project) + end +end -- cgit v1.2.1 From 5e405bfea9a2748af2652bf5dc51fce89a1794f3 Mon Sep 17 00:00:00 2001 From: Grzegorz Bizon Date: Thu, 17 Mar 2016 09:54:16 +0100 Subject: Update methods that use issue close service `Issues::CloseService#execute` signature has changed, because of using keyword parameter for commmit. --- app/services/git_push_service.rb | 2 +- app/services/issues/close_service.rb | 2 +- app/services/merge_requests/post_merge_service.rb | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/app/services/git_push_service.rb b/app/services/git_push_service.rb index d840ab5e340..855d55b3e58 100644 --- a/app/services/git_push_service.rb +++ b/app/services/git_push_service.rb @@ -120,7 +120,7 @@ class GitPushService < BaseService closed_issues = commit.closes_issues(current_user) closed_issues.each do |issue| if can?(current_user, :update_issue, issue) - Issues::CloseService.new(project, authors[commit], {}).execute(issue, commit) + Issues::CloseService.new(project, authors[commit], {}).execute(issue, commit: commit) end end end diff --git a/app/services/issues/close_service.rb b/app/services/issues/close_service.rb index 6866d3fcfb9..859c934ea3b 100644 --- a/app/services/issues/close_service.rb +++ b/app/services/issues/close_service.rb @@ -1,6 +1,6 @@ module Issues class CloseService < Issues::BaseService - def execute(issue, commit = nil, notifications: true, system_note: true) + def execute(issue, commit: nil, notifications: true, system_note: true) if project.jira_tracker? && project.jira_service.active project.jira_service.execute(commit, issue) todo_service.close_issue(issue, current_user) diff --git a/app/services/merge_requests/post_merge_service.rb b/app/services/merge_requests/post_merge_service.rb index ebb67c7db65..064910f81f7 100644 --- a/app/services/merge_requests/post_merge_service.rb +++ b/app/services/merge_requests/post_merge_service.rb @@ -22,7 +22,7 @@ module MergeRequests closed_issues = merge_request.closes_issues(current_user) closed_issues.each do |issue| if can?(current_user, :update_issue, issue) - Issues::CloseService.new(project, current_user, {}).execute(issue, merge_request) + Issues::CloseService.new(project, current_user, {}).execute(issue, commit: merge_request) end end end -- cgit v1.2.1 From dda7f9635fe96eba52110979914ff966188f6e8b Mon Sep 17 00:00:00 2001 From: Grzegorz Bizon Date: Thu, 17 Mar 2016 10:31:17 +0100 Subject: Add field that references issue this issue has been moved to --- app/models/issue.rb | 3 +++ db/migrate/20160317092222_add_moved_to_to_issue.rb | 5 +++++ db/schema.rb | 4 +++- 3 files changed, 11 insertions(+), 1 deletion(-) create mode 100644 db/migrate/20160317092222_add_moved_to_to_issue.rb diff --git a/app/models/issue.rb b/app/models/issue.rb index 5f58c0508fd..b506d174155 100644 --- a/app/models/issue.rb +++ b/app/models/issue.rb @@ -16,6 +16,7 @@ # state :string(255) # iid :integer # updated_by_id :integer +# moved_to_id :integer # require 'carrierwave/orm/activerecord' @@ -31,6 +32,8 @@ class Issue < ActiveRecord::Base ActsAsTaggableOn.strict_case_match = true belongs_to :project + belongs_to :moved_to, class_name: 'Issue' + validates :project, presence: true scope :of_group, diff --git a/db/migrate/20160317092222_add_moved_to_to_issue.rb b/db/migrate/20160317092222_add_moved_to_to_issue.rb new file mode 100644 index 00000000000..d262983b130 --- /dev/null +++ b/db/migrate/20160317092222_add_moved_to_to_issue.rb @@ -0,0 +1,5 @@ +class AddMovedToToIssue < ActiveRecord::Migration + def change + add_reference :issues, :moved_to, references: :issues, index: true + end +end diff --git a/db/schema.rb b/db/schema.rb index 2c27b228864..aa0071094df 100644 --- a/db/schema.rb +++ b/db/schema.rb @@ -11,7 +11,7 @@ # # It's strongly recommended that you check this file into your version control system. -ActiveRecord::Schema.define(version: 20160316123110) do +ActiveRecord::Schema.define(version: 20160317092222) do # These are extensions that must be enabled in order to support this database enable_extension "plpgsql" @@ -416,6 +416,7 @@ ActiveRecord::Schema.define(version: 20160316123110) do t.string "state" t.integer "iid" t.integer "updated_by_id" + t.integer "moved_to_id" end add_index "issues", ["assignee_id"], name: "index_issues_on_assignee_id", using: :btree @@ -424,6 +425,7 @@ ActiveRecord::Schema.define(version: 20160316123110) do add_index "issues", ["created_at"], name: "index_issues_on_created_at", using: :btree add_index "issues", ["description"], name: "index_issues_on_description_trigram", using: :gin, opclasses: {"description"=>"gin_trgm_ops"} add_index "issues", ["milestone_id"], name: "index_issues_on_milestone_id", using: :btree + add_index "issues", ["moved_to_id"], name: "index_issues_on_moved_to_id", using: :btree add_index "issues", ["project_id", "iid"], name: "index_issues_on_project_id_and_iid", unique: true, using: :btree add_index "issues", ["project_id"], name: "index_issues_on_project_id", using: :btree add_index "issues", ["state"], name: "index_issues_on_state", using: :btree -- cgit v1.2.1 From b9036ba61012e6723426f98171a2c111abb0bae5 Mon Sep 17 00:00:00 2001 From: Grzegorz Bizon Date: Thu, 17 Mar 2016 11:11:22 +0100 Subject: Prevent issue move if issue has been already moved --- app/models/issue.rb | 12 ++++++++ app/services/issues/move_service.rb | 9 ++++-- app/views/shared/issuable/_form.html.haml | 2 +- spec/features/issues/move_spec.rb | 11 +++++++ spec/models/issue_spec.rb | 50 +++++++++++++++++++++++++++++++ spec/services/issues/move_service_spec.rb | 18 +++++++++++ 6 files changed, 99 insertions(+), 3 deletions(-) diff --git a/app/models/issue.rb b/app/models/issue.rb index b506d174155..6a016636e0d 100644 --- a/app/models/issue.rb +++ b/app/models/issue.rb @@ -123,4 +123,16 @@ class Issue < ActiveRecord::Base note.all_references(current_user).merge_requests end.uniq.select { |mr| mr.open? && mr.closes_issue?(self) } end + + def moved? + !moved_to.nil? + end + + def can_move?(user, to_project = nil) + if to_project + return false unless user.can?(:admin_issue, to_project) + end + + !moved? && user.can?(:admin_issue, self.project) + end end diff --git a/app/services/issues/move_service.rb b/app/services/issues/move_service.rb index ce31830f2d0..47b58d973f9 100644 --- a/app/services/issues/move_service.rb +++ b/app/services/issues/move_service.rb @@ -33,6 +33,7 @@ module Issues # add_moved_to_note close_old_issue + mark_as_moved end notify_participants @@ -47,8 +48,8 @@ module Issues private def can_move? - can?(@current_user, :admin_issue, @project_old) && - can?(@current_user, :admin_issue, @project_new) + @issue_old.can_move?(@current_user) && + @issue_old.can_move?(@current_user, @project_new) end def create_new_issue @@ -96,5 +97,9 @@ module Issues def notify_participants notification_service.issue_moved(@issue_old, @issue_new, @current_user) end + + def mark_as_moved + @issue_old.update(moved_to: @issue_new) + end end end diff --git a/app/views/shared/issuable/_form.html.haml b/app/views/shared/issuable/_form.html.haml index d3eabe9ea64..5276afec1ca 100644 --- a/app/views/shared/issuable/_form.html.haml +++ b/app/views/shared/issuable/_form.html.haml @@ -67,7 +67,7 @@ - if can? current_user, :admin_label, issuable.project = link_to 'Create new label', new_namespace_project_label_path(issuable.project.namespace, issuable.project), target: :blank -- if issuable.is_a?(Issue) && can?(current_user, :admin_issue, issuable.project) +- if issuable.is_a?(Issue) && issuable.can_move?(current_user) %hr .form-group = label_tag :move_to_project_id, 'Move', class: 'control-label' diff --git a/spec/features/issues/move_spec.rb b/spec/features/issues/move_spec.rb index 6303b117c09..db26b3a4671 100644 --- a/spec/features/issues/move_spec.rb +++ b/spec/features/issues/move_spec.rb @@ -60,6 +60,17 @@ feature 'issue move to another project' do expect(page).to have_select('move_to_project_id', options: options) end end + + context 'issue has been already moved' do + let(:new_issue) { create(:issue, project: new_project) } + let(:issue) do + create(:issue, project: old_project, author: user, moved_to: new_issue) + end + + scenario 'user wants to move issue that has already been moved' do + expect(page).to have_no_select('move_to_project_id') + end + end end def edit_issue(issue) diff --git a/spec/models/issue_spec.rb b/spec/models/issue_spec.rb index 7f44ca2f7db..0d6e9cb3a4c 100644 --- a/spec/models/issue_spec.rb +++ b/spec/models/issue_spec.rb @@ -130,6 +130,56 @@ describe Issue, models: true do end end + describe '#can_move?' do + let(:user) { create(:user) } + let(:issue) { create(:issue) } + subject { issue.can_move?(user) } + + context 'user is not a member of project issue belongs to' do + it { is_expected.to eq false} + end + + context 'user is reporter in project issue belongs to' do + let(:project) { create(:project) } + let(:issue) { create(:issue, project: project) } + + before { project.team << [user, :reporter] } + + it { is_expected.to eq true } + + context 'checking destination project also' do + subject { issue.can_move?(user, to_project) } + let(:to_project) { create(:project) } + + context 'destination project allowed' do + before { to_project.team << [user, :reporter] } + it { is_expected.to eq true } + end + + context 'destination project not allowed' do + before { to_project.team << [user, :guest] } + it { is_expected.to eq false } + end + end + end + end + + describe '#moved?' do + let(:issue) { create(:issue) } + subject { issue.moved? } + + context 'issue not moved' do + it { is_expected.to eq false } + end + + context 'issue already moved' do + let(:moved_to_issue) { create(:issue) } + let(:issue) { create(:issue, moved_to: moved_to_issue) } + + it { is_expected.to eq true } + end + end + it_behaves_like 'an editable mentionable' do subject { create(:issue) } diff --git a/spec/services/issues/move_service_spec.rb b/spec/services/issues/move_service_spec.rb index cb02b8721ac..02ade91ac03 100644 --- a/spec/services/issues/move_service_spec.rb +++ b/spec/services/issues/move_service_spec.rb @@ -91,6 +91,11 @@ describe Issues::MoveService, services: true do it 'creates a new internal id for issue' do expect(new_issue.iid).to be 1 end + + it 'marks issue as moved' do + expect(old_issue.moved?).to eq true + expect(old_issue.moved_to).to eq new_issue + end end context 'issue with notes' do @@ -220,6 +225,19 @@ describe Issues::MoveService, services: true do it { is_expected.to be_falsey } end + + context 'issue has already been moved' do + include_context 'user can move issue' + + let(:moved_to_issue) { create(:issue) } + + let(:old_issue) do + create(:issue, project: old_project, author: author, + moved_to: moved_to_issue) + end + + it { is_expected.to be_falsey } + end end end end -- cgit v1.2.1 From fcf106897e2ff38e16e785ad9bcb18d117490fbf Mon Sep 17 00:00:00 2001 From: Grzegorz Bizon Date: Thu, 17 Mar 2016 12:25:17 +0100 Subject: Do not use javascript in specs for issue move Feature specs that were using javascript took a long time to finish (about 10x longer) and where hitting Poltergeist timeouts. This modification skips using javascript with select2 selecbox, thus it is faster, but this also does not check some JavaScript related features. --- spec/features/issues/move_spec.rb | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/spec/features/issues/move_spec.rb b/spec/features/issues/move_spec.rb index db26b3a4671..6fda0c31866 100644 --- a/spec/features/issues/move_spec.rb +++ b/spec/features/issues/move_spec.rb @@ -36,9 +36,8 @@ feature 'issue move to another project' do edit_issue(issue) end - scenario 'moving issue to another project', js: true do - find('#s2id_move_to_project_id').click - find('.select2-drop li', text: new_project.name_with_namespace).click + scenario 'moving issue to another project' do + select(new_project.name_with_namespace, from: 'move_to_project_id') click_button('Save changes') expect(current_url).to include project_path(new_project) -- cgit v1.2.1 From 9b13ce0b7a50e65dfba31d4865a728c725daa3fe Mon Sep 17 00:00:00 2001 From: Grzegorz Bizon Date: Fri, 18 Mar 2016 14:48:55 +0100 Subject: Improvements in issue move feaure (refactoring) According to endbosses' suggestions. --- app/controllers/projects/issues_controller.rb | 8 +- app/models/concerns/issuable.rb | 9 +++ app/services/issues/create_service.rb | 4 +- app/services/issues/move_service.rb | 87 +++++++++------------- app/views/shared/issuable/_form.html.haml | 24 +++--- db/migrate/20160317092222_add_moved_to_to_issue.rb | 2 +- db/schema.rb | 1 - lib/gitlab/gfm/reference_unfolder.rb | 25 ++----- lib/gitlab/reference_extractor.rb | 10 +++ spec/lib/gitlab/reference_extractor_spec.rb | 5 ++ spec/services/issues/move_service_spec.rb | 72 +++++------------- 11 files changed, 105 insertions(+), 142 deletions(-) diff --git a/app/controllers/projects/issues_controller.rb b/app/controllers/projects/issues_controller.rb index 9ec342a7b2a..016aa55656e 100644 --- a/app/controllers/projects/issues_controller.rb +++ b/app/controllers/projects/issues_controller.rb @@ -88,11 +88,11 @@ class Projects::IssuesController < Projects::ApplicationController def update @issue = Issues::UpdateService.new(project, current_user, issue_params).execute(issue) - move_service = Issues::MoveService.new(project, current_user, issue_params, - @issue, params['move_to_project_id']) - if move_service.move? - @issue = move_service.execute + if params[:move_to_project_id].to_i > 0 + new_project = Project.find(params[:move_to_project_id]) + move_service = Issues::MoveService.new(project, current_user, issue_params) + @issue = move_service.execute(@issue, new_project) end respond_to do |format| diff --git a/app/models/concerns/issuable.rb b/app/models/concerns/issuable.rb index 86ab84615ba..9ab72652190 100644 --- a/app/models/concerns/issuable.rb +++ b/app/models/concerns/issuable.rb @@ -209,4 +209,13 @@ module Issuable Taskable.get_updated_tasks(old_content: previous_changes['description'].first, new_content: description) end + + ## + # Method that checks if issuable can be moved to another project. + # + # Should be overridden if issuable can be moved. + # + def can_move?(*) + false + end end diff --git a/app/services/issues/create_service.rb b/app/services/issues/create_service.rb index 8af1a9b8d3b..e63e1af8766 100644 --- a/app/services/issues/create_service.rb +++ b/app/services/issues/create_service.rb @@ -1,10 +1,10 @@ module Issues class CreateService < Issues::BaseService - def execute(set_author: true) + def execute filter_params label_params = params[:label_ids] issue = project.issues.new(params.except(:label_ids)) - issue.author = current_user if set_author + issue.author = params[:author] || current_user if issue.save issue.update_attributes(label_ids: label_params) diff --git a/app/services/issues/move_service.rb b/app/services/issues/move_service.rb index 47b58d973f9..45b4b50c0f6 100644 --- a/app/services/issues/move_service.rb +++ b/app/services/issues/move_service.rb @@ -1,23 +1,19 @@ module Issues class MoveService < Issues::BaseService - def initialize(project, current_user, params, issue, new_project_id) - super(project, current_user, params) + class MoveError < StandardError; end - @issue_old = issue - @issue_new = nil - @project_old = @project + def execute(issue, new_project) + @old_issue = issue + @old_project = @project + @new_project = new_project - if new_project_id.to_i > 0 - @project_new = Project.find(new_project_id) + unless issue.can_move?(current_user, new_project) + raise MoveError, 'Cannot move issue due to insufficient permissions!' end - if @project_new == @project_old - raise StandardError, 'Cannot move issue to project it originates from!' + if @project == new_project + raise MoveError, 'Cannot move issue to project it originates from!' end - end - - def execute - return unless move? # Using transaction because of a high resources footprint # on rewriting notes (unfolding references) @@ -25,81 +21,72 @@ module Issues ActiveRecord::Base.transaction do # New issue tasks # - create_new_issue + @new_issue = create_new_issue + rewrite_notes - add_moved_from_note + add_note_moved_from # Old issue tasks # - add_moved_to_note - close_old_issue + add_note_moved_to + close_issue mark_as_moved end notify_participants - @issue_new - end - - def move? - @project_new && can_move? + @new_issue end private - def can_move? - @issue_old.can_move?(@current_user) && - @issue_old.can_move?(@current_user, @project_new) - end - def create_new_issue - new_params = { id: nil, iid: nil, milestone: nil, label_ids: [], - project: @project_new, author: @issue_old.author, - description: unfold_references(@issue_old.description) } - - new_params = @issue_old.serializable_hash.merge(new_params) - create_service = CreateService.new(@project_new, @current_user, - new_params) + new_params = { id: nil, iid: nil, label_ids: [], milestone: nil, + project: @new_project, author: @old_issue.author, + description: unfold_references(@old_issue.description) } - @issue_new = create_service.execute(set_author: false) + new_params = @old_issue.serializable_hash.merge(new_params) + CreateService.new(@new_project, @current_user, new_params).execute end def rewrite_notes - @issue_old.notes.find_each do |note| + @old_issue.notes.find_each do |note| new_note = note.dup - new_params = { project: @project_new, noteable: @issue_new, + new_params = { project: @new_project, noteable: @new_issue, note: unfold_references(new_note.note) } new_note.update(new_params) end end - def close_old_issue - close_service = CloseService.new(@project_new, @current_user) - close_service.execute(@issue_old, notifications: false, system_note: false) + def close_issue + close_service = CloseService.new(@old_project, @current_user) + close_service.execute(@old_issue, notifications: false, system_note: false) end - def add_moved_from_note - SystemNoteService.noteable_moved(@issue_new, @project_new, - @issue_old, @current_user, direction: :from) + def add_note_moved_from + SystemNoteService.noteable_moved(@new_issue, @new_project, + @old_issue, @current_user, + direction: :from) end - def add_moved_to_note - SystemNoteService.noteable_moved(@issue_old, @project_old, - @issue_new, @current_user, direction: :to) + def add_note_moved_to + SystemNoteService.noteable_moved(@old_issue, @old_project, + @new_issue, @current_user, + direction: :to) end def unfold_references(content) - unfolder = Gitlab::Gfm::ReferenceUnfolder.new(content, @project_old) - unfolder.unfold(@project_new) + unfolder = Gitlab::Gfm::ReferenceUnfolder.new(content, @old_project) + unfolder.unfold(@new_project) end def notify_participants - notification_service.issue_moved(@issue_old, @issue_new, @current_user) + notification_service.issue_moved(@old_issue, @new_issue, @current_user) end def mark_as_moved - @issue_old.update(moved_to: @issue_new) + @old_issue.update(moved_to: @new_issue) end end end diff --git a/app/views/shared/issuable/_form.html.haml b/app/views/shared/issuable/_form.html.haml index 5276afec1ca..d2fa407f8d3 100644 --- a/app/views/shared/issuable/_form.html.haml +++ b/app/views/shared/issuable/_form.html.haml @@ -67,22 +67,22 @@ - if can? current_user, :admin_label, issuable.project = link_to 'Create new label', new_namespace_project_label_path(issuable.project.namespace, issuable.project), target: :blank -- if issuable.is_a?(Issue) && issuable.can_move?(current_user) +- if issuable.can_move?(current_user) %hr - .form-group - = label_tag :move_to_project_id, 'Move', class: 'control-label' - .col-sm-10 - - projects = project_options(issuable, current_user, ability: :admin_issue) - = select_tag(:move_to_project_id, projects, include_blank: true, - class: 'select2', data: { placeholder: 'Select project' }) + .form-group + = label_tag :move_to_project_id, 'Move', class: 'control-label' + .col-sm-10 + - projects = project_options(issuable, current_user, ability: :admin_issue) + = select_tag(:move_to_project_id, projects, include_blank: true, + class: 'select2', data: { placeholder: 'Select project' }) - if issuable.is_a?(MergeRequest) %hr - - if @merge_request.new_record? - .form-group - = f.label :source_branch, class: 'control-label' - .col-sm-10 - = f.select(:source_branch, [@merge_request.source_branch], { }, { class: 'source_branch select2 span2', disabled: true }) + - if @merge_request.new_record? + .form-group + = f.label :source_branch, class: 'control-label' + .col-sm-10 + = f.select(:source_branch, [@merge_request.source_branch], { }, { class: 'source_branch select2 span2', disabled: true }) .form-group = f.label :target_branch, class: 'control-label' .col-sm-10 diff --git a/db/migrate/20160317092222_add_moved_to_to_issue.rb b/db/migrate/20160317092222_add_moved_to_to_issue.rb index d262983b130..461e7fb3a9b 100644 --- a/db/migrate/20160317092222_add_moved_to_to_issue.rb +++ b/db/migrate/20160317092222_add_moved_to_to_issue.rb @@ -1,5 +1,5 @@ class AddMovedToToIssue < ActiveRecord::Migration def change - add_reference :issues, :moved_to, references: :issues, index: true + add_reference :issues, :moved_to, references: :issues end end diff --git a/db/schema.rb b/db/schema.rb index aa0071094df..968a1c92253 100644 --- a/db/schema.rb +++ b/db/schema.rb @@ -425,7 +425,6 @@ ActiveRecord::Schema.define(version: 20160317092222) do add_index "issues", ["created_at"], name: "index_issues_on_created_at", using: :btree add_index "issues", ["description"], name: "index_issues_on_description_trigram", using: :gin, opclasses: {"description"=>"gin_trgm_ops"} add_index "issues", ["milestone_id"], name: "index_issues_on_milestone_id", using: :btree - add_index "issues", ["moved_to_id"], name: "index_issues_on_moved_to_id", using: :btree add_index "issues", ["project_id", "iid"], name: "index_issues_on_project_id_and_iid", unique: true, using: :btree add_index "issues", ["project_id"], name: "index_issues_on_project_id", using: :btree add_index "issues", ["state"], name: "index_issues_on_state", using: :btree diff --git a/lib/gitlab/gfm/reference_unfolder.rb b/lib/gitlab/gfm/reference_unfolder.rb index 0a68d6f977f..cf8de88b856 100644 --- a/lib/gitlab/gfm/reference_unfolder.rb +++ b/lib/gitlab/gfm/reference_unfolder.rb @@ -7,7 +7,9 @@ module Gitlab # in context of. # # `unfold` method tries to find all local references and unfold each of - # those local references to cross reference format. + # those local references to cross reference format, assuming that the + # argument passed to this method is a project that references will be + # viewed from (see `Referable#to_reference method). # # Examples: # @@ -34,17 +36,12 @@ module Gitlab end def unfold(from_project) - return @text unless @text =~ references_pattern + pattern = Gitlab::ReferenceExtractor.references_pattern + return @text unless @text =~ pattern - unfolded = @text.gsub(references_pattern) do |reference| + @text.gsub(pattern) do |reference| unfold_reference(reference, Regexp.last_match, from_project) end - - unless substitution_valid?(unfolded) - raise StandardError, 'Invalid references unfolding!' - end - - unfolded end private @@ -61,16 +58,6 @@ module Gitlab substitution_valid?(new_text) ? cross_reference : reference end - def references_pattern - return @pattern if @pattern - - patterns = Gitlab::ReferenceExtractor::REFERABLES.map do |ref| - ref.to_s.classify.constantize.try(:reference_pattern) - end - - @pattern = Regexp.union(patterns.compact) - end - def referables return @referables if @referables diff --git a/lib/gitlab/reference_extractor.rb b/lib/gitlab/reference_extractor.rb index 8c698d43bef..13c4d64c99b 100644 --- a/lib/gitlab/reference_extractor.rb +++ b/lib/gitlab/reference_extractor.rb @@ -37,6 +37,16 @@ module Gitlab @references.values.flatten end + def self.references_pattern + return @pattern if @pattern + + patterns = REFERABLES.map do |ref| + ref.to_s.classify.constantize.try(:reference_pattern) + end + + @pattern = Regexp.union(patterns.compact) + end + private def reference_context diff --git a/spec/lib/gitlab/reference_extractor_spec.rb b/spec/lib/gitlab/reference_extractor_spec.rb index 78ab162e5d6..ba47a3540ff 100644 --- a/spec/lib/gitlab/reference_extractor_spec.rb +++ b/spec/lib/gitlab/reference_extractor_spec.rb @@ -134,4 +134,9 @@ describe Gitlab::ReferenceExtractor, lib: true do expect(subject.all).to match_array([issue, label]) end end + + describe '.references_pattern' do + subject { described_class.references_pattern } + it { is_expected.to be_kind_of Regexp } + end end diff --git a/spec/services/issues/move_service_spec.rb b/spec/services/issues/move_service_spec.rb index 02ade91ac03..fd958a44e3e 100644 --- a/spec/services/issues/move_service_spec.rb +++ b/spec/services/issues/move_service_spec.rb @@ -15,11 +15,7 @@ describe Issues::MoveService, services: true do end let(:move_service) do - described_class.new(old_project, user, issue_params, old_issue, new_project_id) - end - - shared_context 'issue move requested' do - let(:new_project_id) { new_project.id } + described_class.new(old_project, user, issue_params) end shared_context 'user can move issue' do @@ -29,19 +25,13 @@ describe Issues::MoveService, services: true do end end - context 'issue movable' do - include_context 'issue move requested' - include_context 'user can move issue' - - describe '#move?' do - subject { move_service.move? } - it { is_expected.to be_truthy } + describe '#execute' do + shared_context 'issue move executed' do + let!(:new_issue) { move_service.execute(old_issue, new_project) } end - describe '#execute' do - shared_context 'issue move executed' do - let!(:new_issue) { move_service.execute } - end + context 'issue movable' do + include_context 'user can move issue' context 'generic issue' do include_context 'issue move executed' @@ -164,57 +154,33 @@ describe Issues::MoveService, services: true do end end end - end - end - - context 'moving to same project' do - let(:new_project) { old_project } - - include_context 'issue move requested' - include_context 'user can move issue' - - it 'raises error' do - expect { move_service } - .to raise_error(StandardError, /Cannot move issue/) - end - end - - context 'issue move not requested' do - let(:new_project_id) { nil } - describe '#move?' do - subject { move_service.move? } - - context 'user do not have permissions to move issue' do - it { is_expected.to be_falsey } - end + context 'moving to same project' do + let(:new_project) { old_project } - context 'user has permissions to move issue' do - include_context 'user can move issue' - it { is_expected.to be_falsey } + it 'raises error' do + expect { move_service.execute(old_issue, new_project) } + .to raise_error(StandardError, /Cannot move issue/) + end end end - end - - describe 'move permissions' do - include_context 'issue move requested' - describe '#move?' do - subject { move_service.move? } + describe 'move permissions' do + let(:move) { move_service.execute(old_issue, new_project) } context 'user is reporter in both projects' do include_context 'user can move issue' - it { is_expected.to be_truthy } + it { expect { move }.to_not raise_error } end context 'user is reporter only in new project' do before { new_project.team << [user, :reporter] } - it { is_expected.to be_falsey } + it { expect { move }.to raise_error(StandardError, /permissions/) } end context 'user is reporter only in old project' do before { old_project.team << [user, :reporter] } - it { is_expected.to be_falsey } + it { expect { move }.to raise_error(StandardError, /permissions/) } end context 'user is reporter in one project and guest in another' do @@ -223,7 +189,7 @@ describe Issues::MoveService, services: true do old_project.team << [user, :reporter] end - it { is_expected.to be_falsey } + it { expect { move }.to raise_error(StandardError, /permissions/) } end context 'issue has already been moved' do @@ -236,7 +202,7 @@ describe Issues::MoveService, services: true do moved_to: moved_to_issue) end - it { is_expected.to be_falsey } + it { expect { move }.to raise_error(StandardError, /permissions/) } end end end -- cgit v1.2.1 From 18f25bc94282a29029721ceeb9b9c6db354ce45f Mon Sep 17 00:00:00 2001 From: Grzegorz Bizon Date: Sat, 19 Mar 2016 18:58:52 +0100 Subject: Update reference unfolder according to recent ability changes Commit 43d8bdb4f048cbeb5675ed9120cb1aeb415b9586 introduced additional checks for permissions to read issue in references extractor. --- app/services/issues/move_service.rb | 3 ++- lib/gitlab/gfm/reference_unfolder.rb | 5 +++-- spec/lib/gitlab/gfm/reference_unfolder_spec.rb | 7 ++++++- spec/lib/gitlab/reference_extractor_spec.rb | 5 ++++- 4 files changed, 15 insertions(+), 5 deletions(-) diff --git a/app/services/issues/move_service.rb b/app/services/issues/move_service.rb index 45b4b50c0f6..892acc8c04d 100644 --- a/app/services/issues/move_service.rb +++ b/app/services/issues/move_service.rb @@ -77,7 +77,8 @@ module Issues end def unfold_references(content) - unfolder = Gitlab::Gfm::ReferenceUnfolder.new(content, @old_project) + unfolder = Gitlab::Gfm::ReferenceUnfolder.new(content, @old_project, + @current_user) unfolder.unfold(@new_project) end diff --git a/lib/gitlab/gfm/reference_unfolder.rb b/lib/gitlab/gfm/reference_unfolder.rb index cf8de88b856..94c09761960 100644 --- a/lib/gitlab/gfm/reference_unfolder.rb +++ b/lib/gitlab/gfm/reference_unfolder.rb @@ -29,9 +29,10 @@ module Gitlab # http://gitlab.com/some/link/#1234, and code `puts #1234`' # class ReferenceUnfolder - def initialize(text, project) + def initialize(text, project, user) @text = text @project = project + @user = user @original = markdown(text) end @@ -61,7 +62,7 @@ module Gitlab def referables return @referables if @referables - extractor = Gitlab::ReferenceExtractor.new(@project) + extractor = Gitlab::ReferenceExtractor.new(@project, @user) extractor.analyze(@text) @referables = extractor.all end diff --git a/spec/lib/gitlab/gfm/reference_unfolder_spec.rb b/spec/lib/gitlab/gfm/reference_unfolder_spec.rb index 40cdb7e1452..2e3b77d9180 100644 --- a/spec/lib/gitlab/gfm/reference_unfolder_spec.rb +++ b/spec/lib/gitlab/gfm/reference_unfolder_spec.rb @@ -4,9 +4,14 @@ describe Gitlab::Gfm::ReferenceUnfolder do let(:text) { 'some text' } let(:old_project) { create(:project) } let(:new_project) { create(:project) } + let(:user) { create(:user) } + + before { old_project.team << [user, :guest] } describe '#unfold' do - subject { described_class.new(text, old_project).unfold(new_project) } + subject do + described_class.new(text, old_project, user).unfold(new_project) + end context 'multiple issues and merge requests referenced' do let!(:issue_first) { create(:issue, project: old_project) } diff --git a/spec/lib/gitlab/reference_extractor_spec.rb b/spec/lib/gitlab/reference_extractor_spec.rb index f2922160331..7c617723e6d 100644 --- a/spec/lib/gitlab/reference_extractor_spec.rb +++ b/spec/lib/gitlab/reference_extractor_spec.rb @@ -130,7 +130,10 @@ describe Gitlab::ReferenceExtractor, lib: true do let(:label) { create(:label, project: project) } let(:text) { "Ref. #{issue.to_reference} and #{label.to_reference}" } - before { subject.analyze(text) } + before do + project.team << [project.creator, :developer] + subject.analyze(text) + end it 'returns all referables' do expect(subject.all).to match_array([issue, label]) -- cgit v1.2.1 From a2f25ed60b81f8c0325a7d1e33898351afa1d60f Mon Sep 17 00:00:00 2001 From: Grzegorz Bizon Date: Sat, 19 Mar 2016 19:04:11 +0100 Subject: Add Changelog entry for issue move feature --- CHANGELOG | 1 + 1 file changed, 1 insertion(+) diff --git a/CHANGELOG b/CHANGELOG index ca360c6706d..027167465f3 100644 --- a/CHANGELOG +++ b/CHANGELOG @@ -1,6 +1,7 @@ Please view this file on the master branch, on stable branches it's out of date. v 8.6.0 (unreleased) + - Add ability to move issue to another project - Add confidential issues - Bump gitlab_git to 9.0.3 (Stan Hu) - Support Golang subpackage fetching (Stan Hu) -- cgit v1.2.1 From f0211a4ea9e14293e2aea6f93798f23a01287bed Mon Sep 17 00:00:00 2001 From: Grzegorz Bizon Date: Sun, 20 Mar 2016 09:28:06 +0100 Subject: Do not pass params that are not used in issue move service --- app/controllers/projects/issues_controller.rb | 2 +- spec/services/issues/move_service_spec.rb | 3 +-- 2 files changed, 2 insertions(+), 3 deletions(-) diff --git a/app/controllers/projects/issues_controller.rb b/app/controllers/projects/issues_controller.rb index aa73c6df545..a51916720eb 100644 --- a/app/controllers/projects/issues_controller.rb +++ b/app/controllers/projects/issues_controller.rb @@ -92,7 +92,7 @@ class Projects::IssuesController < Projects::ApplicationController if params[:move_to_project_id].to_i > 0 new_project = Project.find(params[:move_to_project_id]) - move_service = Issues::MoveService.new(project, current_user, issue_params) + move_service = Issues::MoveService.new(project, current_user) @issue = move_service.execute(@issue, new_project) end diff --git a/spec/services/issues/move_service_spec.rb b/spec/services/issues/move_service_spec.rb index fd958a44e3e..cd24af88d5f 100644 --- a/spec/services/issues/move_service_spec.rb +++ b/spec/services/issues/move_service_spec.rb @@ -7,7 +7,6 @@ describe Issues::MoveService, services: true do let(:description) { 'Some issue description' } let(:old_project) { create(:project) } let(:new_project) { create(:project) } - let(:issue_params) { old_issue.serializable_hash } let(:old_issue) do create(:issue, title: title, description: description, @@ -15,7 +14,7 @@ describe Issues::MoveService, services: true do end let(:move_service) do - described_class.new(old_project, user, issue_params) + described_class.new(old_project, user) end shared_context 'user can move issue' do -- cgit v1.2.1 From 802b28e04ab15f1750f20c188302f172cef6ccf6 Mon Sep 17 00:00:00 2001 From: Grzegorz Bizon Date: Sun, 20 Mar 2016 10:01:12 +0100 Subject: Add tooltip info about issue move filed purpose --- app/views/shared/issuable/_form.html.haml | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/app/views/shared/issuable/_form.html.haml b/app/views/shared/issuable/_form.html.haml index 9be3ca1041e..383d17d1340 100644 --- a/app/views/shared/issuable/_form.html.haml +++ b/app/views/shared/issuable/_form.html.haml @@ -84,6 +84,10 @@ - projects = project_options(issuable, current_user, ability: :admin_issue) = select_tag(:move_to_project_id, projects, include_blank: true, class: 'select2', data: { placeholder: 'Select project' }) +   + %span{ data: { toggle: 'tooltip', placement: 'auto top' }, + title: 'Moving an issue will copy the discussion to a different project and close it here. All participants will be notified of the new location.' } + = icon('info-circle') - if issuable.is_a?(MergeRequest) %hr -- cgit v1.2.1 From 323d328c8644e3ff01b806f7754d33c0c7dedd7b Mon Sep 17 00:00:00 2001 From: Grzegorz Bizon Date: Sun, 20 Mar 2016 10:11:26 +0100 Subject: Rename reference unfolder to rewriter, minor refactorings --- app/services/issues/move_service.rb | 4 +- lib/gitlab/gfm/reference_rewriter.rb | 83 ++++++++++++++++++++++++++ lib/gitlab/gfm/reference_unfolder.rb | 83 -------------------------- spec/lib/gitlab/gfm/reference_rewriter_spec.rb | 74 +++++++++++++++++++++++ spec/lib/gitlab/gfm/reference_unfolder_spec.rb | 74 ----------------------- 5 files changed, 159 insertions(+), 159 deletions(-) create mode 100644 lib/gitlab/gfm/reference_rewriter.rb delete mode 100644 lib/gitlab/gfm/reference_unfolder.rb create mode 100644 spec/lib/gitlab/gfm/reference_rewriter_spec.rb delete mode 100644 spec/lib/gitlab/gfm/reference_unfolder_spec.rb diff --git a/app/services/issues/move_service.rb b/app/services/issues/move_service.rb index 892acc8c04d..c5b2bf80e0b 100644 --- a/app/services/issues/move_service.rb +++ b/app/services/issues/move_service.rb @@ -77,9 +77,9 @@ module Issues end def unfold_references(content) - unfolder = Gitlab::Gfm::ReferenceUnfolder.new(content, @old_project, + rewriter = Gitlab::Gfm::ReferenceRewriter.new(content, @old_project, @current_user) - unfolder.unfold(@new_project) + rewriter.rewrite(@new_project) end def notify_participants diff --git a/lib/gitlab/gfm/reference_rewriter.rb b/lib/gitlab/gfm/reference_rewriter.rb new file mode 100644 index 00000000000..91069732d44 --- /dev/null +++ b/lib/gitlab/gfm/reference_rewriter.rb @@ -0,0 +1,83 @@ +module Gitlab + module Gfm + ## + # Class that unfolds local references in text. + # + # The initializer takes text in Markdown and project this text is valid + # in context of. + # + # `unfold` method tries to find all local references and unfold each of + # those local references to cross reference format, assuming that the + # argument passed to this method is a project that references will be + # viewed from (see `Referable#to_reference method). + # + # Examples: + # + # 'Hello, this issue is related to #123 and + # other issues labeled with ~"label"', will be converted to: + # + # 'Hello, this issue is related to gitlab-org/gitlab-ce#123 and + # other issue labeled with gitlab-org/gitlab-ce~"label"'. + # + # It does respect markdown lexical rules, so text in code block will not be + # replaced, see another example: + # + # 'Merge request for issue #1234, see also link: + # http://gitlab.com/some/link/#1234, and code `puts #1234`' => + # + # 'Merge request for issue gitlab-org/gitlab-ce#1234, se also link: + # http://gitlab.com/some/link/#1234, and code `puts #1234`' + # + class ReferenceRewriter + def initialize(text, source_project, current_user) + @text = text + @source_project = source_project + @current_user = current_user + @original_html = markdown(text) + end + + def rewrite(target_project) + pattern = Gitlab::ReferenceExtractor.references_pattern + + @text.gsub(pattern) do |reference| + unfold_reference(reference, Regexp.last_match, target_project) + end + end + + private + + def unfold_reference(reference, match, target_project) + before = @text[0...match.begin(0)] + after = @text[match.end(0)..-1] + referable = find_local_referable(reference) + + return reference unless referable + cross_reference = referable.to_reference(target_project) + new_text = before + cross_reference + after + + substitution_valid?(new_text) ? cross_reference : reference + end + + def referables + return @referables if @referables + + extractor = Gitlab::ReferenceExtractor.new(@source_project, + @current_user) + extractor.analyze(@text) + @referables = extractor.all + end + + def find_local_referable(reference) + referables.find { |ref| ref.to_reference == reference } + end + + def substitution_valid?(substituted) + @original_html == markdown(substituted) + end + + def markdown(text) + Banzai.render(text, project: @source_project, no_original_data: true) + end + end + end +end diff --git a/lib/gitlab/gfm/reference_unfolder.rb b/lib/gitlab/gfm/reference_unfolder.rb deleted file mode 100644 index 94c09761960..00000000000 --- a/lib/gitlab/gfm/reference_unfolder.rb +++ /dev/null @@ -1,83 +0,0 @@ -module Gitlab - module Gfm - ## - # Class that unfolds local references in text. - # - # The initializer takes text in Markdown and project this text is valid - # in context of. - # - # `unfold` method tries to find all local references and unfold each of - # those local references to cross reference format, assuming that the - # argument passed to this method is a project that references will be - # viewed from (see `Referable#to_reference method). - # - # Examples: - # - # 'Hello, this issue is related to #123 and - # other issues labeled with ~"label"', will be converted to: - # - # 'Hello, this issue is related to gitlab-org/gitlab-ce#123 and - # other issue labeled with gitlab-org/gitlab-ce~"label"'. - # - # It does respect markdown lexical rules, so text in code block will not be - # replaced, see another example: - # - # 'Merge request for issue #1234, see also link: - # http://gitlab.com/some/link/#1234, and code `puts #1234`' => - # - # 'Merge request for issue gitlab-org/gitlab-ce#1234, se also link: - # http://gitlab.com/some/link/#1234, and code `puts #1234`' - # - class ReferenceUnfolder - def initialize(text, project, user) - @text = text - @project = project - @user = user - @original = markdown(text) - end - - def unfold(from_project) - pattern = Gitlab::ReferenceExtractor.references_pattern - return @text unless @text =~ pattern - - @text.gsub(pattern) do |reference| - unfold_reference(reference, Regexp.last_match, from_project) - end - end - - private - - def unfold_reference(reference, match, from_project) - before = @text[0...match.begin(0)] - after = @text[match.end(0)...@text.length] - referable = find_referable(reference) - - return reference unless referable - cross_reference = referable.to_reference(from_project) - new_text = before + cross_reference + after - - substitution_valid?(new_text) ? cross_reference : reference - end - - def referables - return @referables if @referables - - extractor = Gitlab::ReferenceExtractor.new(@project, @user) - extractor.analyze(@text) - @referables = extractor.all - end - - def find_referable(reference) - referables.find { |ref| ref.to_reference == reference } - end - - def substitution_valid?(substituted) - @original == markdown(substituted) - end - - def markdown(text) - Banzai.render(text, project: @project, no_original_data: true) - end - end - end -end diff --git a/spec/lib/gitlab/gfm/reference_rewriter_spec.rb b/spec/lib/gitlab/gfm/reference_rewriter_spec.rb new file mode 100644 index 00000000000..db7bee110af --- /dev/null +++ b/spec/lib/gitlab/gfm/reference_rewriter_spec.rb @@ -0,0 +1,74 @@ +require 'spec_helper' + +describe Gitlab::Gfm::ReferenceRewriter do + let(:text) { 'some text' } + let(:old_project) { create(:project) } + let(:new_project) { create(:project) } + let(:user) { create(:user) } + + before { old_project.team << [user, :guest] } + + describe '#rewrite' do + subject do + described_class.new(text, old_project, user).rewrite(new_project) + end + + context 'multiple issues and merge requests referenced' do + let!(:issue_first) { create(:issue, project: old_project) } + let!(:issue_second) { create(:issue, project: old_project) } + let!(:merge_request) { create(:merge_request, source_project: old_project) } + + context 'plain text description' do + let(:text) { 'Description that references #1, #2 and !1' } + + it { is_expected.to include issue_first.to_reference(new_project) } + it { is_expected.to include issue_second.to_reference(new_project) } + it { is_expected.to include merge_request.to_reference(new_project) } + end + + context 'description with ignored elements' do + let(:text) do + "Hi. This references #1, but not `#2`\n" + + '
and not !1
' + end + + it { is_expected.to include issue_first.to_reference(new_project) } + it { is_expected.to_not include issue_second.to_reference(new_project) } + it { is_expected.to_not include merge_request.to_reference(new_project) } + end + + context 'description ambigous elements' do + context 'url' do + let(:url) { 'http://gitlab.com/#1' } + let(:text) { "This references #1, but not #{url}" } + + it { is_expected.to include url } + end + + context 'code' do + let(:text) { "#1, but not `[#1]`" } + it { is_expected.to eq "#{issue_first.to_reference(new_project)}, but not `[#1]`" } + end + + context 'code reverse' do + let(:text) { "not `#1`, but #1" } + it { is_expected.to eq "not `#1`, but #{issue_first.to_reference(new_project)}" } + end + + context 'code in random order' do + let(:text) { "#1, `#1`, #1, `#1`" } + let(:ref) { issue_first.to_reference(new_project) } + + it { is_expected.to eq "#{ref}, `#1`, #{ref}, `#1`" } + end + end + + context 'reference contains milestone' do + let(:milestone) { create(:milestone) } + let(:text) { "milestone ref: #{milestone.to_reference}" } + + it { is_expected.to eq text } + end + end + end +end diff --git a/spec/lib/gitlab/gfm/reference_unfolder_spec.rb b/spec/lib/gitlab/gfm/reference_unfolder_spec.rb deleted file mode 100644 index 2e3b77d9180..00000000000 --- a/spec/lib/gitlab/gfm/reference_unfolder_spec.rb +++ /dev/null @@ -1,74 +0,0 @@ -require 'spec_helper' - -describe Gitlab::Gfm::ReferenceUnfolder do - let(:text) { 'some text' } - let(:old_project) { create(:project) } - let(:new_project) { create(:project) } - let(:user) { create(:user) } - - before { old_project.team << [user, :guest] } - - describe '#unfold' do - subject do - described_class.new(text, old_project, user).unfold(new_project) - end - - context 'multiple issues and merge requests referenced' do - let!(:issue_first) { create(:issue, project: old_project) } - let!(:issue_second) { create(:issue, project: old_project) } - let!(:merge_request) { create(:merge_request, source_project: old_project) } - - context 'plain text description' do - let(:text) { 'Description that references #1, #2 and !1' } - - it { is_expected.to include issue_first.to_reference(new_project) } - it { is_expected.to include issue_second.to_reference(new_project) } - it { is_expected.to include merge_request.to_reference(new_project) } - end - - context 'description with ignored elements' do - let(:text) do - "Hi. This references #1, but not `#2`\n" + - '
and not !1
' - end - - it { is_expected.to include issue_first.to_reference(new_project) } - it { is_expected.to_not include issue_second.to_reference(new_project) } - it { is_expected.to_not include merge_request.to_reference(new_project) } - end - - context 'description ambigous elements' do - context 'url' do - let(:url) { 'http://gitlab.com/#1' } - let(:text) { "This references #1, but not #{url}" } - - it { is_expected.to include url } - end - - context 'code' do - let(:text) { "#1, but not `[#1]`" } - it { is_expected.to eq "#{issue_first.to_reference(new_project)}, but not `[#1]`" } - end - - context 'code reverse' do - let(:text) { "not `#1`, but #1" } - it { is_expected.to eq "not `#1`, but #{issue_first.to_reference(new_project)}" } - end - - context 'code in random order' do - let(:text) { "#1, `#1`, #1, `#1`" } - let(:ref) { issue_first.to_reference(new_project) } - - it { is_expected.to eq "#{ref}, `#1`, #{ref}, `#1`" } - end - end - - context 'reference contains milestone' do - let(:milestone) { create(:milestone) } - let(:text) { "milestone ref: #{milestone.to_reference}" } - - it { is_expected.to eq text } - end - end - end -end -- cgit v1.2.1 From 6eb31056346ed07db4f66e4ba2369ff9779230c5 Mon Sep 17 00:00:00 2001 From: Grzegorz Bizon Date: Sun, 20 Mar 2016 10:52:01 +0100 Subject: Find referable for each ref found in references rewriter --- lib/gitlab/gfm/reference_rewriter.rb | 14 ++++---------- spec/lib/gitlab/gfm/reference_rewriter_spec.rb | 15 +++++++++++++++ 2 files changed, 19 insertions(+), 10 deletions(-) diff --git a/lib/gitlab/gfm/reference_rewriter.rb b/lib/gitlab/gfm/reference_rewriter.rb index 91069732d44..edc96a5e83b 100644 --- a/lib/gitlab/gfm/reference_rewriter.rb +++ b/lib/gitlab/gfm/reference_rewriter.rb @@ -49,7 +49,7 @@ module Gitlab def unfold_reference(reference, match, target_project) before = @text[0...match.begin(0)] after = @text[match.end(0)..-1] - referable = find_local_referable(reference) + referable = find_referable(reference) return reference unless referable cross_reference = referable.to_reference(target_project) @@ -58,17 +58,11 @@ module Gitlab substitution_valid?(new_text) ? cross_reference : reference end - def referables - return @referables if @referables - + def find_referable(reference) extractor = Gitlab::ReferenceExtractor.new(@source_project, @current_user) - extractor.analyze(@text) - @referables = extractor.all - end - - def find_local_referable(reference) - referables.find { |ref| ref.to_reference == reference } + extractor.analyze(reference) + extractor.all.first end def substitution_valid?(substituted) diff --git a/spec/lib/gitlab/gfm/reference_rewriter_spec.rb b/spec/lib/gitlab/gfm/reference_rewriter_spec.rb index db7bee110af..0a7ca3ec848 100644 --- a/spec/lib/gitlab/gfm/reference_rewriter_spec.rb +++ b/spec/lib/gitlab/gfm/reference_rewriter_spec.rb @@ -61,6 +61,21 @@ describe Gitlab::Gfm::ReferenceRewriter do it { is_expected.to eq "#{ref}, `#1`, #{ref}, `#1`" } end + + context 'description with labels' do + let!(:label) { create(:label, id: 123, name: 'test', project: old_project) } + let(:project_ref) { old_project.to_reference } + + context 'label referenced by id' do + let(:text) { '#1 and ~123' } + it { is_expected.to eq %Q{#{project_ref}#1 and #{project_ref}~123} } + end + + context 'label referenced by text' do + let(:text) { '#1 and ~"test"' } + it { is_expected.to eq %Q{#{project_ref}#1 and #{project_ref}~123} } + end + end end context 'reference contains milestone' do -- cgit v1.2.1 From d6474f22d263e5c04318c64979dfec3f7f45b7bc Mon Sep 17 00:00:00 2001 From: Grzegorz Bizon Date: Sun, 20 Mar 2016 17:05:21 +0100 Subject: Preserve created at time of notes when moving issue --- app/services/issues/move_service.rb | 3 ++- spec/services/issues/move_service_spec.rb | 5 +++++ 2 files changed, 7 insertions(+), 1 deletion(-) diff --git a/app/services/issues/move_service.rb b/app/services/issues/move_service.rb index c5b2bf80e0b..3cfbafe1576 100644 --- a/app/services/issues/move_service.rb +++ b/app/services/issues/move_service.rb @@ -53,7 +53,8 @@ module Issues @old_issue.notes.find_each do |note| new_note = note.dup new_params = { project: @new_project, noteable: @new_issue, - note: unfold_references(new_note.note) } + note: unfold_references(new_note.note), + created_at: note.created_at } new_note.update(new_params) end diff --git a/spec/services/issues/move_service_spec.rb b/spec/services/issues/move_service_spec.rb index cd24af88d5f..14cc20e529a 100644 --- a/spec/services/issues/move_service_spec.rb +++ b/spec/services/issues/move_service_spec.rb @@ -121,6 +121,11 @@ describe Issues::MoveService, services: true do it 'preserves orignal author of comment' do expect(user_notes.pluck(:author_id)).to all(eq(author.id)) end + + it 'preserves time when note has been created at' do + expect(old_issue.notes.first.created_at) + .to eq new_issue.notes.first.created_at + end end context 'notes with references' do -- cgit v1.2.1 From 91963267a0b4573fa193f5b7b11e0a793ea1fb9c Mon Sep 17 00:00:00 2001 From: Grzegorz Bizon Date: Sun, 20 Mar 2016 17:38:08 +0100 Subject: Change icon and cursor for issue move field tooltip --- app/views/shared/issuable/_form.html.haml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/app/views/shared/issuable/_form.html.haml b/app/views/shared/issuable/_form.html.haml index ce91e6fb027..1740b128ee4 100644 --- a/app/views/shared/issuable/_form.html.haml +++ b/app/views/shared/issuable/_form.html.haml @@ -94,9 +94,9 @@ = select_tag(:move_to_project_id, projects, include_blank: true, class: 'select2', data: { placeholder: 'Select project' })   - %span{ data: { toggle: 'tooltip', placement: 'auto top' }, + %span{ data: { toggle: 'tooltip', placement: 'auto top' }, style: 'cursor: default', title: 'Moving an issue will copy the discussion to a different project and close it here. All participants will be notified of the new location.' } - = icon('info-circle') + = icon('question-circle') - if issuable.is_a?(MergeRequest) %hr -- cgit v1.2.1 From db8f70d5088cc1e0172fd6b94fc4628bd83aa4a4 Mon Sep 17 00:00:00 2001 From: Grzegorz Bizon Date: Mon, 21 Mar 2016 14:12:49 +0100 Subject: Do not rewrite reference if already a cross reference --- lib/gitlab/gfm/reference_rewriter.rb | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/lib/gitlab/gfm/reference_rewriter.rb b/lib/gitlab/gfm/reference_rewriter.rb index edc96a5e83b..a1c6ee7bd69 100644 --- a/lib/gitlab/gfm/reference_rewriter.rb +++ b/lib/gitlab/gfm/reference_rewriter.rb @@ -49,12 +49,14 @@ module Gitlab def unfold_reference(reference, match, target_project) before = @text[0...match.begin(0)] after = @text[match.end(0)..-1] - referable = find_referable(reference) + referable = find_referable(reference) return reference unless referable + cross_reference = referable.to_reference(target_project) - new_text = before + cross_reference + after + return reference if reference == cross_reference + new_text = before + cross_reference + after substitution_valid?(new_text) ? cross_reference : reference end -- cgit v1.2.1