Commit 73c59de0 authored by Douwe Maan's avatar Douwe Maan

Merge branch 'master' into split-up-builds

parents 6c6fb1d8 9bfd6c44
Please view this file on the master branch, on stable branches it's out of date. Please view this file on the master branch, on stable branches it's out of date.
v 8.3.0 (unreleased) v 8.3.0 (unreleased)
- Merge when build succeeds (Zeger-Jan van de Weg)
- Bump gollum-lib to 4.1.0 (Stan Hu) - Bump gollum-lib to 4.1.0 (Stan Hu)
- Fix broken group avatar upload under "New group" (Stan Hu) - Fix broken group avatar upload under "New group" (Stan Hu)
- Update project repositorize size and commit count during import:repos task (Stan Hu) - Update project repositorize size and commit count during import:repos task (Stan Hu)
......
...@@ -18,6 +18,7 @@ ...@@ -18,6 +18,7 @@
.accept-merge-holder { .accept-merge-holder {
.accept-action { .accept-action {
display: inline-block; display: inline-block;
float: left;
.accept_merge_request { .accept_merge_request {
&.ci-pending, &.ci-pending,
...@@ -36,14 +37,15 @@ ...@@ -36,14 +37,15 @@
.accept-control { .accept-control {
display: inline-block; display: inline-block;
float: left;
margin: 0; margin: 0;
margin-left: 20px; margin-left: 20px;
padding: 5px; padding: 5px;
padding-top: 12px;
line-height: 20px; line-height: 20px;
&.right { &.right {
float: right; float: right;
padding-top: 12px;
a { a {
color: $gl-gray; color: $gl-gray;
} }
......
...@@ -2,11 +2,12 @@ class Projects::MergeRequestsController < Projects::ApplicationController ...@@ -2,11 +2,12 @@ class Projects::MergeRequestsController < Projects::ApplicationController
before_action :module_enabled before_action :module_enabled
before_action :merge_request, only: [ before_action :merge_request, only: [
:edit, :update, :show, :diffs, :commits, :builds, :merge, :merge_check, :edit, :update, :show, :diffs, :commits, :builds, :merge, :merge_check,
:ci_status, :toggle_subscription :ci_status, :toggle_subscription, :cancel_merge_when_build_succeeds
] ]
before_action :closes_issues, only: [:edit, :update, :show, :diffs, :commits, :builds] before_action :closes_issues, only: [:edit, :update, :show, :diffs, :commits, :builds]
before_action :validates_merge_request, only: [:show, :diffs, :commits, :builds] before_action :validates_merge_request, only: [:show, :diffs, :commits, :builds]
before_action :define_show_vars, only: [:show, :diffs, :commits, :builds] before_action :define_show_vars, only: [:show, :diffs, :commits, :builds]
before_action :define_widget_vars, only: [:merge, :cancel_merge_when_build_succeeds]
before_action :ensure_ref_fetched, only: [:show, :diffs, :commits, :builds] before_action :ensure_ref_fetched, only: [:show, :diffs, :commits, :builds]
# Allow read any merge_request # Allow read any merge_request
...@@ -164,15 +165,29 @@ class Projects::MergeRequestsController < Projects::ApplicationController ...@@ -164,15 +165,29 @@ class Projects::MergeRequestsController < Projects::ApplicationController
render partial: "projects/merge_requests/widget/show.html.haml", layout: false render partial: "projects/merge_requests/widget/show.html.haml", layout: false
end end
def cancel_merge_when_build_succeeds
return access_denied! unless @merge_request.can_cancel_merge_when_build_succeeds?(current_user)
MergeRequests::MergeWhenBuildSucceedsService.new(@project, current_user).cancel(@merge_request)
end
def merge def merge
return access_denied! unless @merge_request.can_be_merged_by?(current_user) return access_denied! unless @merge_request.can_be_merged_by?(current_user)
if @merge_request.mergeable? unless @merge_request.mergeable?
@merge_request.update(merge_error: nil) @status = :failed
MergeWorker.perform_async(@merge_request.id, current_user.id, params) return
@status = true end
@merge_request.update(merge_error: nil)
if params[:merge_when_build_succeeds] && @merge_request.ci_commit && @merge_request.ci_commit.active?
MergeRequests::MergeWhenBuildSucceedsService.new(@project, current_user, merge_params)
.execute(@merge_request)
@status = :merge_when_build_succeeds
else else
@status = false MergeWorker.perform_async(@merge_request.id, current_user.id, params)
@status = :success
end end
end end
...@@ -287,6 +302,10 @@ class Projects::MergeRequestsController < Projects::ApplicationController ...@@ -287,6 +302,10 @@ class Projects::MergeRequestsController < Projects::ApplicationController
end end
end end
def define_widget_vars
@ci_commit = @merge_request.ci_commit
end
def invalid_mr def invalid_mr
# Render special view for MR with removed source or target branch # Render special view for MR with removed source or target branch
render 'invalid' render 'invalid'
...@@ -300,6 +319,10 @@ class Projects::MergeRequestsController < Projects::ApplicationController ...@@ -300,6 +319,10 @@ class Projects::MergeRequestsController < Projects::ApplicationController
) )
end end
def merge_params
params.permit(:should_remove_source_branch, :commit_message)
end
# Make sure merge requests created before 8.0 # Make sure merge requests created before 8.0
# have head file in refs/merge-requests/ # have head file in refs/merge-requests/
def ensure_ref_fetched def ensure_ref_fetched
......
...@@ -165,6 +165,14 @@ module Ci ...@@ -165,6 +165,14 @@ module Ci
status == 'canceled' status == 'canceled'
end end
def active?
running? || pending?
end
def complete?
canceled? || success? || failed?
end
def duration def duration
duration_array = latest_statuses.map(&:duration).compact duration_array = latest_statuses.map(&:duration).compact
duration_array.reduce(:+).to_i duration_array.reduce(:+).to_i
......
# == Schema Information # == Schema Information
# #
# Table name: ci_builds # project_id integer
# # status string
# id :integer not null, primary key # finished_at datetime
# project_id :integer # trace text
# status :string(255) # created_at datetime
# finished_at :datetime # updated_at datetime
# trace :text # started_at datetime
# created_at :datetime # runner_id integer
# updated_at :datetime # coverage float
# started_at :datetime # commit_id integer
# runner_id :integer # commands text
# coverage :float # job_id integer
# commit_id :integer # name string
# commands :text # deploy boolean default: false
# job_id :integer # options text
# name :string(255) # allow_failure boolean default: false, null: false
# deploy :boolean default(FALSE) # stage string
# options :text # trigger_request_id integer
# allow_failure :boolean default(FALSE), not null # stage_idx integer
# stage :string(255) # tag boolean
# trigger_request_id :integer # ref string
# stage_idx :integer # user_id integer
# tag :boolean # type string
# ref :string(255) # target_url string
# user_id :integer # description string
# type :string(255)
# target_url :string(255)
# description :string(255)
# artifacts_file :text
# #
class CommitStatus < ActiveRecord::Base class CommitStatus < ActiveRecord::Base
...@@ -79,6 +75,10 @@ class CommitStatus < ActiveRecord::Base ...@@ -79,6 +75,10 @@ class CommitStatus < ActiveRecord::Base
build.update_attributes finished_at: Time.now build.update_attributes finished_at: Time.now
end end
after_transition [:pending, :running] => :success do |build, transition|
MergeRequests::MergeWhenBuildSucceedsService.new(build.commit.gl_project, nil).trigger(build)
end
state :pending, value: 'pending' state :pending, value: 'pending'
state :running, value: 'running' state :running, value: 'running'
state :failed, value: 'failed' state :failed, value: 'failed'
......
...@@ -2,25 +2,28 @@ ...@@ -2,25 +2,28 @@
# #
# Table name: merge_requests # Table name: merge_requests
# #
# id :integer not null, primary key # id :integer not null, primary key
# target_branch :string(255) not null # target_branch :string(255) not null
# source_branch :string(255) not null # source_branch :string(255) not null
# source_project_id :integer not null # source_project_id :integer not null
# author_id :integer # author_id :integer
# assignee_id :integer # assignee_id :integer
# title :string(255) # title :string(255)
# created_at :datetime # created_at :datetime
# updated_at :datetime # updated_at :datetime
# milestone_id :integer # milestone_id :integer
# state :string(255) # state :string(255)
# merge_status :string(255) # merge_status :string(255)
# target_project_id :integer not null # target_project_id :integer not null
# iid :integer # iid :integer
# description :text # description :text
# position :integer default(0) # position :integer default(0)
# locked_at :datetime # locked_at :datetime
# updated_by_id :integer # updated_by_id :integer
# merge_error :string(255) # merge_error :string(255)
# merge_params :text (serialized to hash)
# merge_when_build_succeeds :boolean default(false), not null
# merge_user_id :integer
# #
require Rails.root.join("app/models/commit") require Rails.root.join("app/models/commit")
...@@ -35,9 +38,12 @@ class MergeRequest < ActiveRecord::Base ...@@ -35,9 +38,12 @@ class MergeRequest < ActiveRecord::Base
belongs_to :target_project, foreign_key: :target_project_id, class_name: "Project" belongs_to :target_project, foreign_key: :target_project_id, class_name: "Project"
belongs_to :source_project, foreign_key: :source_project_id, class_name: "Project" belongs_to :source_project, foreign_key: :source_project_id, class_name: "Project"
belongs_to :merge_user, class_name: "User"
has_one :merge_request_diff, dependent: :destroy has_one :merge_request_diff, dependent: :destroy
serialize :merge_params, Hash
after_create :create_merge_request_diff after_create :create_merge_request_diff
after_update :update_merge_request_diff after_update :update_merge_request_diff
...@@ -121,6 +127,7 @@ class MergeRequest < ActiveRecord::Base ...@@ -121,6 +127,7 @@ class MergeRequest < ActiveRecord::Base
validates :source_branch, presence: true validates :source_branch, presence: true
validates :target_project, presence: true validates :target_project, presence: true
validates :target_branch, presence: true validates :target_branch, presence: true
validates :merge_user, presence: true, if: :merge_when_build_succeeds?
validate :validate_branches validate :validate_branches
validate :validate_fork validate :validate_fork
...@@ -258,6 +265,16 @@ class MergeRequest < ActiveRecord::Base ...@@ -258,6 +265,16 @@ class MergeRequest < ActiveRecord::Base
end end
end end
def can_cancel_merge_when_build_succeeds?(current_user)
can_be_merged_by?(current_user) || self.author == current_user
end
def can_remove_source_branch?(current_user)
!source_project.protected_branch?(source_branch) &&
!source_project.root_ref?(source_branch) &&
Ability.abilities.allowed?(current_user, :push_code, source_project)
end
def mr_and_commit_notes def mr_and_commit_notes
# Fetch comments only from last 100 commits # Fetch comments only from last 100 commits
commits_for_notes_limit = 100 commits_for_notes_limit = 100
...@@ -393,6 +410,16 @@ class MergeRequest < ActiveRecord::Base ...@@ -393,6 +410,16 @@ class MergeRequest < ActiveRecord::Base
message message
end end
def reset_merge_when_build_succeeds
return unless merge_when_build_succeeds?
self.merge_when_build_succeeds = false
self.merge_user = nil
self.merge_params = nil
self.save
end
# Return array of possible target branches # Return array of possible target branches
# depends on target project of MR # depends on target project of MR
def target_branches def target_branches
...@@ -480,8 +507,6 @@ class MergeRequest < ActiveRecord::Base ...@@ -480,8 +507,6 @@ class MergeRequest < ActiveRecord::Base
end end
def ci_commit def ci_commit
if last_commit and source_project @ci_commit ||= source_project.ci_commit(last_commit.id) if last_commit && source_project
source_project.ci_commit(last_commit.id)
end
end end
end end
...@@ -6,15 +6,12 @@ module MergeRequests ...@@ -6,15 +6,12 @@ module MergeRequests
# Executed when you do merge via GitLab UI # Executed when you do merge via GitLab UI
# #
class MergeService < MergeRequests::BaseService class MergeService < MergeRequests::BaseService
attr_reader :merge_request, :commit_message attr_reader :merge_request
def execute(merge_request, commit_message) def execute(merge_request)
@commit_message = commit_message
@merge_request = merge_request @merge_request = merge_request
unless @merge_request.mergeable? return error('Merge request is not mergeable') unless @merge_request.mergeable?
return error('Merge request is not mergeable')
end
merge_request.in_locked_state do merge_request.in_locked_state do
if commit if commit
...@@ -32,7 +29,7 @@ module MergeRequests ...@@ -32,7 +29,7 @@ module MergeRequests
committer = repository.user_to_committer(current_user) committer = repository.user_to_committer(current_user)
options = { options = {
message: commit_message, message: params[:commit_message] || merge_request.merge_commit_message,
author: committer, author: committer,
committer: committer committer: committer
} }
...@@ -46,6 +43,11 @@ module MergeRequests ...@@ -46,6 +43,11 @@ module MergeRequests
def after_merge def after_merge
MergeRequests::PostMergeService.new(project, current_user).execute(merge_request) MergeRequests::PostMergeService.new(project, current_user).execute(merge_request)
if params[:should_remove_source_branch]
DeleteBranchService.new(@merge_request.source_project, current_user).
execute(merge_request.source_branch)
end
end end
end end
end end
module MergeRequests
class MergeWhenBuildSucceedsService < MergeRequests::BaseService
# Marks the passed `merge_request` to be merged when the build succeeds or
# updates the params for the automatic merge
def execute(merge_request)
merge_request.merge_params.merge!(params)
# The service is also called when the merge params are updated.
already_approved = merge_request.merge_when_build_succeeds?
unless already_approved
merge_request.merge_when_build_succeeds = true
merge_request.merge_user = @current_user
SystemNoteService.merge_when_build_succeeds(merge_request, @project, @current_user, merge_request.last_commit)
end
merge_request.save
end
# Triggers the automatic merge of merge_request once the build succeeds
def trigger(build)
merge_requests = merge_request_from(build)
merge_requests.each do |merge_request|
next unless merge_request.merge_when_build_succeeds?
if merge_request.ci_commit && merge_request.ci_commit.success? && merge_request.mergeable?
MergeWorker.perform_async(merge_request.id, merge_request.merge_user_id, merge_request.merge_params)
end
end
end
# Cancels the automatic merge
def cancel(merge_request)
if merge_request.merge_when_build_succeeds? && merge_request.open?
merge_request.reset_merge_when_build_succeeds
SystemNoteService.cancel_merge_when_build_succeeds(merge_request, @project, @current_user)
success
else
error("Can't cancel the automatic merge", 406)
end
end
private
def merge_request_from(build)
merge_requests = @project.origin_merge_requests.opened.where(source_branch: build.ref).to_a
merge_requests += @project.fork_merge_requests.opened.where(source_branch: build.ref).to_a
merge_requests.uniq.select(&:source_project)
end
end
end
...@@ -11,6 +11,7 @@ module MergeRequests ...@@ -11,6 +11,7 @@ module MergeRequests
# empty diff during a manual merge # empty diff during a manual merge
close_merge_requests close_merge_requests
reload_merge_requests reload_merge_requests
reset_merge_when_build_succeeds
# Leave a system note if a branch was deleted/added # Leave a system note if a branch was deleted/added
if branch_added? || branch_removed? if branch_added? || branch_removed?
...@@ -57,7 +58,6 @@ module MergeRequests ...@@ -57,7 +58,6 @@ module MergeRequests
merge_requests = filter_merge_requests(merge_requests) merge_requests = filter_merge_requests(merge_requests)
merge_requests.each do |merge_request| merge_requests.each do |merge_request|
if merge_request.source_branch == @branch_name || force_push? if merge_request.source_branch == @branch_name || force_push?
merge_request.reload_code merge_request.reload_code
merge_request.mark_as_unchecked merge_request.mark_as_unchecked
...@@ -76,6 +76,10 @@ module MergeRequests ...@@ -76,6 +76,10 @@ module MergeRequests
end end
end end
def reset_merge_when_build_succeeds
merge_requests_for_source_branch.each(&:reset_merge_when_build_succeeds)
end
def find_new_commits def find_new_commits
if branch_added? if branch_added?
@commits = [] @commits = []
......
...@@ -130,6 +130,20 @@ class SystemNoteService ...@@ -130,6 +130,20 @@ class SystemNoteService
create_note(noteable: noteable, project: project, author: author, note: body) create_note(noteable: noteable, project: project, author: author, note: body)
end end
# Called when 'merge when build succeeds' is executed
def self.merge_when_build_succeeds(noteable, project, author, last_commit)
body = "Enabled an automatic merge when the build for #{last_commit.to_reference(project)} succeeds"
create_note(noteable: noteable, project: project, author: author, note: body)
end
# Called when 'merge when build succeeds' is canceled
def self.cancel_merge_when_build_succeeds(noteable, project, author)
body = "Canceled the automatic merge"
create_note(noteable: noteable, project: project, author: author, note: body)
end
# Called when the title of a Noteable is changed # Called when the title of a Noteable is changed
# #
# noteable - Noteable object that responds to `title` # noteable - Noteable object that responds to `title`
......
- ci_commit = merge_request.ci_commit
%li{ class: mr_css_classes(merge_request) } %li{ class: mr_css_classes(merge_request) }
.merge-request-title .merge-request-title
%span.merge-request-title-text %span.merge-request-title-text
= link_to_gfm merge_request.title, merge_request_path(merge_request), class: "row_title" = link_to_gfm merge_request.title, merge_request_path(merge_request), class: "row_title"
.pull-right.light .pull-right.light
- if ci_commit - if merge_request.ci_commit
= render_ci_status(ci_commit) = render_ci_status(merge_request.ci_commit)
- if merge_request.merged? - if merge_request.merged?
%span %span
= icon('check') = icon('check')
......
:plain
$('.mr-widget-body').html("#{escape_javascript(render('projects/merge_requests/widget/open/accept'))}");
- if @status - case @status
- when :success
:plain :plain
merge_request_widget.mergeInProgress(#{params[:should_remove_source_branch] == '1'}); merge_request_widget.mergeInProgress(#{params[:should_remove_source_branch] == '1'});
- when :merge_when_build_succeeds
:plain
$('.mr-widget-body').html("#{escape_javascript(render('projects/merge_requests/widget/open/merge_when_build_succeeds'))}");
- else - else
:plain :plain
$('.mr-widget-body').html("#{escape_javascript(render('projects/merge_requests/widget/open/reload'))}"); $('.mr-widget-body').html("#{escape_javascript(render('projects/merge_requests/widget/open/reload'))}");
...@@ -14,7 +14,7 @@ ...@@ -14,7 +14,7 @@
= @merge_request.target_branch = @merge_request.target_branch
The source branch has been removed. The source branch has been removed.
- elsif can_remove_branch?(@merge_request.source_project, @merge_request.source_branch) - elsif @merge_request.can_remove_source_branch?(current_user)
.remove_source_branch_widget .remove_source_branch_widget
%p %p
= succeed '.' do = succeed '.' do
...@@ -50,5 +50,3 @@ ...@@ -50,5 +50,3 @@
$('.remove_source_branch_in_progress').hide(); $('.remove_source_branch_in_progress').hide();
$('.remove_source_branch_widget.failed').show(); $('.remove_source_branch_widget.failed').show();
}); });
...@@ -13,6 +13,8 @@ ...@@ -13,6 +13,8 @@
= render 'projects/merge_requests/widget/open/conflicts' = render 'projects/merge_requests/widget/open/conflicts'
- elsif @merge_request.work_in_progress? - elsif @merge_request.work_in_progress?
= render 'projects/merge_requests/widget/open/wip' = render 'projects/merge_requests/widget/open/wip'
- elsif @merge_request.merge_when_build_succeeds?
= render 'projects/merge_requests/widget/open/merge_when_build_succeeds'
- elsif !@merge_request.can_be_merged_by?(current_user) - elsif !@merge_request.can_be_merged_by?(current_user)
= render 'projects/merge_requests/widget/open/not_allowed' = render 'projects/merge_requests/widget/open/not_allowed'
- elsif @merge_request.can_be_merged? - elsif @merge_request.can_be_merged?
......
...@@ -3,26 +3,60 @@ ...@@ -3,26 +3,60 @@
= form_for [:merge, @project.namespace.becomes(Namespace), @project, @merge_request], remote: true, method: :post, html: { class: 'accept-mr-form js-requires-input' } do |f| = form_for [:merge, @project.namespace.becomes(Namespace), @project, @merge_request], remote: true, method: :post, html: { class: 'accept-mr-form js-requires-input' } do |f|
= hidden_field_tag :authenticity_token, form_authenticity_token = hidden_field_tag :authenticity_token, form_authenticity_token
.accept-merge-holder.clearfix.js-toggle-container .accept-merge-holder.clearfix.js-toggle-container
.accept-action .clearfix
= f.button class: "btn btn-create accept_merge_request#{status_class}" do .accept-action
Accept Merge Request - if @ci_commit && @ci_commit.active?
- if can_remove_branch?(@merge_request.source_project, @merge_request.source_branch) && !@merge_request.for_fork? %span.btn-group
.accept-control.checkbox = link_to "#", class: "btn btn-create merge_when_build_succeeds" do
= label_tag :should_remove_source_branch, class: "remove_source_checkbox" do Merge When Build Succeeds
= check_box_tag :should_remove_source_branch %a.btn.btn-success.dropdown-toggle{ 'data-toggle' => 'dropdown' }
Remove source branch %span.caret
.accept-control.right %span.sr-only
= link_to "#", class: "modify-merge-commit-link js-toggle-button" do Select Merge Moment
= icon('edit') %ul.dropdown-menu.dropdown-menu-right{ role: 'menu' }
Modify commit message %li
.js-toggle-content.hide.prepend-top-20 = link_to "#", class: "merge_when_build_succeeds" do
= icon('check fw')
Merge When Build Succeeds
%li
= link_to "#", class: "accept_merge_request" do
= icon('warning fw')
Merge Immediately
- else
= f.button class: "btn btn-create btn-grouped accept_merge_request #{status_class}" do
Accept Merge Request
- if @merge_request.can_remove_source_branch?(current_user)
.accept-control.checkbox
= label_tag :should_remove_source_branch, class: "remove_source_checkbox" do
= check_box_tag :should_remove_source_branch
Remove source branch
.accept-control.right
= link_to "#", class: "modify-merge-commit-link js-toggle-button" do
= icon('edit')
Modify commit message
.js-toggle-content.hide.prepend-top-default
= render 'shared/commit_message_container', params: params, = render 'shared/commit_message_container', params: params,
text: @merge_request.merge_commit_message, text: @merge_request.merge_commit_message,
rows: 14, hint: true rows: 14, hint: true
= hidden_field_tag :merge_when_build_succeeds, "", autocomplete: "off"
:javascript :javascript
$('.accept-mr-form').on('ajax:before', function() { $('.accept_merge_request').on('click', function() {
var btn = $('.accept_merge_request'); $(this).html("<i class='fa fa-spinner fa-spin'></i> Merge in progress");
btn.disable(); });
btn.html("<i class='fa fa-spinner fa-spin'></i> Merge in progress");
$('.accept-mr-form').on('ajax:send', function() {
$(".accept-mr-form :input").disable();
});
$('a.accept_merge_request').on('click', function(e) {
e.preventDefault();
$(this).closest("form").submit();
});
$('a.merge_when_build_succeeds').on('click', function(e) {
e.preventDefault();
$("#merge_when_build_succeeds").val("1");
$(this).closest("form").submit();
}); });
%h4
Set by #{link_to_member(@project, @merge_request.merge_user, avatar: true)}
to be merged automatically when the build succeeds.
%div
- should_remove_source_branch = @merge_request.merge_params["should_remove_source_branch"].present?
%p
= succeed '.' do
The changes will be merged into
%span.label-branch= @merge_request.target_branch
- if should_remove_source_branch
The source branch will be removed.
- else
The source branch will not be removed.
- remove_source_branch_button = @merge_request.can_remove_source_branch?(current_user) && !should_remove_source_branch
- user_can_cancel_automatic_merge = @merge_request.can_cancel_merge_when_build_succeeds?(current_user)
- if remove_source_branch_button || user_can_cancel_automatic_merge
.clearfix.prepend-top-10
- if remove_source_branch_button
= link_to merge_namespace_project_merge_request_path(@merge_request.target_project.namespace, @merge_request.target_project, @merge_request, merge_when_build_succeeds: true, should_remove_source_branch: true), remote: true, method: :post, class: "btn btn-grouped btn-primary btn-sm remove_source_branch" do
= icon('times')
Remove Source Branch When Merged
- if user_can_cancel_automatic_merge
= link_to cancel_merge_when_build_succeeds_namespace_project_merge_request_path(@merge_request.target_project.namespace, @merge_request.target_project, @merge_request), remote: true, method: :post, class: "btn btn-grouped btn-warning btn-sm" do
Cancel Automatic Merge
...@@ -8,16 +8,7 @@ class MergeWorker ...@@ -8,16 +8,7 @@ class MergeWorker
current_user = User.find(current_user_id) current_user = User.find(current_user_id)
merge_request = MergeRequest.find(merge_request_id) merge_request = MergeRequest.find(merge_request_id)
result = MergeRequests::MergeService.new(merge_request.target_project, current_user). MergeRequests::MergeService.new(merge_request.target_project, current_user, params).
execute(merge_request, params[:commit_message]) execute(merge_request)
if result[:status] == :success && params[:should_remove_source_branch].present?
DeleteBranchService.new(merge_request.source_project, current_user).
execute(merge_request.source_branch)
merge_request.source_project.repository.expire_branch_names
end
result
end end
end end
...@@ -573,8 +573,9 @@ Rails.application.routes.draw do ...@@ -573,8 +573,9 @@ Rails.application.routes.draw do
get :commits get :commits
get :diffs get :diffs
get :builds get :builds
post :merge
get :merge_check get :merge_check
post :merge
post :cancel_merge_when_build_succeeds
get :ci_status get :ci_status
post :toggle_subscription post :toggle_subscription
end end
......
class AddMergeWhenBuildSucceedsToMergeRequest < ActiveRecord::Migration
def change
add_column :merge_requests, :merge_params, :text
add_column :merge_requests, :merge_when_build_succeeds, :boolean, default: false, null: false
add_column :merge_requests, :merge_user_id, :integer
end
end
...@@ -476,9 +476,9 @@ ActiveRecord::Schema.define(version: 20151203162133) do ...@@ -476,9 +476,9 @@ ActiveRecord::Schema.define(version: 20151203162133) do
add_index "merge_request_diffs", ["merge_request_id"], name: "index_merge_request_diffs_on_merge_request_id", unique: true, using: :btree add_index "merge_request_diffs", ["merge_request_id"], name: "index_merge_request_diffs_on_merge_request_id", unique: true, using: :btree
create_table "merge_requests", force: :cascade do |t| create_table "merge_requests", force: :cascade do |t|
t.string "target_branch", null: false t.string "target_branch", null: false
t.string "source_branch", null: false t.string "source_branch", null: false
t.integer "source_project_id", null: false t.integer "source_project_id", null: false
t.integer "author_id" t.integer "author_id"
t.integer "assignee_id" t.integer "assignee_id"
t.string "title" t.string "title"
...@@ -487,13 +487,16 @@ ActiveRecord::Schema.define(version: 20151203162133) do ...@@ -487,13 +487,16 @@ ActiveRecord::Schema.define(version: 20151203162133) do
t.integer "milestone_id" t.integer "milestone_id"
t.string "state" t.string "state"
t.string "merge_status" t.string "merge_status"
t.integer "target_project_id", null: false t.integer "target_project_id", null: false
t.integer "iid" t.integer "iid"
t.text "description" t.text "description"
t.integer "position", default: 0 t.integer "position", default: 0
t.datetime "locked_at" t.datetime "locked_at"
t.integer "updated_by_id" t.integer "updated_by_id"
t.string "merge_error" t.string "merge_error"
t.text "merge_params"
t.boolean "merge_when_build_succeeds", default: false, null: false
t.integer "merge_user_id"
end end
add_index "merge_requests", ["assignee_id"], name: "index_merge_requests_on_assignee_id", using: :btree add_index "merge_requests", ["assignee_id"], name: "index_merge_requests_on_assignee_id", using: :btree
......
...@@ -24,9 +24,21 @@ ...@@ -24,9 +24,21 @@
- [Using Docker Images](ci/docker/using_docker_images.md) - [Using Docker Images](ci/docker/using_docker_images.md)
- [Using Docker Build](ci/docker/using_docker_build.md) - [Using Docker Build](ci/docker/using_docker_build.md)
- [Using Variables](ci/variables/README.md) - [Using Variables](ci/variables/README.md)
- [Using SSH keys](ci/ssh_keys/README.md)
- [User permissions](ci/permissions/README.md) - [User permissions](ci/permissions/README.md)
- [API](ci/api/README.md) - [API](ci/api/README.md)
### CI Languages
+ [Testing PHP](ci/languages/php.md)
### CI Services
+ [Using MySQL](ci/services/mysql.md)
+ [Using PostgreSQL](ci/services/postgres.md)
+ [Using Redis](ci/services/redis.md)
+ [Using Other Services](ci/docker/using_docker_images.md#how-to-use-other-images-as-services)
### CI Examples ### CI Examples
- [Test and deploy Ruby applications to Heroku](ci/examples/test-and-deploy-ruby-application-to-heroku.md) - [Test and deploy Ruby applications to Heroku](ci/examples/test-and-deploy-ruby-application-to-heroku.md)
......
...@@ -335,9 +335,57 @@ PUT /projects/:id/merge_request/:merge_request_id/merge ...@@ -335,9 +335,57 @@ PUT /projects/:id/merge_request/:merge_request_id/merge
Parameters: Parameters:
- `id` (required) - The ID of a project - `id` (required) - The ID of a project
- `merge_request_id` (required) - ID of MR - `merge_request_id` (required) - ID of MR
- `merge_commit_message` (optional) - Custom merge commit message - `merge_commit_message` (optional) - Custom merge commit message
- `should_remove_source_branch` (optional) - if `true` removes the source branch
- `merged_when_build_succeeds` (optional) - if `true` the MR is merge when the build succeeds
```json
{
"id": 1,
"target_branch": "master",
"source_branch": "test1",
"project_id": 3,
"title": "test1",
"state": "merged",
"upvotes": 0,
"downvotes": 0,
"author": {
"id": 1,
"username": "admin",
"email": "admin@example.com",
"name": "Administrator",
"state": "active",
"created_at": "2012-04-29T08:46:00Z"
},
"assignee": {
"id": 1,
"username": "admin",
"email": "admin@example.com",
"name": "Administrator",
"state": "active",
"created_at": "2012-04-29T08:46:00Z"
}
}
```
## Cancel Merge When Build Succeeds
If successful you'll get `200 OK`.
If you don't have permissions to accept this merge request - you'll get a 401
If the merge request is already merged or closed - you get 405 and error message 'Method Not Allowed'
In case the merge request is not set to be merged when the build succeeds, you'll also get a 406 error.
```
PUT /projects/:id/merge_request/:merge_request_id/cancel_merge_when_build_succeeds
```
Parameters:
- `id` (required) - The ID of a project
- `merge_request_id` (required) - ID of MR
```json ```json
{ {
......
...@@ -9,6 +9,18 @@ ...@@ -9,6 +9,18 @@
+ [Using Docker Images](docker/using_docker_images.md) + [Using Docker Images](docker/using_docker_images.md)
+ [Using Docker Build](docker/using_docker_build.md) + [Using Docker Build](docker/using_docker_build.md)
+ [Using Variables](variables/README.md) + [Using Variables](variables/README.md)
+ [Using SSH keys](ssh_keys/README.md)
### Languages
+ [Testing PHP](languages/php.md)
### Services
+ [Using MySQL](services/mysql.md)
+ [Using PostgreSQL](services/postgres.md)
+ [Using Redis](services/redis.md)
+ [Using Other Services](docker/using_docker_images.md#how-to-use-other-images-as-services)
### Examples ### Examples
......
This diff is collapsed.
### Languages
This is a list of languages you can test with GitLab CI. Each section has
comprehensive documentation and comes with a test repository hosted on
GitLab.com
+ [Testing PHP](php.md)
# Testing PHP projects
This guide covers basic building instructions for PHP projects.
There are covered two cases: testing using the Docker executor and testing
using the Shell executor.
## Test PHP projects using the Docker executor
While it is possible to test PHP apps on any system, this would require manual
configuration from the developer. To overcome this we will be using the
official [PHP docker image][php-hub] that can be found in Docker Hub.
This will allow us to test PHP projects against different versions of PHP.
However, not everything is plug 'n' play, you still need to onfigure some
things manually.
As with every build, you need to create a valid `.gitlab-ci.yml` describing the
build environment.
Let's first specify the PHP image that will be used for the build process
(you can read more about what an image means in the Runner's lingo reading
about [Using Docker images](../docker/using_docker_images.md#what-is-image)).
Start by adding the image to your `.gitlab-ci.yml`:
```yaml
image: php:5.6
```
The official images are great, but they lack a few useful tools for testing.
We need to first prepare the build environment. A way to overcome this is to
create a script which installs all prerequisites prior the actual testing is
done.
Let's create a `ci/docker_install.sh` file in the root directory of our
repository with the following content:
```bash
#!/bin/bash
# We need to install dependencies only for Docker
[[ ! -e /.dockerinit ]] && exit 0
set -xe
# Install git (the php image doesn't have it) which is required by composer
apt-get update -yqq
apt-get install git -yqq
# Install phpunit, the tool that we will use for testing
curl -o /usr/local/bin/phpunit https://phar.phpunit.de/phpunit.phar
chmod +x /usr/local/bin/phpunit
# Install mysql driver
# Here you can install any other extension that you need
docker-php-ext-install pdo_mysql
```
You might wonder what `docker-php-ext-install` is. In short, it is a script
provided by the official php docker image that you can use to easilly install
extensions. For more information read the the documentation at
<https://hub.docker.com/_/php/>.
Now that we created the script that contains all prerequisites for our build
environment, let's add it in `.gitlab-ci.yml`:
```yaml
...
before_script:
- bash ci/docker_install.sh > /dev/null
...
```
Last step, run the actual tests using `phpunit`:
```yaml
...
test:app:
script:
- phpunit --configuration phpunit_myapp.xml
...
```
Finally, commit your files and push them to GitLab to see your build succeeding
(or failing).
The final `.gitlab-ci.yml` should look similar to this:
```yaml
# Select image from https://hub.docker.com/_/php/
image: php:5.6
before_script:
# Install dependencies
- ci/docker_install.sh > /dev/null
test:app:
script:
- phpunit --configuration phpunit_myapp.xml
```
### Test against different PHP versions in Docker builds
Testing against multiple versions of PHP is super easy. Just add another job
with a different docker image version and the runner will do the rest:
```yaml
before_script:
# Install dependencies
- ci/docker_install.sh > /dev/null
# We test PHP5.6
test:5.6:
image: php:5.6
script:
- phpunit --configuration phpunit_myapp.xml
# We test PHP7.0 (good luck with that)
test:7.0:
image: php:7.0
script:
- phpunit --configuration phpunit_myapp.xml
```
### Custom PHP configuration in Docker builds
There are times where you will need to customise your PHP environment by
putting your `.ini` file into `/usr/local/etc/php/conf.d/`. For that purpose
add a `before_script` action:
```yaml
before_script:
- cp my_php.ini /usr/local/etc/php/conf.d/test.ini
```
Of course, `my_php.ini` must be present in the root directory of your repository.
## Test PHP projects using the Shell executor
The shell executor runs your builds in a terminal session on your server.
Thus, in order to test your projects you first need to make sure that all
dependencies are installed.
For example, in a VM running Debian 8 we first update the cache, then we
install `phpunit` and `php5-mysql`:
```bash
sudo apt-get update -y
sudo apt-get install -y phpunit php5-mysql
```
Next, add the following snippet to your `.gitlab-ci.yml`:
```yaml
test:app:
script:
- phpunit --configuration phpunit_myapp.xml
```
Finally, push to GitLab and let the tests begin!
### Test against different PHP versions in Shell builds
The [phpenv][] project allows you to easily manage different versions of PHP
each with its own config. This is specially usefull when testing PHP projects
with the Shell executor.
You will have to install it on your build machine under the `gitlab-runner`
user following [the upstream installation guide][phpenv-installation].
Using phpenv also allows to easily configure the PHP environment with:
```
phpenv config-add my_config.ini
```
*__Important note:__ It seems `phpenv/phpenv`
[is abandoned](https://github.com/phpenv/phpenv/issues/57). There is a fork
at [madumlao/phpenv](https://github.com/madumlao/phpenv) that tries to bring
the project back to life. [CHH/phpenv](https://github.com/CHH/phpenv) also
seems like a good alternative. Picking any of the mentioned tools will work
with the basic phpenv commands. Guiding you to choose the right phpenv is out
of the scope of this tutorial.*
### Install custom extensions
Since this is a pretty bare installation of the PHP environment, you may need
some extensions that are not currently present on the build machine.
To install additional extensions simply execute:
```bash
pecl install <extension>
```
It's not advised to add this to `.gitlab-ci.yml`. You should execute this
command once, only to setup the build environment.
## Extend your tests
### Using atoum
Instead of PHPUnit, you can use any other tool to run unit tests. For example
you can use [atoum](https://github.com/atoum/atoum):
```yaml
before_script:
- wget http://downloads.atoum.org/nightly/mageekguy.atoum.phar
test:atoum:
script:
- php mageekguy.atoum.phar
```
### Using Composer
The majority of the PHP projects use Composer for managing their PHP packages.
In order to execute Composer before running your tests, simply add the
following in your `.gitlab-ci.yml`:
```yaml
...
# Composer stores all downloaded packages in the vendor/ directory.
# Do not use the following if the vendor/ directory is commited to
# your git repository.
cache:
paths:
- vendor/
before_script:
# Install composer dependencies
- curl -sS https://getcomposer.org/installer | php
- php composer.phar install
...
```
## Access private packages / dependencies
If your test suite needs to access a private repository, you need to configure
[the SSH keys](../ssh_keys/README.md) in order to be able to clone it.
## Use databases or other services
Most of the time you will need a running database in order for your tests to
run. If you are using the Docker executor you can leverage Docker's ability to
link to other containers. In GitLab Runner lingo, this can be achieved by
defining a `service`.
This functionality is covered in [the CI services](../services/README.md)
documentation.
## Testing things locally
With GitLab Runner 1.0 you can also test any changes locally. From your
terminal execute:
```bash
# Check using docker executor
gitlab-runner exec docker test:app
# Check using shell executor
gitlab-runner exec shell test:app
```
## Example project
We have set up an [Example PHP Project][php-example-repo] for your convenience
that runs on [GitLab.com](https://gitlab.com) using our publicly available
[shared runners](../runners/README.md).
Want to hack on it? Simply fork it, commit and push your changes. Within a few
moments the changes will be picked by a public runner and the build will begin.
[php-hub]: https://hub.docker.com/_/php/
[phpenv]: https://github.com/phpenv/phpenv
[phpenv-installation]: https://github.com/phpenv/phpenv#installation
[php-example-repo]: https://gitlab.com/gitlab-examples/php
## GitLab CI Services
GitLab CI uses the `services` keyword to define what docker containers should be
linked with your base image. Below is a list of examples you may use.
+ [Using MySQL](mysql.md)
+ [Using PostgreSQL](postgres.md)
+ [Using Redis](redis.md)
+ [Using Other Services](../docker/using_docker_images.md#how-to-use-other-images-as-services)
## GitLab CI Services
+ [Using MySQL](mysql.md)
+ [Using PostgreSQL](postgres.md)
+ [Using Redis](redis.md)
# Using MySQL
As many applications depend on MySQL as their database, you will eventually
need it in order for your tests to run. Below you are guided how to do this
with the Docker and Shell executors of GitLab Runner.
## Use MySQL with the Docker executor
If you are using [GitLab Runner](../runners/README.md) with the Docker executor
you basically have everything set up already.
First, in your `.gitlab-ci.yml` add:
```yaml
services:
- mysql:latest
variables:
# Configure mysql environment variables (https://hub.docker.com/_/mysql/)
MYSQL_DATABASE: el_duderino
MYSQL_ROOT_PASSWORD: mysql_strong_password
```
And then configure your application to use the database, for example:
```yaml
Host: mysql
User: root
Password: mysql_strong_password
Database: el_duderino
```
If you are wondering why we used `mysql` for the `Host`, read more at
[How is service linked to the build](../docker/using_docker_images.md#how-is-service-linked-to-the-build).
You can also use any other docker image available on [Docker Hub][hub-mysql].
For example, to use MySQL 5.5 the service becomes `mysql:5.5`.
The `mysql` image can accept some environment variables. For more details
check the documentation on [Docker Hub][hub-mysql].
## Use MySQL with the Shell executor
You can also use MySQL on manually configured servers that are using
GitLab Runner with the Shell executor.
First install the MySQL server:
```bash
sudo apt-get install -y mysql-server mysql-client libmysqlclient-dev
```
Pick a MySQL root password (can be anything), and type it twice when asked.
*Note: As a security measure you can run `mysql_secure_installation` to
remove anonymous users, drop the test database and disable remote logins with
the root user.*
The next step is to create a user, so login to MySQL as root:
```bash
mysql -u root -p
```
Then create a user (in our case `runner`) which will be used by your
application. Change `$password` in the command below to a real strong password.
*Note: Do not type `mysql>`, this is part of the MySQL prompt.*
```bash
mysql> CREATE USER 'runner'@'localhost' IDENTIFIED BY '$password';
```
Create the database:
```bash
mysql> CREATE DATABASE IF NOT EXISTS `el_duderino` DEFAULT CHARACTER SET `utf8` COLLATE `utf8_unicode_ci`;
```
Grant the necessary permissions on the database:
```bash
mysql> GRANT SELECT, INSERT, UPDATE, DELETE, CREATE, CREATE TEMPORARY TABLES, DROP, INDEX, ALTER, LOCK TABLES ON `el_duderino`.* TO 'runner'@'localhost';
```
If all went well you can now quit the database session:
```bash
mysql> \q
```
Now, try to connect to the newly created database to check that everything is
in place:
```bash
mysql -u runner -p -D el_duderino
```
As a final step, configure your application to use the database, for example:
```bash
Host: localhost
User: runner
Password: $password
Database: el_duderino
```
## Example project
We have set up an [Example MySQL Project][mysql-example-repo] for your
convenience that runs on [GitLab.com](https://gitlab.com) using our publicly
available [shared runners](../runners/README.md).
Want to hack on it? Simply fork it, commit and push your changes. Within a few
moments the changes will be picked by a public runner and the build will begin.
[hub-mysql]: https://hub.docker.com/_/mysql/
[mysql-example-repo]: https://gitlab.com/gitlab-examples/mysql
# Using PostgreSQL
As many applications depend on PostgreSQL as their database, you will
eventually need it in order for your tests to run. Below you are guided how to
do this with the Docker and Shell executors of GitLab Runner.
## Use PostgreSQL with the Docker executor
If you are using [GitLab Runner](../runners/README.md) with the Docker executor
you basically have everything set up already.
First, in your `.gitlab-ci.yml` add:
```yaml
services:
- postgres:latest
variables:
POSTGRES_DB: nice_marmot
POSTGRES_USER: runner
POSTGRES_PASSWORD: ""
```
And then configure your application to use the database, for example:
```yaml
Host: postgres
User: runner
Password:
Database: nice_marmot
```
If you are wondering why we used `postgres` for the `Host`, read more at
[How is service linked to the build](../docker/using_docker_images.md#how-is-service-linked-to-the-build).
You can also use any other docker image available on [Docker Hub][hub-pg].
For example, to use PostgreSQL 9.3 the service becomes `postgres:9.3`.
The `postgres` image can accept some environment variables. For more details
check the documentation on [Docker Hub][hub-pg].
## Use PostgreSQL with the Shell executor
You can also use PostgreSQL on manually configured servers that are using
GitLab Runner with the Shell executor.
First install the PostgreSQL server:
```bash
sudo apt-get install -y postgresql postgresql-client libpq-dev
```
The next step is to create a user, so login to PostgreSQL:
```bash
sudo -u postgres psql -d template1
```
Then create a user (in our case `runner`) which will be used by your
application. Change `$password` in the command below to a real strong password.
*__Note:__ Do not type `template1=#`, this is part of the PostgreSQL prompt.*
```bash
template1=# CREATE USER runner WITH PASSWORD '$password' CREATEDB;
```
*__Note:__ Notice that we created the user with the privilege to be able to
create databases (`CREATEDB`). In the following steps we will create a database
explicitly for that user but having that privilege can be useful if in your
testing framework you have tools that drop and create databases.*
Create the database and grant all privileges on it for the user `runner`:
```bash
template1=# CREATE DATABASE nice_marmot OWNER runner;
```
If all went well you can now quit the database session:
```bash
template1=# \q
```
Now, try to connect to the newly created database with the user `runner` to
check that everything is in place.
```bash
psql -U runner -h localhost -d nice_marmot -W
```
*__Note:__ We are explicitly telling `psql` to connect to localhost in order
to use the md5 authentication. If you omit this step you will be denied access.*
Finally, configure your application to use the database, for example:
```yaml
Host: localhost
User: runner
Password: $password
Database: nice_marmot
```
## Example project
We have set up an [Example PostgreSQL Project][postgres-example-repo] for your
convenience that runs on [GitLab.com](https://gitlab.com) using our publicly
available [shared runners](../runners/README.md).
Want to hack on it? Simply fork it, commit and push your changes. Within a few
moments the changes will be picked by a public runner and the build will begin.
[hub-pg]: https://hub.docker.com/_/postgres/
[postgres-example-repo]: https://gitlab.com/gitlab-examples/postgres
# Using Redis
As many applications depend on Redis as their key-value store, you will
eventually need it in order for your tests to run. Below you are guided how to
do this with the Docker and Shell executors of GitLab Runner.
## Use Redis with the Docker executor
If you are using [GitLab Runner](../runners/README.md) with the Docker executor
you basically have everything set up already.
First, in your `.gitlab-ci.yml` add:
```yaml
services:
- redis:latest
```
Then you need to configure your application to use the Redis database, for
example:
```yaml
Host: redis
```
And that's it. Redis will now be available to be used within your testing
framework.
You can also use any other docker image available on [Docker Hub][hub-redis].
For example, to use Redis 2.8 the service becomes `redis:2.8`.
## Use Redis with the Shell executor
Redis can also be used on manually configured servers that are using GitLab
Runner with the Shell executor.
In your build machine install the Redis server:
```bash
sudo apt-get install redis-server
```
Verify that you can connect to the server with the `gitlab-runner` user:
```bash
# Try connecting the the Redis server
sudo -u gitlab-runner -H redis-cli
# Quit the session
127.0.0.1:6379> quit
```
Finally, configure your application to use the database, for example:
```yaml
Host: localhost
```
## Example project
We have set up an [Example Redis Project][redis-example-repo] for your convenience
that runs on [GitLab.com](https://gitlab.com) using our publicly available
[shared runners](../runners/README.md).
Want to hack on it? Simply fork it, commit and push your changes. Within a few
moments the changes will be picked by a public runner and the build will begin.
[hub-redis]: https://hub.docker.com/_/redis/
[redis-example-repo]: https://gitlab.com/gitlab-examples/redis
# Using SSH keys
GitLab currently doesn't have built-in support for managing SSH keys in a build
environment.
The SSH keys can be useful when:
1. You want to checkout internal submodules
2. You want to download private packages using your package manager (eg. bundler)
3. You want to deploy your application to eg. Heroku or your own server
4. You want to execute SSH commands from the build server to the remote server
5. You want to rsync files from your build server to the remote server
If anything of the above rings a bell, then you most likely need an SSH key.
## Inject keys in your build server
The most widely supported method is to inject an SSH key into your build
environment by extending your `.gitlab-ci.yml`.
This is the universal solution which works with any type of executor
(docker, shell, etc.).
### How it works
1. Create a new SSH key pair with [ssh-keygen][]
2. Add the private key as a **Secret Variable** to the project
3. Run the [ssh-agent][] during build to load the private key.
## SSH keys when using the Docker executor
You will first need to create an SSH key pair. For more information, follow the
instructions to [generate an SSH key](../ssh/README.md).
Then, create a new **Secret Variable** in your project settings on GitLab
following **Settings > Variables**. As **Key** add the name `SSH_PRIVATE_KEY`
and in the **Value** field paste the content of your _private_ key that you
created earlier.
Next you need to modify your `.gitlab-ci.yml` with a `before_script` action.
Add it to the top:
```
before_script:
# Install ssh-agent if not already installed, it is required by Docker.
# (change apt-get to yum if you use a CentOS-based image)
- 'which ssh-agent || ( apt-get update -y && apt-get install openssh-client -y )'
# Run ssh-agent (inside the build environment)
- eval $(ssh-agent -s)
# Add the SSH key stored in SSH_PRIVATE_KEY variable to the agent store
- ssh-add <(echo "$SSH_PRIVATE_KEY")
# For Docker builds disable host key checking. Be aware that by adding that
# you are suspectible to man-in-the-middle attacks.
# WARNING: Use this only with the Docker executor, if you use it with shell
# you will overwrite your user's SSH config.
- mkdir -p ~/.ssh
- '[[ -f /.dockerinit ]] && echo -e "Host *\n\tStrictHostKeyChecking no\n\n" > ~/.ssh/config`
```
As a final step, add the _public_ key from the one you created earlier to the
services that you want to have an access to from within the build environment.
If you are accessing a private GitLab repository you need to add it as a
[deploy key](../ssh/README.md#deploy-keys).
That's it! You can now have access to private servers or repositories in your
build environment.
## SSH keys when using the Shell executor
If you are using the Shell executor and not Docker, it is easier to set up an
SSH key.
You can generate the SSH key from the machine that GitLab Runner is installed
on, and use that key for all projects that are run on this machine.
First, you need to login to the server that runs your builds.
Then from the terminal login as the `gitlab-runner` user and generate the SSH
key pair as described in the [SSH keys documentation](../ssh/README.md).
As a final step, add the _public_ key from the one you created earlier to the
services that you want to have an access to from within the build environment.
If you are accessing a private GitLab repository you need to add it as a
[deploy key](../ssh/README.md#deploy-keys).
Once done, try to login to the remote server in order to accept the fingerprint:
```bash
ssh <address-of-my-server>
```
For accessing repositories on GitLab.com, the `<address-of-my-server>` would be
`git@gitlab.com`.
## Example project
We have set up an [Example SSH Project][ssh-example-repo] for your convenience
that runs on [GitLab.com](https://gitlab.com) using our publicly available
[shared runners](../runners/README.md).
Want to hack on it? Simply fork it, commit and push your changes. Within a few
moments the changes will be picked by a public runner and the build will begin.
[ssh-keygen]: http://linux.die.net/man/1/ssh-keygen
[ssh-agent]: http://linux.die.net/man/1/ssh-agent
[ssh-example-repo]: https://gitlab.com/gitlab-examples/ssh-private-key/
...@@ -47,7 +47,7 @@ sudo -u git -H git checkout v`cat /home/git/gitlab/GITLAB_SHELL_VERSION` -b v`ca ...@@ -47,7 +47,7 @@ sudo -u git -H git checkout v`cat /home/git/gitlab/GITLAB_SHELL_VERSION` -b v`ca
```bash ```bash
cd /home/git/gitlab-workhorse cd /home/git/gitlab-workhorse
sudo -u git -H git fetch sudo -u git -H git fetch
sudo -u git -H git checkout v`cat /home/git/gitlab/GITLAB_WORKHORSE_VERSION` -b v`cat /home/git/gitlab/GITLAB_WORKHORSE_VERSION` sudo -u git -H git checkout `cat /home/git/gitlab/GITLAB_WORKHORSE_VERSION` -b `cat /home/git/gitlab/GITLAB_WORKHORSE_VERSION`
``` ```
### 5. Install libs, migrations, etc. ### 5. Install libs, migrations, etc.
......
...@@ -17,4 +17,5 @@ ...@@ -17,4 +17,5 @@
- [Milestones](milestones.md) - [Milestones](milestones.md)
- [Merge Requests](merge_requests.md) - [Merge Requests](merge_requests.md)
- ["Work In Progress" Merge Requests](wip_merge_requests.md) - ["Work In Progress" Merge Requests](wip_merge_requests.md)
- [Merge When Build Succeeds](merge_when_build_succeeds.md)
- [Manage large binaries with Git LFS](lfs/manage_large_binaries_with_git_lfs.md) - [Manage large binaries with Git LFS](lfs/manage_large_binaries_with_git_lfs.md)
# Merge When Build Succeeds
When reviewing a merge request that looks ready to merge but still has one or more CI builds running, you can set it to be merged automatically when all builds succeed. This way, you don't have to wait for the builds to finish and remember to merge the request manually.
![Enable](merge_when_build_succeeds/enable.png)
When you hit the "Merge When Build Succeeds" button, the status of the merge request will be updated to represent the impending merge. If you cannot wait for the build to succeed and want to merge immediately, this option is available in the dropdown menu on the right of the main button.
Both team developers and the author of the merge request have the option to cancel the automatic merge if they find a reason why it shouldn't be merged after all.
![Status](merge_when_build_succeeds/status.png)
When the build succeeds, the merge request will automatically be merged. When the build fails, the author gets a chance to retry any failed builds, or to push new commits to fix the failure.
When the builds are retried and succeed on the second try, the merge request will automatically be merged after all. When the merge request is updated with new commits, the automatic merge is automatically canceled to allow the new changes to be reviewed.
...@@ -171,6 +171,7 @@ module API ...@@ -171,6 +171,7 @@ module API
expose :description expose :description
expose :work_in_progress?, as: :work_in_progress expose :work_in_progress?, as: :work_in_progress
expose :milestone, using: Entities::Milestone expose :milestone, using: Entities::Milestone
expose :merge_when_build_succeeds
end end
class MergeRequestChanges < MergeRequest class MergeRequestChanges < MergeRequest
......
...@@ -195,46 +195,54 @@ module API ...@@ -195,46 +195,54 @@ module API
# Merge MR # Merge MR
# #
# Parameters: # Parameters:
# id (required) - The ID of a project # id (required) - The ID of a project
# merge_request_id (required) - ID of MR # merge_request_id (required) - ID of MR
# merge_commit_message (optional) - Custom merge commit message # merge_commit_message (optional) - Custom merge commit message
# should_remove_source_branch (optional) - When true, the source branch will be deleted if possible
# merge_when_build_succeeds (optional) - When true, this MR will be merged when the build succeeds
# Example: # Example:
# PUT /projects/:id/merge_request/:merge_request_id/merge # PUT /projects/:id/merge_request/:merge_request_id/merge
# #
put ":id/merge_request/:merge_request_id/merge" do put ":id/merge_request/:merge_request_id/merge" do
merge_request = user_project.merge_requests.find(params[:merge_request_id]) merge_request = user_project.merge_requests.find(params[:merge_request_id])
allowed = ::Gitlab::GitAccess.new(current_user, user_project). # Merge request can not be merged
can_push_to_branch?(merge_request.target_branch) # because user dont have permissions to push into target branch
unauthorized! unless merge_request.can_be_merged_by?(current_user)
not_allowed! if !merge_request.open? || merge_request.work_in_progress?
if allowed merge_request.check_if_can_be_merged if merge_request.unchecked?
if merge_request.unchecked?
merge_request.check_if_can_be_merged
end
if merge_request.open? && !merge_request.work_in_progress? render_api_error!('Branch cannot be merged', 406) unless merge_request.can_be_merged?
if merge_request.can_be_merged?
commit_message = params[:merge_commit_message] || merge_request.merge_commit_message merge_params = {
commit_message: params[:merge_commit_message],
::MergeRequests::MergeService.new(merge_request.target_project, current_user). should_remove_source_branch: params[:should_remove_source_branch]
execute(merge_request, commit_message) }
present merge_request, with: Entities::MergeRequest if parse_boolean(params[:merge_when_build_succeeds]) && merge_request.ci_commit && merge_request.ci_commit.active?
else ::MergeRequests::MergeWhenBuildSucceedsService.new(merge_request.target_project, current_user, merge_params).
render_api_error!('Branch cannot be merged', 405) execute(merge_request)
end
else
# Merge request can not be merged
# because it is already closed/merged or marked as WIP
not_allowed!
end
else else
# Merge request can not be merged ::MergeRequests::MergeService.new(merge_request.target_project, current_user, merge_params).
# because user dont have permissions to push into target branch execute(merge_request)
unauthorized!
end end
present merge_request, with: Entities::MergeRequest
end end
# Cancel Merge if Merge When build succeeds is enabled
# Parameters:
# id (required) - The ID of a project
# merge_request_id (required) - ID of MR
#
post ":id/merge_request/:merge_request_id/cancel_merge_when_build_succeeds" do
merge_request = user_project.merge_requests.find(params[:merge_request_id])
unauthorized! unless merge_request.can_cancel_merge_when_build_succeeds?(current_user)
::MergeRequest::MergeWhenBuildSucceedsService.new(merge_request.target_project, current_user).cancel(merge_request)
end
# Get a merge request's comments # Get a merge request's comments
# #
......
...@@ -2,17 +2,18 @@ ...@@ -2,17 +2,18 @@
# #
# Table name: commits # Table name: commits
# #
# id :integer not null, primary key # id :integer not null, primary key
# project_id :integer # project_id :integer
# ref :string(255) # ref :string(255)
# sha :string(255) # sha :string(255)
# before_sha :string(255) # before_sha :string(255)
# push_data :text # push_data :text
# created_at :datetime # created_at :datetime
# updated_at :datetime # updated_at :datetime
# tag :boolean default(FALSE) # tag :boolean default(FALSE)
# yaml_errors :text # yaml_errors :text
# committed_at :datetime # committed_at :datetime
# gl_project_id :integer
# #
# Read about factories at https://github.com/thoughtbot/factory_girl # Read about factories at https://github.com/thoughtbot/factory_girl
......
...@@ -5,7 +5,7 @@ FactoryGirl.define do ...@@ -5,7 +5,7 @@ FactoryGirl.define do
name 'default' name 'default'
status 'success' status 'success'
description 'commit status' description 'commit status'
commit factory: :ci_commit commit factory: :ci_commit_with_one_job
factory :generic_commit_status, class: GenericCommitStatus do factory :generic_commit_status, class: GenericCommitStatus do
name 'generic' name 'generic'
......
...@@ -65,6 +65,11 @@ FactoryGirl.define do ...@@ -65,6 +65,11 @@ FactoryGirl.define do
target_branch "master" target_branch "master"
end end
trait :merge_when_build_succeeds do
merge_when_build_succeeds true
merge_user author
end
factory :closed_merge_request, traits: [:closed] factory :closed_merge_request, traits: [:closed]
factory :reopened_merge_request, traits: [:reopened] factory :reopened_merge_request, traits: [:reopened]
factory :merge_request_with_diffs, traits: [:with_diffs] factory :merge_request_with_diffs, traits: [:with_diffs]
......
require 'spec_helper'
feature 'Merge When Build Succeeds', feature: true, js: true do
let(:user) { create(:user) }
let(:project) { create(:project, :public) }
let(:merge_request) { create(:merge_request_with_diffs, source_project: project, author: user, title: "Bug NS-04") }
before do
project.team << [user, :master]
project.enable_ci
end
context "Active build for Merge Request" do
let!(:ci_commit) { create(:ci_commit, gl_project: project, sha: merge_request.last_commit.id, ref: merge_request.source_branch) }
let!(:ci_build) { create(:ci_build, commit: ci_commit) }
before do
login_as user
visit_merge_request(merge_request)
end
it 'displays the Merge When Build Succeeds button' do
expect(page).to have_link "Merge When Build Succeeds"
end
context "Merge When Build succeeds enabled" do
before do
click_link "Merge When Build Succeeds"
end
it 'activates Merge When Build Succeeds feature' do
expect(page).to have_link "Cancel Automatic Merge"
expect(page).to have_content "Set by #{user.name} to be merged automatically when the build succeeds."
expect(page).to have_content "The source branch will not be removed."
visit_merge_request(merge_request) # Needed to refresh the page
expect(page).to have_content /Enabled an automatic merge when the build for [0-9a-f]{8} succeeds/i
end
end
end
context 'When it is enabled' do
let(:merge_request) do
create(:merge_request_with_diffs, :simple, source_project: project, author: user,
merge_user: user, title: "MepMep", merge_when_build_succeeds: true)
end
let!(:ci_commit) { create(:ci_commit, gl_project: project, sha: merge_request.last_commit.id, ref: merge_request.source_branch) }
let!(:ci_build) { create(:ci_build, commit: ci_commit) }
before do
login_as user
visit_merge_request(merge_request)
end
it 'cancels the automatic merge' do
click_link "Cancel Automatic Merge"
expect(page).to have_link "Merge When Build Succeeds"
visit_merge_request(merge_request) # Needed to refresh the page
expect(page).to have_content "Canceled the automatic merge"
end
it "allows the user to remove the source branch" do
expect(page).to have_link "Remove Source Branch When Merged"
click_link "Remove Source Branch When Merged"
expect(page).to have_content "The source branch will be removed"
end
end
context 'Build is not active' do
it "should not allow for enabling" do
visit_merge_request(merge_request)
expect(page).not_to have_link "Merge When Build Succeeds"
end
end
def visit_merge_request(merge_request)
visit namespace_project_merge_request_path(merge_request.project.namespace, merge_request.project, merge_request)
end
end
...@@ -31,7 +31,7 @@ describe MergeRequest, models: true do ...@@ -31,7 +31,7 @@ describe MergeRequest, models: true do
describe 'associations' do describe 'associations' do
it { is_expected.to belong_to(:target_project).with_foreign_key(:target_project_id).class_name('Project') } it { is_expected.to belong_to(:target_project).with_foreign_key(:target_project_id).class_name('Project') }
it { is_expected.to belong_to(:source_project).with_foreign_key(:source_project_id).class_name('Project') } it { is_expected.to belong_to(:source_project).with_foreign_key(:source_project_id).class_name('Project') }
it { is_expected.to belong_to(:merge_user).class_name("User") }
it { is_expected.to have_one(:merge_request_diff).dependent(:destroy) } it { is_expected.to have_one(:merge_request_diff).dependent(:destroy) }
end end
...@@ -48,12 +48,32 @@ describe MergeRequest, models: true do ...@@ -48,12 +48,32 @@ describe MergeRequest, models: true do
describe 'validation' do describe 'validation' do
it { is_expected.to validate_presence_of(:target_branch) } it { is_expected.to validate_presence_of(:target_branch) }
it { is_expected.to validate_presence_of(:source_branch) } it { is_expected.to validate_presence_of(:source_branch) }
context "Validation of merge user with Merge When Build succeeds" do
it "allows user to be nil when the feature is disabled" do
expect(subject).to be_valid
end
it "is invalid without merge user" do
subject.merge_when_build_succeeds = true
expect(subject).not_to be_valid
end
it "is valid with merge user" do
subject.merge_when_build_succeeds = true
subject.merge_user = build(:user)
expect(subject).to be_valid
end
end
end end
describe 'respond to' do describe 'respond to' do
it { is_expected.to respond_to(:unchecked?) } it { is_expected.to respond_to(:unchecked?) }
it { is_expected.to respond_to(:can_be_merged?) } it { is_expected.to respond_to(:can_be_merged?) }
it { is_expected.to respond_to(:cannot_be_merged?) } it { is_expected.to respond_to(:cannot_be_merged?) }
it { is_expected.to respond_to(:merge_params) }
it { is_expected.to respond_to(:merge_when_build_succeeds) }
end end
describe '#to_reference' do describe '#to_reference' do
...@@ -172,6 +192,50 @@ describe MergeRequest, models: true do ...@@ -172,6 +192,50 @@ describe MergeRequest, models: true do
end end
end end
describe '#can_remove_source_branch?' do
let(:user) { create(:user) }
let(:user2) { create(:user) }
before do
subject.source_project.team << [user, :master]
subject.source_branch = "feature"
subject.target_branch = "master"
subject.save!
end
it "can't be removed when its a protected branch" do
allow(subject.source_project).to receive(:protected_branch?).and_return(true)
expect(subject.can_remove_source_branch?(user)).to be_falsey
end
it "cant remove a root ref" do
subject.source_branch = "master"
subject.target_branch = "feature"
expect(subject.can_remove_source_branch?(user)).to be_falsey
end
it "is unable to remove the source branch for a project the user cannot push to" do
expect(subject.can_remove_source_branch?(user2)).to be_falsey
end
it "is can be removed in all other cases" do
expect(subject.can_remove_source_branch?(user)).to be_truthy
end
end
describe "#reset_merge_when_build_succeeds" do
let(:merge_if_green) { create :merge_request, merge_when_build_succeeds: true, merge_user: create(:user) }
it "sets the item to false" do
merge_if_green.reset_merge_when_build_succeeds
merge_if_green.reload
expect(merge_if_green.merge_when_build_succeeds).to be_falsey
end
end
describe "#hook_attrs" do describe "#hook_attrs" do
it "has all the required keys" do it "has all the required keys" do
attrs = subject.hook_attrs attrs = subject.hook_attrs
......
...@@ -320,19 +320,21 @@ describe API::API, api: true do ...@@ -320,19 +320,21 @@ describe API::API, api: true do
end end
describe "PUT /projects/:id/merge_request/:merge_request_id/merge" do describe "PUT /projects/:id/merge_request/:merge_request_id/merge" do
let(:ci_commit) { create(:ci_commit_without_jobs) }
it "should return merge_request in case of success" do it "should return merge_request in case of success" do
put api("/projects/#{project.id}/merge_request/#{merge_request.id}/merge", user) put api("/projects/#{project.id}/merge_request/#{merge_request.id}/merge", user)
expect(response.status).to eq(200) expect(response.status).to eq(200)
end end
it "should return 405 if branch can't be merged" do it "should return 406 if branch can't be merged" do
allow_any_instance_of(MergeRequest). allow_any_instance_of(MergeRequest).
to receive(:can_be_merged?).and_return(false) to receive(:can_be_merged?).and_return(false)
put api("/projects/#{project.id}/merge_request/#{merge_request.id}/merge", user) put api("/projects/#{project.id}/merge_request/#{merge_request.id}/merge", user)
expect(response.status).to eq(405) expect(response.status).to eq(406)
expect(json_response['message']).to eq('Branch cannot be merged') expect(json_response['message']).to eq('Branch cannot be merged')
end end
...@@ -357,6 +359,17 @@ describe API::API, api: true do ...@@ -357,6 +359,17 @@ describe API::API, api: true do
expect(response.status).to eq(401) expect(response.status).to eq(401)
expect(json_response['message']).to eq('401 Unauthorized') expect(json_response['message']).to eq('401 Unauthorized')
end end
it "enables merge when build succeeds if the ci is active" do
allow_any_instance_of(MergeRequest).to receive(:ci_commit).and_return(ci_commit)
allow(ci_commit).to receive(:active?).and_return(true)
put api("/projects/#{project.id}/merge_request/#{merge_request.id}/merge", user), merge_when_build_succeeds: true
expect(response.status).to eq(200)
expect(json_response['title']).to eq('Test')
expect(json_response['merge_when_build_succeeds']).to eq(true)
end
end end
describe "PUT /projects/:id/merge_request/:merge_request_id" do describe "PUT /projects/:id/merge_request/:merge_request_id" do
......
...@@ -13,12 +13,13 @@ describe MergeRequests::MergeService, services: true do ...@@ -13,12 +13,13 @@ describe MergeRequests::MergeService, services: true do
describe :execute do describe :execute do
context 'valid params' do context 'valid params' do
let(:service) { MergeRequests::MergeService.new(project, user, {}) } let(:service) { MergeRequests::MergeService.new(project, user, commit_message: 'Awesome message') }
before do before do
allow(service).to receive(:execute_hooks) allow(service).to receive(:execute_hooks)
perform_enqueued_jobs do perform_enqueued_jobs do
service.execute(merge_request, 'Awesome message') service.execute(merge_request)
end end
end end
...@@ -38,14 +39,14 @@ describe MergeRequests::MergeService, services: true do ...@@ -38,14 +39,14 @@ describe MergeRequests::MergeService, services: true do
end end
context "error handling" do context "error handling" do
let(:service) { MergeRequests::MergeService.new(project, user, {}) } let(:service) { MergeRequests::MergeService.new(project, user, commit_message: 'Awesome message') }
it 'saves error if there is an exception' do it 'saves error if there is an exception' do
allow(service).to receive(:repository).and_raise("error") allow(service).to receive(:repository).and_raise("error")
allow(service).to receive(:execute_hooks) allow(service).to receive(:execute_hooks)
service.execute(merge_request, 'Awesome message') service.execute(merge_request)
expect(merge_request.merge_error).to eq("Something went wrong during merge") expect(merge_request.merge_error).to eq("Something went wrong during merge")
end end
......
require 'spec_helper'
describe MergeRequests::MergeWhenBuildSucceedsService do
let(:user) { create(:user) }
let(:merge_request) { create(:merge_request) }
let(:mr_merge_if_green_enabled) do
create(:merge_request, merge_when_build_succeeds: true, merge_user: user,
source_branch: "source_branch", target_branch: project.default_branch,
source_project: project, target_project: project, state: "opened")
end
let(:project) { create(:project) }
let(:ci_commit) { create(:ci_commit_with_one_job, ref: mr_merge_if_green_enabled.source_branch, gl_project: project) }
let(:service) { MergeRequests::MergeWhenBuildSucceedsService.new(project, user, commit_message: 'Awesome message') }
describe "#execute" do
context 'first time enabling' do
before do
allow(merge_request).to receive(:ci_commit).and_return(ci_commit)
service.execute(merge_request)
end
it 'sets the params, merge_user, and flag' do
expect(merge_request).to be_valid
expect(merge_request.merge_when_build_succeeds).to be_truthy
expect(merge_request.merge_params).to eq commit_message: 'Awesome message'
expect(merge_request.merge_user).to be user
end
it 'creates a system note' do
note = merge_request.notes.last
expect(note.note).to match /Enabled an automatic merge when the build for (\w+\/\w+@)?[0-9a-z]{8}/
end
end
context 'already approved' do
let(:service) { MergeRequests::MergeWhenBuildSucceedsService.new(project, user, new_key: true) }
let(:build) { create(:ci_build, ref: mr_merge_if_green_enabled.source_branch) }
before do
allow(mr_merge_if_green_enabled).to receive(:ci_commit).and_return(ci_commit)
allow(mr_merge_if_green_enabled).to receive(:mergeable?).and_return(true)
allow(ci_commit).to receive(:success?).and_return(true)
end
it 'updates the merge params' do
expect(SystemNoteService).not_to receive(:merge_when_build_succeeds)
service.execute(mr_merge_if_green_enabled)
expect(mr_merge_if_green_enabled.merge_params).to have_key(:new_key)
end
end
end
describe "#trigger" do
let(:build) { create(:ci_build, ref: mr_merge_if_green_enabled.source_branch, status: "success") }
it "merges all merge requests with merge when build succeeds enabled" do
allow_any_instance_of(MergeRequest).to receive(:ci_commit).and_return(ci_commit)
allow(ci_commit).to receive(:success?).and_return(true)
expect(MergeWorker).to receive(:perform_async)
service.trigger(build)
end
end
describe "#cancel" do
before do
service.cancel(mr_merge_if_green_enabled)
end
it "resets all the merge_when_build_succeeds params" do
expect(mr_merge_if_green_enabled.merge_when_build_succeeds).to be_falsey
expect(mr_merge_if_green_enabled.merge_params).to eq({})
expect(mr_merge_if_green_enabled.merge_user).to be nil
end
it 'Posts a system note' do
note = mr_merge_if_green_enabled.notes.last
expect(note.note).to include 'Canceled the automatic merge'
end
end
end
...@@ -17,7 +17,9 @@ describe MergeRequests::RefreshService, services: true do ...@@ -17,7 +17,9 @@ describe MergeRequests::RefreshService, services: true do
source_project: @project, source_project: @project,
source_branch: 'master', source_branch: 'master',
target_branch: 'feature', target_branch: 'feature',
target_project: @project) target_project: @project,
merge_when_build_succeeds: true,
merge_user: @user)
@fork_merge_request = create(:merge_request, @fork_merge_request = create(:merge_request,
source_project: @fork_project, source_project: @fork_project,
...@@ -46,6 +48,7 @@ describe MergeRequests::RefreshService, services: true do ...@@ -46,6 +48,7 @@ describe MergeRequests::RefreshService, services: true do
it { expect(@merge_request.notes).not_to be_empty } it { expect(@merge_request.notes).not_to be_empty }
it { expect(@merge_request).to be_open } it { expect(@merge_request).to be_open }
it { expect(@merge_request.merge_when_build_succeeds).to be_falsey}
it { expect(@fork_merge_request).to be_open } it { expect(@fork_merge_request).to be_open }
it { expect(@fork_merge_request.notes).to be_empty } it { expect(@fork_merge_request.notes).to be_empty }
end end
...@@ -146,6 +149,7 @@ describe MergeRequests::RefreshService, services: true do ...@@ -146,6 +149,7 @@ describe MergeRequests::RefreshService, services: true do
end end
end end
def reload_mrs def reload_mrs
@merge_request.reload @merge_request.reload
@fork_merge_request.reload @fork_merge_request.reload
......
...@@ -207,6 +207,32 @@ describe SystemNoteService, services: true do ...@@ -207,6 +207,32 @@ describe SystemNoteService, services: true do
end end
end end
describe '.merge_when_build_succeeds' do
let(:ci_commit) { build :ci_commit_without_jobs }
let(:noteable) { create :merge_request }
subject { described_class.merge_when_build_succeeds(noteable, project, author, noteable.last_commit) }
it_behaves_like 'a system note'
it "posts the Merge When Build Succeeds system note" do
expect(subject.note).to match /Enabled an automatic merge when the build for (\w+\/\w+@)?[0-9a-f]{40} succeeds/
end
end
describe '.cancel_merge_when_build_succeeds' do
let(:ci_commit) { build :ci_commit_without_jobs }
let(:noteable) { create :merge_request }
subject { described_class.cancel_merge_when_build_succeeds(noteable, project, author) }
it_behaves_like 'a system note'
it "posts the Merge When Build Succeeds system note" do
expect(subject.note).to eq "Canceled the automatic merge"
end
end
describe '.change_title' do describe '.change_title' do
subject { described_class.change_title(noteable, project, author, 'Old title') } subject { described_class.change_title(noteable, project, author, 'Old title') }
......
Markdown is supported
0%
or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment