Commit 77f8a1e3 authored by Zeger-Jan van de Weg's avatar Zeger-Jan van de Weg

Merge when build succeeds

parent 8c9e1df9
...@@ -14,6 +14,7 @@ v 8.2.0 (unreleased) ...@@ -14,6 +14,7 @@ v 8.2.0 (unreleased)
- Fix: 500 error returned if destroy request without HTTP referer (Kazuki Shimizu) - Fix: 500 error returned if destroy request without HTTP referer (Kazuki Shimizu)
- Remove deprecated CI events from project settings page - Remove deprecated CI events from project settings page
- Use issue editor as cross reference comment author when issue is edited with a new mention. - Use issue editor as cross reference comment author when issue is edited with a new mention.
- Merge when build succeeds (Zeger-Jan van de Weg)
v 8.1.1 v 8.1.1
- Fix cloning Wiki repositories via HTTP (Stan Hu) - Fix cloning Wiki repositories via HTTP (Stan Hu)
......
...@@ -19,18 +19,20 @@ ...@@ -19,18 +19,20 @@
.accept-merge-holder { .accept-merge-holder {
.accept-action { .accept-action {
display: inline-block; display: inline-block;
float: left;
} }
.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,7 +2,7 @@ class Projects::MergeRequestsController < Projects::ApplicationController ...@@ -2,7 +2,7 @@ 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, :merge, :merge_check, :edit, :update, :show, :diffs, :commits, :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] before_action :closes_issues, only: [:edit, :update, :show, :diffs, :commits]
before_action :validates_merge_request, only: [:show, :diffs, :commits] before_action :validates_merge_request, only: [:show, :diffs, :commits]
...@@ -149,15 +149,34 @@ class Projects::MergeRequestsController < Projects::ApplicationController ...@@ -149,15 +149,34 @@ 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_be_merged_by?(current_user)
if @merge_request.merge_when_build_succeeds?
@merge_request.reset_merge_when_build_succeeds
SystemNoteService.cancel_merge_when_build_succeeds(merge_request, @project, @current_user)
end
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.active?
MergeRequests::MergeWhenBuildSucceedsService.new(@project,
current_user,
merge_params: 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
...@@ -282,6 +301,10 @@ class Projects::MergeRequestsController < Projects::ApplicationController ...@@ -282,6 +301,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
......
...@@ -164,6 +164,14 @@ module Ci ...@@ -164,6 +164,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
#
# project_id integer
# status string
# finished_at datetime
# trace text
# created_at datetime
# updated_at datetime
# started_at datetime
# runner_id integer
# coverage float
# commit_id integer
# commands text
# job_id integer
# name string
# deploy boolean default: false
# options text
# allow_failure boolean default: false, null: false
# stage string
# trigger_request_id integer
# stage_idx integer
# tag boolean
# ref string
# user_id integer
# type string
# target_url string
# description string
#
class CommitStatus < ActiveRecord::Base class CommitStatus < ActiveRecord::Base
self.table_name = 'ci_builds' self.table_name = 'ci_builds'
...@@ -46,6 +75,10 @@ class CommitStatus < ActiveRecord::Base ...@@ -46,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 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'
......
...@@ -34,9 +34,12 @@ class MergeRequest < ActiveRecord::Base ...@@ -34,9 +34,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
...@@ -385,6 +388,16 @@ class MergeRequest < ActiveRecord::Base ...@@ -385,6 +388,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
......
module MergeRequests
class MergeWhenBuildSucceedsService < MergeRequests::BaseService
def execute(merge_request)
merge_request.merge_params.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
end
merge_request.save
unless already_approved
SystemNoteService.merge_when_build_succeeds(merge_request, @project, @current_user)
end
end
def trigger(build)
merge_requests = merge_request_from(build)
merge_requests.each do |merge_request|
next unless merge_request.merge_when_build_succeeds?
ci_commit = merge_request.ci_commit
if ci_commit && ci_commit.success? && merge_request.mergeable?
MergeWorker.perform_async(merge_request.id, merge_request.merge_user_id, merge_request.merge_params)
end
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
...@@ -8,6 +8,7 @@ module MergeRequests ...@@ -8,6 +8,7 @@ module MergeRequests
find_new_commits find_new_commits
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,12 @@ module MergeRequests ...@@ -76,6 +76,12 @@ module MergeRequests
end end
end end
def reset_merge_when_build_succeeds
merge_requests_for_source_branch.each do |merge_request|
merge_request.reset_merge_when_build_succeeds
end
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)
body = "Approved this request to be merged automatically when the build 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`
......
: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(); merge_request_widget.mergeInProgress();
- 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'))}");
...@@ -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?
......
...@@ -2,8 +2,15 @@ ...@@ -2,8 +2,15 @@
= 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 .accept-action
= f.button class: "btn btn-create accept_merge_request" do - ci_commit = @merge_request.ci_commit
Accept Merge Request - if ci_commit && ci_commit.active?
= f.button class: "btn btn-create btn-grouped merge_when_build_succeeds", name: "merge_when_build_succeeds" do
Merge when Build Succeeds
= f.button class: "btn btn-warning btn-grouped accept_merge_request" do
Accept Merge Request Now
- else
= f.button class: "btn btn-create btn-grouped accept_merge_request" do
Accept Merge Request
- if can_remove_branch?(@merge_request.source_project, @merge_request.source_branch) && !@merge_request.for_fork? - if can_remove_branch?(@merge_request.source_project, @merge_request.source_branch) && !@merge_request.for_fork?
.accept-control.checkbox .accept-control.checkbox
= label_tag :should_remove_source_branch, class: "remove_source_checkbox" do = label_tag :should_remove_source_branch, class: "remove_source_checkbox" do
...@@ -19,7 +26,8 @@ ...@@ -19,7 +26,8 @@
rows: 14, hint: true rows: 14, hint: true
:coffeescript :coffeescript
$('.accept-mr-form').on 'ajax:before', -> $('.accept_merge_request').on 'click', ->
btn = $('.accept_merge_request') btn = $(this)
btn.disable()
btn.html("<i class='fa fa-spinner fa-spin'></i> Merge in progress") btn.html("<i class='fa fa-spinner fa-spin'></i> Merge in progress")
$('.accept-mr-form').on 'ajax:sen', ->
$(".accept-mr-form :input").disable()
%h4
Approved by #{link_to_member(@project, @merge_request.merge_user, avatar: true)}
to be merged automatically when
#{link_to "the build", ci_status_path(@merge_request.ci_commit)} succeeds
%div
- if @merge_request.merge_params["should_remove_source_branch"]
= succeed '.' do
The changes will be merged into
%span.label-branch= @merge_request.target_branch
The source branch will be removed.
- elsif can_remove_branch?(@merge_request.source_project, @merge_request.source_branch)
- remove_source_branch_button = true
%p
= succeed '.' do
The changes will be merged into
%span.label-branch= @merge_request.target_branch
The source branch will not be removed.
- if remove_source_branch_button || @merge_request.can_be_merged_by?(current_user)
.clearfix.prepend-top-10
- if remove_source_branch_button
= link_to merge_namespace_project_merge_request_path(@merge_request.source_project.namespace, @merge_request.source_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 @merge_request.can_be_merged_by?(current_user)
= link_to merge_namespace_project_merge_request_path(@merge_request.source_project.namespace, @merge_request.source_project, @merge_request), remote: true, method: :delete, class: "btn btn-grouped btn-warning btn-sm" do
Cancel Automatic Merge
...@@ -556,6 +556,7 @@ Gitlab::Application.routes.draw do ...@@ -556,6 +556,7 @@ Gitlab::Application.routes.draw do
get :diffs get :diffs
get :commits get :commits
post :merge post :merge
delete :merge, action: :cancel_merge_when_build_succeeds
get :merge_check get :merge_check
get :ci_status get :ci_status
post :toggle_subscription post :toggle_subscription
......
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
...@@ -11,7 +11,7 @@ ...@@ -11,7 +11,7 @@
# #
# It's strongly recommended that you check this file into your version control system. # It's strongly recommended that you check this file into your version control system.
ActiveRecord::Schema.define(version: 20151026182941) do ActiveRecord::Schema.define(version: 20151028152939) do
# These are extensions that must be enabled in order to support this database # These are extensions that must be enabled in order to support this database
enable_extension "plpgsql" enable_extension "plpgsql"
...@@ -418,6 +418,7 @@ ActiveRecord::Schema.define(version: 20151026182941) do ...@@ -418,6 +418,7 @@ ActiveRecord::Schema.define(version: 20151026182941) do
end end
add_index "labels", ["project_id"], name: "index_labels_on_project_id", using: :btree add_index "labels", ["project_id"], name: "index_labels_on_project_id", using: :btree
add_index "labels", ["title"], name: "index_labels_on_title", using: :btree
create_table "members", force: true do |t| create_table "members", force: true do |t|
t.integer "access_level", null: false t.integer "access_level", null: false
...@@ -453,9 +454,9 @@ ActiveRecord::Schema.define(version: 20151026182941) do ...@@ -453,9 +454,9 @@ ActiveRecord::Schema.define(version: 20151026182941) 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: true do |t| create_table "merge_requests", force: true 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"
...@@ -464,13 +465,16 @@ ActiveRecord::Schema.define(version: 20151026182941) do ...@@ -464,13 +465,16 @@ ActiveRecord::Schema.define(version: 20151026182941) 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
...@@ -499,6 +503,7 @@ ActiveRecord::Schema.define(version: 20151026182941) do ...@@ -499,6 +503,7 @@ ActiveRecord::Schema.define(version: 20151026182941) do
add_index "milestones", ["due_date"], name: "index_milestones_on_due_date", using: :btree add_index "milestones", ["due_date"], name: "index_milestones_on_due_date", using: :btree
add_index "milestones", ["project_id", "iid"], name: "index_milestones_on_project_id_and_iid", unique: true, using: :btree add_index "milestones", ["project_id", "iid"], name: "index_milestones_on_project_id_and_iid", unique: true, using: :btree
add_index "milestones", ["project_id"], name: "index_milestones_on_project_id", using: :btree add_index "milestones", ["project_id"], name: "index_milestones_on_project_id", using: :btree
add_index "milestones", ["title"], name: "index_milestones_on_title", using: :btree
create_table "namespaces", force: true do |t| create_table "namespaces", force: true do |t|
t.string "name", null: false t.string "name", null: false
......
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