summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorJarka Kadlecova <kadlecovaj@gmail.com>2016-11-24 15:05:15 +0100
committerJarka Kadlecova <jarka@gitlab.com>2017-01-11 08:48:07 -0500
commit7ab3dd4b302a85c1b005e9ef290ebac631cda673 (patch)
tree9210fbc8f4e53c33970e6218c822de95c8963288
parent4404ea8662508c60f96e6730d9a45feb68498c28 (diff)
downloadgitlab-ce-7ab3dd4b302a85c1b005e9ef290ebac631cda673.tar.gz
support `/merge` slash comand for MRs
-rw-r--r--app/controllers/projects/notes_controller.rb3
-rw-r--r--app/models/merge_request.rb14
-rw-r--r--app/services/merge_requests/update_service.rb15
-rw-r--r--app/services/notes/create_service.rb5
-rw-r--r--app/services/notes/slash_commands_service.rb4
-rw-r--r--app/services/slash_commands/interpret_service.rb16
-rw-r--r--app/views/projects/notes/_form.html.haml1
-rw-r--r--changelogs/unreleased/24915_merge_slash_command.yml4
-rw-r--r--doc/user/project/slash_commands.md1
-rw-r--r--spec/controllers/projects/notes_controller_spec.rb48
-rw-r--r--spec/features/merge_requests/user_uses_slash_commands_spec.rb57
-rw-r--r--spec/models/merge_request_spec.rb96
-rw-r--r--spec/services/merge_requests/update_service_spec.rb93
-rw-r--r--spec/services/notes/create_service_spec.rb11
-rw-r--r--spec/services/slash_commands/interpret_service_spec.rb69
15 files changed, 429 insertions, 8 deletions
diff --git a/app/controllers/projects/notes_controller.rb b/app/controllers/projects/notes_controller.rb
index b71509f2c9b..c5d93ce25bc 100644
--- a/app/controllers/projects/notes_controller.rb
+++ b/app/controllers/projects/notes_controller.rb
@@ -23,7 +23,8 @@ class Projects::NotesController < Projects::ApplicationController
end
def create
- @note = Notes::CreateService.new(project, current_user, note_params).execute
+ create_params = note_params.merge(merge_request_diff_head_sha: params[:merge_request_diff_head_sha])
+ @note = Notes::CreateService.new(project, current_user, create_params).execute
if @note.is_a?(Note)
Banzai::NoteRenderer.render([@note], @project, current_user)
diff --git a/app/models/merge_request.rb b/app/models/merge_request.rb
index 70005a87f4b..10251302db8 100644
--- a/app/models/merge_request.rb
+++ b/app/models/merge_request.rb
@@ -898,10 +898,22 @@ class MergeRequest < ActiveRecord::Base
end
def has_commits?
- commits_count > 0
+ merge_request_diff && commits_count > 0
end
def has_no_commits?
!has_commits?
end
+
+ def mergeable_with_slash_command?(current_user, autocomplete_precheck: false, last_diff_sha: nil)
+ return false unless can_be_merged_by?(current_user)
+
+ return true if autocomplete_precheck
+
+ return false unless mergeable?(skip_ci_check: true)
+ return false if head_pipeline && !(head_pipeline.success? || head_pipeline.active?)
+ return false if last_diff_sha != diff_head_sha
+
+ true
+ end
end
diff --git a/app/services/merge_requests/update_service.rb b/app/services/merge_requests/update_service.rb
index ad16ef8c70f..3cb9aae83f6 100644
--- a/app/services/merge_requests/update_service.rb
+++ b/app/services/merge_requests/update_service.rb
@@ -7,6 +7,8 @@ module MergeRequests
params.except!(:target_project_id)
params.except!(:source_branch)
+ merge_from_slash_command(merge_request) if params[:merge]
+
if merge_request.closed_without_fork?
params.except!(:target_branch, :force_remove_source_branch)
end
@@ -69,6 +71,19 @@ module MergeRequests
end
end
+ def merge_from_slash_command(merge_request)
+ last_diff_sha = params.delete(:merge)
+ return unless merge_request.mergeable_with_slash_command?(current_user, last_diff_sha: last_diff_sha)
+
+ merge_request.update(merge_error: nil)
+
+ if merge_request.head_pipeline && merge_request.head_pipeline.active?
+ MergeRequests::MergeWhenPipelineSucceedsService.new(project, current_user).execute(merge_request)
+ else
+ MergeWorker.perform_async(merge_request.id, current_user.id, {})
+ end
+ end
+
def reopen_service
MergeRequests::ReopenService
end
diff --git a/app/services/notes/create_service.rb b/app/services/notes/create_service.rb
index 1beca9f4109..cdd765c85eb 100644
--- a/app/services/notes/create_service.rb
+++ b/app/services/notes/create_service.rb
@@ -1,6 +1,8 @@
module Notes
class CreateService < BaseService
def execute
+ merge_request_diff_head_sha = params.delete(:merge_request_diff_head_sha)
+
note = project.notes.new(params)
note.author = current_user
note.system = false
@@ -19,7 +21,8 @@ module Notes
slash_commands_service = SlashCommandsService.new(project, current_user)
if slash_commands_service.supported?(note)
- content, command_params = slash_commands_service.extract_commands(note)
+ options = { merge_request_diff_head_sha: merge_request_diff_head_sha }
+ content, command_params = slash_commands_service.extract_commands(note, options)
only_commands = content.empty?
diff --git a/app/services/notes/slash_commands_service.rb b/app/services/notes/slash_commands_service.rb
index 2edbd39a9e7..aaea9717fc4 100644
--- a/app/services/notes/slash_commands_service.rb
+++ b/app/services/notes/slash_commands_service.rb
@@ -19,10 +19,10 @@ module Notes
self.class.supported?(note, current_user)
end
- def extract_commands(note)
+ def extract_commands(note, options = {})
return [note.note, {}] unless supported?(note)
- SlashCommands::InterpretService.new(project, current_user).
+ SlashCommands::InterpretService.new(project, current_user, options).
execute(note.note, note.noteable)
end
diff --git a/app/services/slash_commands/interpret_service.rb b/app/services/slash_commands/interpret_service.rb
index d75c5b1800e..14fad3ba120 100644
--- a/app/services/slash_commands/interpret_service.rb
+++ b/app/services/slash_commands/interpret_service.rb
@@ -2,7 +2,7 @@ module SlashCommands
class InterpretService < BaseService
include Gitlab::SlashCommands::Dsl
- attr_reader :issuable
+ attr_reader :issuable, :options
# Takes a text and interprets the commands that are extracted from it.
# Returns the content without commands, and hash of changes to be applied to a record.
@@ -13,7 +13,8 @@ module SlashCommands
opts = {
issuable: issuable,
current_user: current_user,
- project: project
+ project: project,
+ params: params
}
content, commands = extractor.extract_commands(content, opts)
@@ -58,6 +59,17 @@ module SlashCommands
@updates[:state_event] = 'reopen'
end
+ desc 'Merge (when build succeeds)'
+ condition do
+ last_diff_sha = params.to_h[:merge_request_diff_head_sha]
+ issuable.is_a?(MergeRequest) &&
+ issuable.mergeable_with_slash_command?(current_user, autocomplete_precheck: !last_diff_sha, last_diff_sha: last_diff_sha) &&
+ issuable.persisted?
+ end
+ command :merge do
+ @updates[:merge] = params[:merge_request_diff_head_sha]
+ end
+
desc 'Change title'
params '<New title>'
condition do
diff --git a/app/views/projects/notes/_form.html.haml b/app/views/projects/notes/_form.html.haml
index 39731668a61..b561052e721 100644
--- a/app/views/projects/notes/_form.html.haml
+++ b/app/views/projects/notes/_form.html.haml
@@ -3,6 +3,7 @@
= form_for [@project.namespace.becomes(Namespace), @project, @note], remote: true, html: { :'data-type' => 'json', multipart: true, id: nil, class: "new-note js-new-note-form js-quick-submit common-note-form", "data-noteable-iid" => @note.noteable.try(:iid), }, authenticity_token: true do |f|
= hidden_field_tag :view, diff_view
= hidden_field_tag :line_type
+ = hidden_field_tag :merge_request_diff_head_sha, @note.noteable.try(:diff_head_sha)
= note_target_fields(@note)
= f.hidden_field :commit_id
= f.hidden_field :line_code
diff --git a/changelogs/unreleased/24915_merge_slash_command.yml b/changelogs/unreleased/24915_merge_slash_command.yml
new file mode 100644
index 00000000000..eb8ced8ab01
--- /dev/null
+++ b/changelogs/unreleased/24915_merge_slash_command.yml
@@ -0,0 +1,4 @@
+---
+title: Support slash comand `/merge` for merging merge requests.
+merge_request: 7746
+author: Jarka Kadlecova
diff --git a/doc/user/project/slash_commands.md b/doc/user/project/slash_commands.md
index 5f6a6c6503e..0a66e9a3e15 100644
--- a/doc/user/project/slash_commands.md
+++ b/doc/user/project/slash_commands.md
@@ -14,6 +14,7 @@ do.
|:---------------------------|:-------------|
| `/close` | Close the issue or merge request |
| `/reopen` | Reopen the issue or merge request |
+| `/merge` | Merge (when build succeeds) |
| `/title <New title>` | Change title |
| `/assign @username` | Assign |
| `/unassign` | Remove assignee |
diff --git a/spec/controllers/projects/notes_controller_spec.rb b/spec/controllers/projects/notes_controller_spec.rb
index 92e38b02615..9f6d4ec6537 100644
--- a/spec/controllers/projects/notes_controller_spec.rb
+++ b/spec/controllers/projects/notes_controller_spec.rb
@@ -14,6 +14,54 @@ describe Projects::NotesController do
}
end
+ describe 'POST create' do
+ let(:merge_request) { create(:merge_request) }
+ let(:request_params) do
+ {
+ note: { note: 'some note', noteable_id: merge_request.id, noteable_type: 'MergeRequest' },
+ namespace_id: project.namespace,
+ project_id: project,
+ merge_request_diff_head_sha: 'sha'
+ }
+ end
+
+ before do
+ sign_in(user)
+ project.team << [user, :developer]
+ end
+
+ it "returns status 302 for html" do
+ post :create, request_params
+
+ expect(response).to have_http_status(302)
+ end
+
+ it "returns status 200 for json" do
+ post :create, request_params.merge(format: :json)
+
+ expect(response).to have_http_status(200)
+ end
+
+ context 'when merge_request_diff_head_sha present' do
+ before do
+ service_params = {
+ note: 'some note',
+ noteable_id: merge_request.id.to_s,
+ noteable_type: 'MergeRequest',
+ merge_request_diff_head_sha: 'sha'
+ }
+
+ expect(Notes::CreateService).to receive(:new).with(project, user, service_params).and_return(double(execute: true))
+ end
+
+ it "returns status 302 for html" do
+ post :create, request_params
+
+ expect(response).to have_http_status(302)
+ end
+ end
+ end
+
describe 'POST toggle_award_emoji' do
before do
sign_in(user)
diff --git a/spec/features/merge_requests/user_uses_slash_commands_spec.rb b/spec/features/merge_requests/user_uses_slash_commands_spec.rb
index b1b3a47a1ce..d7e8723b63a 100644
--- a/spec/features/merge_requests/user_uses_slash_commands_spec.rb
+++ b/spec/features/merge_requests/user_uses_slash_commands_spec.rb
@@ -68,6 +68,63 @@ feature 'Merge Requests > User uses slash commands', feature: true, js: true do
end
end
+ describe 'merging the MR from the note' do
+ context 'when the current user can merge the MR' do
+ it 'merges the MR' do
+ write_note("/merge")
+
+ expect(page).to have_content 'Your commands have been executed!'
+
+ expect(merge_request.reload).to be_merged
+ end
+ end
+
+ context 'when the head diff changes in the meanwhile' do
+ before do
+ path = File.expand_path("#{project.repository_storage_path}/#{project.namespace.path}/#{project.path}/new_file.txt")
+
+ params = {
+ source_project: merge_request.project,
+ target_project: merge_request.project,
+ target_branch: merge_request.source_branch,
+ source_branch: merge_request.source_branch,
+ file_path: path,
+ file_content: 'some content',
+ commit_message: 'additional commit',
+ }
+
+ Files::UpdateService.new(project, user, params).execute
+ merge_request.reload_diff
+ end
+
+ it 'does not merge the MR' do
+ write_note("/merge")
+
+ expect(page).not_to have_content 'Your commands have been executed!'
+
+ expect(merge_request.reload).not_to be_merged
+ end
+ end
+
+ context 'when the current user cannot merge the MR' do
+ let(:guest) { create(:user) }
+ before do
+ project.team << [guest, :guest]
+ logout
+ login_with(guest)
+ visit namespace_project_merge_request_path(project.namespace, project, merge_request)
+ end
+
+ it 'does not merge the MR' do
+ write_note("/merge")
+
+ expect(page).not_to have_content 'Your commands have been executed!'
+
+ expect(merge_request.reload).not_to be_merged
+ end
+ end
+ end
+
describe 'adding a due date from note' do
it 'does not recognize the command nor create a note' do
write_note('/due 2016-08-28')
diff --git a/spec/models/merge_request_spec.rb b/spec/models/merge_request_spec.rb
index 8d1385016fd..722987a423c 100644
--- a/spec/models/merge_request_spec.rb
+++ b/spec/models/merge_request_spec.rb
@@ -1514,6 +1514,102 @@ describe MergeRequest, models: true do
end
end
+ describe '#mergeable_with_slash_command?' do
+ def create_pipeline(status)
+ create(:ci_pipeline_with_one_job,
+ project: project,
+ ref: merge_request.source_branch,
+ sha: merge_request.diff_head_sha,
+ status: status)
+ end
+
+ let(:project) { create(:project, :public, only_allow_merge_if_build_succeeds: true) }
+ let(:developer) { create(:user) }
+ let(:user) { create(:user) }
+ let(:merge_request) { create(:merge_request, source_project: project) }
+ let(:mr_sha) { merge_request.diff_head_sha }
+
+ before do
+ project.team << [developer, :developer]
+ end
+
+ context 'when autocomplete_precheck is set to true' do
+ it 'is mergeable by developer' do
+ expect(merge_request.mergeable_with_slash_command?(developer, autocomplete_precheck: true)).to be_truthy
+ end
+
+ it 'is not mergeable by normal user' do
+ expect(merge_request.mergeable_with_slash_command?(user, autocomplete_precheck: true)).to be_falsey
+ end
+ end
+
+ context 'when autocomplete_precheck is set to false' do
+ it 'is mergeable by developer' do
+ expect(merge_request.mergeable_with_slash_command?(developer, last_diff_sha: mr_sha)).to be_truthy
+ end
+
+ it 'is not mergeable by normal user' do
+ expect(merge_request.mergeable_with_slash_command?(user, last_diff_sha: mr_sha)).to be_falsey
+ end
+
+ context 'closed MR' do
+ before do
+ merge_request.update_attribute(:state, :closed)
+ end
+
+ it 'is not mergeable' do
+ expect(merge_request.mergeable_with_slash_command?(developer, last_diff_sha: mr_sha)).to be_falsey
+ end
+ end
+
+ context 'MR with WIP' do
+ before do
+ merge_request.update_attribute(:title, 'WIP: some MR')
+ end
+
+ it 'is not mergeable' do
+ expect(merge_request.mergeable_with_slash_command?(developer, last_diff_sha: mr_sha)).to be_falsey
+ end
+ end
+
+ context 'sha differs from the MR diff_head_sha' do
+ it 'is not mergeable' do
+ expect(merge_request.mergeable_with_slash_command?(developer, last_diff_sha: 'some other sha')).to be_falsey
+ end
+ end
+
+ context 'with pipeline ok' do
+ before do
+ create_pipeline(:success)
+ end
+
+ it 'is mergeable' do
+ expect(merge_request.mergeable_with_slash_command?(developer, last_diff_sha: mr_sha)).to be_truthy
+ end
+ end
+
+ context 'with failing pipeline' do
+ before do
+ create_pipeline(:failed)
+ end
+
+ it 'is not mergeable' do
+ expect(merge_request.mergeable_with_slash_command?(developer, last_diff_sha: mr_sha)).to be_falsey
+ end
+ end
+
+ context 'with running pipeline' do
+ before do
+ create_pipeline(:running)
+ end
+
+ it 'is mergeable' do
+ expect(merge_request.mergeable_with_slash_command?(developer, last_diff_sha: mr_sha)).to be_truthy
+ end
+ end
+ end
+ end
+
describe '#has_commits?' do
before do
allow(subject.merge_request_diff).to receive(:commits_count).
diff --git a/spec/services/merge_requests/update_service_spec.rb b/spec/services/merge_requests/update_service_spec.rb
index 88c786947d3..c2f6ae29d62 100644
--- a/spec/services/merge_requests/update_service_spec.rb
+++ b/spec/services/merge_requests/update_service_spec.rb
@@ -121,6 +121,99 @@ describe MergeRequests::UpdateService, services: true do
end
end
+ context 'merge' do
+ let(:opts) do
+ {
+ merge: merge_request.diff_head_sha
+ }
+ end
+
+ let(:service) { MergeRequests::UpdateService.new(project, user, opts) }
+
+ context 'without pipeline' do
+ before do
+ merge_request.merge_error = 'Error'
+
+ perform_enqueued_jobs do
+ service.execute(merge_request)
+ @merge_request = MergeRequest.find(merge_request.id)
+ end
+ end
+
+ it { expect(@merge_request).to be_valid }
+ it { expect(@merge_request.state).to eq('merged') }
+ it { expect(@merge_request.merge_error).to be_nil }
+ end
+
+ context 'with finished pipeline' do
+ before do
+ create(:ci_pipeline_with_one_job,
+ project: project,
+ ref: merge_request.source_branch,
+ sha: merge_request.diff_head_sha,
+ status: :success)
+
+ perform_enqueued_jobs do
+ @merge_request = service.execute(merge_request)
+ @merge_request = MergeRequest.find(merge_request.id)
+ end
+ end
+
+ it { expect(@merge_request).to be_valid }
+ it { expect(@merge_request.state).to eq('merged') }
+ end
+
+ context 'with active pipeline' do
+ before do
+ service_mock = double
+ create(:ci_pipeline_with_one_job,
+ project: project,
+ ref: merge_request.source_branch,
+ sha: merge_request.diff_head_sha)
+
+ expect(MergeRequests::MergeWhenPipelineSucceedsService).to receive(:new).with(project, user).
+ and_return(service_mock)
+ expect(service_mock).to receive(:execute).with(merge_request)
+ end
+
+ it { service.execute(merge_request) }
+ end
+
+ context 'MR can not be merged by non authorised user' do
+ let(:visitor) { create(:user) }
+ let(:service) { MergeRequests::UpdateService.new(project, visitor, opts) }
+
+ before do
+ merge_request.update_attribute(:merge_error, 'Error')
+
+ perform_enqueued_jobs do
+ @merge_request = service.execute(merge_request)
+ @merge_request = MergeRequest.find(merge_request.id)
+ end
+ end
+
+ it { expect(@merge_request.state).to eq('opened') }
+ it { expect(@merge_request.merge_error).not_to be_nil }
+ end
+
+ context 'MR can not be merged when note sha != MR sha' do
+ let(:opts) do
+ {
+ merge: 'other_commit'
+ }
+ end
+
+ before do
+ perform_enqueued_jobs do
+ @merge_request = service.execute(merge_request)
+ @merge_request = MergeRequest.find(merge_request.id)
+ end
+ end
+
+ it { expect(@merge_request.state).to eq('opened') }
+ end
+ end
+
context 'todos' do
let!(:pending_todo) { create(:todo, :assigned, user: user, project: project, target: merge_request, author: user2) }
diff --git a/spec/services/notes/create_service_spec.rb b/spec/services/notes/create_service_spec.rb
index 25804696d2e..b0cc3ce5f5a 100644
--- a/spec/services/notes/create_service_spec.rb
+++ b/spec/services/notes/create_service_spec.rb
@@ -63,6 +63,17 @@ describe Notes::CreateService, services: true do
expect(note.note).to eq "HELLO\nWORLD"
end
end
+
+ describe '/merge with sha option' do
+ let(:note_text) { %(HELLO\n/merge\nWORLD) }
+ let(:params) { opts.merge(note: note_text, merge_request_diff_head_sha: 'sha') }
+
+ it 'saves the note and exectues merge command' do
+ note = described_class.new(project, user, params).execute
+
+ expect(note.note).to eq "HELLO\nWORLD"
+ end
+ end
end
end
diff --git a/spec/services/slash_commands/interpret_service_spec.rb b/spec/services/slash_commands/interpret_service_spec.rb
index becf627a4f5..dfcffcc6131 100644
--- a/spec/services/slash_commands/interpret_service_spec.rb
+++ b/spec/services/slash_commands/interpret_service_spec.rb
@@ -1,12 +1,13 @@
require 'spec_helper'
describe SlashCommands::InterpretService, services: true do
- let(:project) { create(:empty_project, :public) }
+ let(:project) { create(:project, :public) }
let(:developer) { create(:user) }
let(:issue) { create(:issue, project: project) }
let(:milestone) { create(:milestone, project: project, title: '9.10') }
let(:inprogress) { create(:label, project: project, title: 'In Progress') }
let(:bug) { create(:label, project: project, title: 'Bug') }
+ let(:note) { build(:note, commit_id: merge_request.diff_head_sha) }
before do
project.team << [developer, :developer]
@@ -218,6 +219,14 @@ describe SlashCommands::InterpretService, services: true do
end
end
+ shared_examples 'merge command' do
+ it 'runs merge command if content contains /merge' do
+ _, updates = service.execute(content, issuable)
+
+ expect(updates).to eq(merge: merge_request.diff_head_sha)
+ end
+ end
+
it_behaves_like 'reopen command' do
let(:content) { '/reopen' }
let(:issuable) { issue }
@@ -238,6 +247,64 @@ describe SlashCommands::InterpretService, services: true do
let(:issuable) { merge_request }
end
+ context 'merge command' do
+ let(:service) { described_class.new(project, developer, { merge_request_diff_head_sha: merge_request.diff_head_sha }) }
+
+ it_behaves_like 'merge command' do
+ let(:content) { '/merge' }
+ let(:issuable) { merge_request }
+ end
+
+ context 'can not be merged when logged user does not have permissions' do
+ let(:service) { described_class.new(project, create(:user)) }
+
+ it_behaves_like 'empty command' do
+ let(:content) { "/merge" }
+ let(:issuable) { merge_request }
+ end
+ end
+
+ context 'can not be merged when sha does not match' do
+ let(:service) { described_class.new(project, developer, { merge_request_diff_head_sha: 'othersha' }) }
+
+ it_behaves_like 'empty command' do
+ let(:content) { "/merge" }
+ let(:issuable) { merge_request }
+ end
+ end
+
+ context 'when sha is missing' do
+ let(:service) { described_class.new(project, developer, {}) }
+
+ it 'precheck passes and returns merge command' do
+ _, updates = service.execute('/merge', merge_request)
+
+ expect(updates).to eq(merge: nil)
+ end
+ end
+
+ context 'non merge request object cant be merged' do
+ it_behaves_like 'empty command' do
+ let(:content) { "/merge" }
+ let(:issuable) { issue }
+ end
+ end
+
+ context 'non persisted merge request cant be merged' do
+ it_behaves_like 'empty command' do
+ let(:content) { "/merge" }
+ let(:issuable) { build(:merge_request) }
+ end
+ end
+
+ context 'not persisted merge request can not be merged' do
+ it_behaves_like 'empty command' do
+ let(:content) { "/merge" }
+ let(:issuable) { build(:merge_request, source_project: project) }
+ end
+ end
+ end
+
it_behaves_like 'title command' do
let(:content) { '/title A brand new title' }
let(:issuable) { issue }