Commit 6eb944e5 authored by Dmitriy Zaporozhets's avatar Dmitriy Zaporozhets

Merge branch 'rebase_on_merge_request' into 'master'

Rebase on merge request

Introduces merge request option to rebase before merging.

Issue #185

See merge request !254
parents 2d20c1e2 9714608e
v 7.6.0 v 7.6.0
- Added Audit events related to membership changes for groups and projects - Added Audit events related to membership changes for groups and projects
- Added option to attempt a rebase before merging merge request
v 7.5.0 v 7.5.0
- Added an ability to check each author commit's email by regex - Added an ability to check each author commit's email by regex
......
...@@ -23,3 +23,8 @@ class @ProjectNew ...@@ -23,3 +23,8 @@ class @ProjectNew
$('#project_issues_tracker_id').attr('disabled', 'disabled') $('#project_issues_tracker_id').attr('disabled', 'disabled')
else else
$('#project_issues_tracker_id').removeAttr('disabled') $('#project_issues_tracker_id').removeAttr('disabled')
$("#project_merge_requests_enabled").change ->
checked = $(this).prop("checked")
$("#project_merge_requests_template").prop "disabled", not checked
$("#project_merge_requests_rebase_enabled").prop "disabled", not checked
...@@ -122,6 +122,7 @@ class Projects::MergeRequestsController < Projects::ApplicationController ...@@ -122,6 +122,7 @@ class Projects::MergeRequestsController < Projects::ApplicationController
if @merge_request.open? && @merge_request.can_be_merged? if @merge_request.open? && @merge_request.can_be_merged?
@merge_request.should_remove_source_branch = params[:should_remove_source_branch] @merge_request.should_remove_source_branch = params[:should_remove_source_branch]
@merge_request.should_rebase = params[:should_rebase]
@merge_request.automerge!(current_user, params[:commit_message]) @merge_request.automerge!(current_user, params[:commit_message])
@status = true @status = true
else else
......
...@@ -202,7 +202,8 @@ class ProjectsController < ApplicationController ...@@ -202,7 +202,8 @@ class ProjectsController < ApplicationController
params.require(:project).permit( params.require(:project).permit(
:name, :path, :description, :issues_tracker, :tag_list, :name, :path, :description, :issues_tracker, :tag_list,
:issues_enabled, :merge_requests_enabled, :snippets_enabled, :issues_tracker_id, :default_branch, :issues_enabled, :merge_requests_enabled, :snippets_enabled, :issues_tracker_id, :default_branch,
:wiki_enabled, :visibility_level, :import_url, :last_activity_at, :namespace_id, :merge_requests_template :wiki_enabled, :visibility_level, :import_url, :last_activity_at, :namespace_id, :merge_requests_template,
:merge_requests_rebase_enabled
) )
end end
end end
...@@ -19,4 +19,14 @@ module BranchesHelper ...@@ -19,4 +19,14 @@ module BranchesHelper
current_user.can?(action, project) current_user.can?(action, project)
end end
def can_rebase?(project, branch_name)
if project.protected_branch? branch_name
can?(current_user, :push_code_to_protected_branches, project)
elsif can?(current_user, :push_code, project)
true
else
false
end
end
end end
...@@ -40,6 +40,9 @@ class MergeRequest < ActiveRecord::Base ...@@ -40,6 +40,9 @@ class MergeRequest < ActiveRecord::Base
attr_accessor :should_remove_source_branch attr_accessor :should_remove_source_branch
# If true, merge request should rebase before merging
attr_accessor :should_rebase
# When this attribute is true some MR validation is ignored # When this attribute is true some MR validation is ignored
# It allows us to close or modify broken merge requests # It allows us to close or modify broken merge requests
attr_accessor :allow_broken attr_accessor :allow_broken
......
...@@ -66,6 +66,13 @@ ...@@ -66,6 +66,13 @@
= f.check_box :merge_requests_enabled = f.check_box :merge_requests_enabled
%span.descr Submit changes to be merged upstream. %span.descr Submit changes to be merged upstream.
.form-group
= f.label :merge_requests_rebase_enabled, "Merge Requests Rebase", class: 'control-label'
.col-sm-10
.checkbox
= f.check_box :merge_requests_rebase_enabled
%span.descr Allows rebasing of merge requests before merging.
.form-group .form-group
= f.label :merge_requests_template, class: 'control-label' do = f.label :merge_requests_template, class: 'control-label' do
Merge request template Merge request template
......
...@@ -24,6 +24,11 @@ ...@@ -24,6 +24,11 @@
= label_tag :should_remove_source_branch, class: "checkbox" do = label_tag :should_remove_source_branch, class: "checkbox" do
= check_box_tag :should_remove_source_branch = check_box_tag :should_remove_source_branch
Remove source-branch Remove source-branch
- if @merge_request.target_project.merge_requests_rebase_enabled && can_rebase?(@merge_request.target_project, @merge_request.target_branch)
.remove_branch_holder.pull-left
= label_tag :should_rebase, class: "checkbox" do
= check_box_tag :should_rebase
Rebase before merge
.js-toggle-container .js-toggle-container
%label %label
%i.fa.fa-edit %i.fa.fa-edit
......
class AddMergeRequestRebaseEnabledToProjects < ActiveRecord::Migration
def change
add_column :projects, :merge_requests_rebase_enabled, :boolean, default: false
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: 20141118150935) do ActiveRecord::Schema.define(version: 20141126120926) 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"
...@@ -318,6 +318,7 @@ ActiveRecord::Schema.define(version: 20141118150935) do ...@@ -318,6 +318,7 @@ ActiveRecord::Schema.define(version: 20141118150935) do
t.float "repository_size", default: 0.0 t.float "repository_size", default: 0.0
t.integer "star_count", default: 0, null: false t.integer "star_count", default: 0, null: false
t.text "merge_requests_template" t.text "merge_requests_template"
t.boolean "merge_requests_rebase_enabled", default: false
end end
add_index "projects", ["creator_id"], name: "index_projects_on_creator_id", using: :btree add_index "projects", ["creator_id"], name: "index_projects_on_creator_id", using: :btree
......
...@@ -238,6 +238,16 @@ In a CI strategy you can merge in master at the start of the day to prevent pain ...@@ -238,6 +238,16 @@ In a CI strategy you can merge in master at the start of the day to prevent pain
In a synchronization point strategy you only merge in from well defined points in time, for example a tagged release. In a synchronization point strategy you only merge in from well defined points in time, for example a tagged release.
This strategy is [advocated by Linus Torvalds](https://www.mail-archive.com/dri-devel@lists.sourceforge.net/msg39091.html) because the state of the code at these points is better known. This strategy is [advocated by Linus Torvalds](https://www.mail-archive.com/dri-devel@lists.sourceforge.net/msg39091.html) because the state of the code at these points is better known.
GitLab Enterprise Edition offers a way to rebase before merging a merge request. You can configure this per project bassis by navigating to the project settings page and selecting `Merge Requests Rebase` checkbox.
![Merge request settings](merge_request_settings.png)
Before accepting a merge request, select `rebase before merge`.
![Merge request widget](merge_request_widget.png)
GitLab will attempt to cleanly rebase before merging branches. If clean rebase is not possible, regular merge will be performed.
If clean rebase is possible and history of the traget branch will be altered with the the merge.
In conclusion, we can say that you should try to prevent merge commits, but not eliminate them. In conclusion, we can say that you should try to prevent merge commits, but not eliminate them.
Your codebase should be clean but your history should represent what actually happened. Your codebase should be clean but your history should represent what actually happened.
Developing software happen in small messy steps and it is OK to have your history reflect this. Developing software happen in small messy steps and it is OK to have your history reflect this.
......
...@@ -2,6 +2,8 @@ module Gitlab ...@@ -2,6 +2,8 @@ module Gitlab
module Satellite module Satellite
# GitLab server-side merge # GitLab server-side merge
class MergeAction < Action class MergeAction < Action
include Gitlab::Popen
attr_accessor :merge_request attr_accessor :merge_request
def initialize(user, merge_request) def initialize(user, merge_request)
...@@ -27,16 +29,24 @@ module Gitlab ...@@ -27,16 +29,24 @@ module Gitlab
def merge!(merge_commit_message = nil) def merge!(merge_commit_message = nil)
in_locked_and_timed_satellite do |merge_repo| in_locked_and_timed_satellite do |merge_repo|
prepare_satellite!(merge_repo) prepare_satellite!(merge_repo)
# If rebase before merge
# Attempt a rebase and return if succesful.
# If unsuccesful, continue with regular merge.
if merge_request.should_rebase
if rebase_in_satellite!(merge_repo)
remove_source_branch(merge_repo)
return true
end
end
if merge_in_satellite!(merge_repo, merge_commit_message) if merge_in_satellite!(merge_repo, merge_commit_message)
# push merge back to bare repo # push merge back to bare repo
# will raise CommandFailed when push fails # will raise CommandFailed when push fails
merge_repo.git.push(default_options, :origin, merge_request.target_branch) merge_repo.git.push(default_options, :origin, merge_request.target_branch)
# remove source branch # remove source branch
if merge_request.should_remove_source_branch && !project.root_ref?(merge_request.source_branch) && !merge_request.for_fork? remove_source_branch(merge_repo)
# will raise CommandFailed when push fails
merge_repo.git.push(default_options, :origin, ":#{merge_request.source_branch}")
end
# merge, push and branch removal successful # merge, push and branch removal successful
true true
end end
...@@ -133,6 +143,40 @@ module Gitlab ...@@ -133,6 +143,40 @@ module Gitlab
handle_exception(ex) handle_exception(ex)
end end
# Rebases before merging the source_branch into the target_branch in the satellite.
# Before starting the rebase, clears out the satellite.
# Returns false if rebase cannot be done cleanly and resets the satellite.
# Returns true otherwise.
# Eg. of clean rebase
# source_branch: feature
# target_branch: master
#
# git checkout feature
# git pull --rebase origin master
# git push origin feature:master
# git merge feature
# git push remote master
def rebase_in_satellite!(repo)
update_satellite_source_and_target!(repo)
repo.git.checkout(default_options({b: true}), merge_request.source_branch, "source/#{merge_request.source_branch}")
output, status = popen(%W(git pull --rebase origin #{merge_request.target_branch}), repo.working_dir)
if status == 0
Gitlab::AppLogger.info "Rebasing before merge in #{merge_request.source_project.path_with_namespace} MR!#{merge_request.id}: #{output}."
if merge_request.source_branch && merge_request.target_branch
repo.git.push(default_options, "origin", "#{merge_request.source_branch}:#{merge_request.target_branch}")
end
else
repo.git.rebase(default_options, "--abort")
Gitlab::AppLogger.info "Rebasing in in #{merge_request.source_project.path_with_namespace} MR!#{merge_request.id} aborted, rebase manually."
prepare_satellite!(repo)
false
end
rescue Grit::Git::CommandFailed => ex
handle_exception(ex)
end
# Assumes a satellite exists that is a fresh clone of the projects repo, prepares satellite for merges, diffs etc # Assumes a satellite exists that is a fresh clone of the projects repo, prepares satellite for merges, diffs etc
def update_satellite_source_and_target!(repo) def update_satellite_source_and_target!(repo)
repo.remote_add('source', merge_request.source_project.repository.path_to_repo) repo.remote_add('source', merge_request.source_project.repository.path_to_repo)
...@@ -141,6 +185,15 @@ module Gitlab ...@@ -141,6 +185,15 @@ module Gitlab
rescue Grit::Git::CommandFailed => ex rescue Grit::Git::CommandFailed => ex
handle_exception(ex) handle_exception(ex)
end end
def remove_source_branch(repo)
if merge_request.should_remove_source_branch && !project.root_ref?(merge_request.source_branch) && !merge_request.for_fork?
# will raise CommandFailed when push fails
repo.git.push(default_options, :origin, ":#{merge_request.source_branch}")
end
rescue Grit::Git::CommandFailed => ex
handle_exception(ex)
end
end end
end end
end end
...@@ -101,4 +101,36 @@ describe 'Gitlab::Satellite::MergeAction' do ...@@ -101,4 +101,36 @@ describe 'Gitlab::Satellite::MergeAction' do
merge_request_with_conflict).can_be_merged?.should be_false } merge_request_with_conflict).can_be_merged?.should be_false }
end end
end end
describe '#can_be_rebased?' do
context 'on fork' do
before(:each) do
merge_request.stub(:should_rebase).and_return(true)
end
it { Gitlab::Satellite::MergeAction.new(
merge_request_fork.author,
merge_request_fork).merge!.should be_true }
it { Gitlab::Satellite::MergeAction.new(
merge_request_fork_with_conflict.author,
merge_request_fork_with_conflict).merge!.should be_false }
end
context 'between branches' do
before(:each) do
merge_request.stub(:should_rebase).and_return(true)
end
it { Gitlab::Satellite::MergeAction.new(
merge_request.author,
merge_request).merge!.should be_true }
it { Gitlab::Satellite::MergeAction.new(
merge_request_with_conflict.author,
merge_request_with_conflict).merge!.should be_false }
end
end
end end
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