Commit 2aa91fd2 authored by Filipa Lacerda's avatar Filipa Lacerda

Merge branch 'jprovazn-rebase-ee' into 'master'

Backport 'Rebase' feature from EE to CE

See merge request gitlab-org/gitlab-ee!3890
parents f88c2fe0 3ae31798
<script>
import simplePoll from '~/lib/utils/simple_poll';
import eventHub from '~/vue_merge_request_widget/event_hub';
import statusIcon from '~/vue_merge_request_widget/components/mr_widget_status_icon';
import loadingIcon from '~/vue_shared/components/loading_icon.vue';
import Flash from '~/flash';
import simplePoll from '../../../lib/utils/simple_poll';
import eventHub from '../../event_hub';
import statusIcon from '../mr_widget_status_icon';
import loadingIcon from '../../../vue_shared/components/loading_icon.vue';
import Flash from '../../../flash';
export default {
name: 'MRWidgetRebase',
props: {
mr: {
type: Object,
......
......@@ -32,6 +32,7 @@ export { default as UnresolvedDiscussionsState } from './components/states/mr_wi
export { default as PipelineBlockedState } from './components/states/mr_widget_pipeline_blocked';
export { default as PipelineFailedState } from './components/states/mr_widget_pipeline_failed';
export { default as MergeWhenPipelineSucceedsState } from './components/states/mr_widget_merge_when_pipeline_succeeds';
export { default as RebaseState } from './components/states/mr_widget_rebase.vue';
export { default as AutoMergeFailed } from './components/states/mr_widget_auto_merge_failed';
export { default as CheckingState } from './components/states/mr_widget_checking';
export { default as MRWidgetStore } from 'ee/vue_merge_request_widget/stores/mr_widget_store';
......
......@@ -10,6 +10,7 @@ import {
MergedState,
ClosedState,
MergingState,
RebaseState,
WipState,
ArchivedState,
ConflictsState,
......@@ -230,6 +231,7 @@ export default {
'mr-widget-pipeline-failed': PipelineFailedState,
'mr-widget-merge-when-pipeline-succeeds': MergeWhenPipelineSucceedsState,
'mr-widget-auto-merge-failed': AutoMergeFailed,
'mr-widget-rebase': RebaseState,
},
template: `
<div class="mr-state-widget prepend-top-default">
......
......@@ -37,6 +37,10 @@ export default class MRWidgetService {
return axios.get(this.endpoints.mergeActionsContentPath);
}
rebase() {
return axios.post(this.endpoints.rebasePath);
}
static stopEnvironment(url) {
return axios.post(url);
}
......
......@@ -25,6 +25,8 @@ export default function deviseState(data) {
return this.mergeError ? stateKey.autoMergeFailed : stateKey.mergeWhenPipelineSucceeds;
} else if (!this.canMerge) {
return stateKey.notAllowedToMerge;
} else if (this.shouldBeRebased) {
return stateKey.rebase;
} else if (this.canBeMerged) {
return stateKey.readyToMerge;
}
......
......@@ -28,6 +28,7 @@ export default class MergeRequestStore {
this.divergedCommitsCount = data.diverged_commits_count;
this.pipeline = data.pipeline || {};
this.deployments = this.deployments || data.deployments || [];
this.initRebase(data);
if (data.issues_links) {
const links = data.issues_links;
......@@ -126,6 +127,13 @@ export default class MergeRequestStore {
return this.state === stateKey.nothingToMerge;
}
initRebase(data) {
this.canPushToSourceBranch = data.can_push_to_source_branch;
this.rebaseInProgress = data.rebase_in_progress;
this.approvalsLeft = !data.approved;
this.rebasePath = data.rebase_path;
}
static buildMetrics(metrics) {
if (!metrics) {
return {};
......
......@@ -17,6 +17,7 @@ const stateToComponentMap = {
failedToMerge: 'mr-widget-failed-to-merge',
autoMergeFailed: 'mr-widget-auto-merge-failed',
shaMismatch: 'mr-widget-sha-mismatch',
rebase: 'mr-widget-rebase',
};
const statesToShowHelpWidget = [
......@@ -29,6 +30,7 @@ const statesToShowHelpWidget = [
'pipelineFailed',
'pipelineBlocked',
'autoMergeFailed',
'rebase',
];
export const stateKey = {
......@@ -46,6 +48,7 @@ export const stateKey = {
mergeWhenPipelineSucceeds: 'mergeWhenPipelineSucceeds',
notAllowedToMerge: 'notAllowedToMerge',
readyToMerge: 'readyToMerge',
rebase: 'rebase',
};
export default {
......
......@@ -12,6 +12,7 @@ class Projects::MergeRequestsController < Projects::MergeRequests::ApplicationCo
before_action :authorize_update_issuable!, only: [:close, :edit, :update, :remove_wip, :sort]
before_action :set_issuables_index, only: [:index]
before_action :authenticate_user!, only: [:assign_related_issues]
before_action :check_user_can_push_to_source_branch!, only: [:rebase]
def index
@merge_requests = @issuables
......@@ -226,6 +227,12 @@ class Projects::MergeRequestsController < Projects::MergeRequests::ApplicationCo
render json: environments
end
def rebase
RebaseWorker.perform_async(@merge_request.id, current_user.id)
render nothing: true, status: 200
end
protected
alias_method :subscribable_resource, :merge_request
......@@ -331,4 +338,14 @@ class Projects::MergeRequestsController < Projects::MergeRequests::ApplicationCo
@finder_type = MergeRequestsFinder
super
end
def check_user_can_push_to_source_branch!
return access_denied! unless @merge_request.source_branch_exists?
access_check = ::Gitlab::UserAccess
.new(current_user, project: @merge_request.source_project)
.can_push_to_branch?(@merge_request.source_branch)
access_denied! unless access_check
end
end
......@@ -161,6 +161,13 @@ class MergeRequest < ActiveRecord::Base
'!'
end
def rebase_in_progress?
# The source project can be deleted
return false unless source_project
source_project.repository.rebase_in_progress?(id)
end
# Use this method whenever you need to make sure the head_pipeline is synced with the
# branch head commit, for example checking if a merge request can be merged.
# For more information check: https://gitlab.com/gitlab-org/gitlab-ce/issues/40004
......
......@@ -1154,6 +1154,13 @@ class Repository
@project.repository_storage_path
end
def rebase(user, merge_request)
raw.rebase(user, merge_request.id, branch: merge_request.source_branch,
branch_sha: merge_request.source_branch_sha,
remote_repository: merge_request.target_project.repository.raw,
remote_branch: merge_request.target_branch)
end
private
# TODO Generice finder, later split this on finders by Ref or Oid
......
......@@ -22,6 +22,7 @@ class MergeRequestWidgetEntity < IssuableEntity
# EE-specific
expose :approvals_before_merge
expose :squash
expose :rebase_commit_sha
expose :rebase_in_progress?, as: :rebase_in_progress
......
......@@ -10,4 +10,4 @@
No merge commits are created and all merges are fast-forwarded, which means that merging is only allowed if the branch could be fast-forwarded.
%br
%span.descr
When fast-forward merge is not possible, the user must first rebase locally.
When fast-forward merge is not possible, the user is given the option to rebase.
......@@ -10,4 +10,4 @@
This way you could make sure that if this merge request would build, after merging to target branch it would also build.
%br
%span.descr
When fast-forward merge is not possible, the user must first rebase locally.
When fast-forward merge is not possible, the user is given the option to rebase.
......@@ -89,6 +89,7 @@
- project_service
- propagate_service_template
- reactive_caching
- rebase
- repository_fork
- repository_import
- storage_migrator
......
......@@ -105,6 +105,7 @@ constraints(ProjectUrlConstrainer.new) do
post :remove_wip
post :assign_related_issues
post :rebase
scope constraints: { format: nil }, action: :show do
get :commits, defaults: { tab: 'commits' }
......
......@@ -2,7 +2,6 @@ import { n__, s__, sprintf } from '~/locale';
import CEWidgetOptions from '~/vue_merge_request_widget/mr_widget_options';
import WidgetApprovals from './components/approvals/mr_widget_approvals';
import GeoSecondaryNode from './components/states/mr_widget_secondary_geo_node';
import RebaseState from './components/states/mr_widget_rebase.vue';
import collapsibleSection from './components/mr_widget_report_collapsible_section.vue';
export default {
......@@ -10,7 +9,6 @@ export default {
components: {
'mr-widget-approvals': WidgetApprovals,
'mr-widget-geo-secondary-node': GeoSecondaryNode,
'mr-widget-rebase': RebaseState,
collapsibleSection,
},
data() {
......
......@@ -7,7 +7,6 @@ export default class MRWidgetService extends CEWidgetService {
super(mr);
this.approvalsPath = mr.approvalsPath;
this.rebasePath = mr.rebasePath;
}
fetchApprovals() {
......@@ -25,10 +24,6 @@ export default class MRWidgetService extends CEWidgetService {
.then(res => res.data);
}
rebase() {
return axios.post(this.rebasePath);
}
fetchReport(endpoint) { // eslint-disable-line
return axios.get(endpoint)
.then(res => res.data);
......
......@@ -5,10 +5,6 @@ export default function (data) {
return 'geoSecondaryNode';
}
if (this.shouldBeRebased) {
return 'rebase';
}
return CEGetStateKey.call(this, data);
}
......@@ -14,7 +14,6 @@ export default class MergeRequestStore extends CEMergeRequestStore {
setData(data) {
this.initGeo(data);
this.initSquashBeforeMerge(data);
this.initRebase(data);
this.initApprovals(data);
super.setData(data);
......@@ -27,13 +26,6 @@ export default class MergeRequestStore extends CEMergeRequestStore {
|| data.enable_squash_before_merge;
}
initRebase(data) {
this.canPushToSourceBranch = data.can_push_to_source_branch;
this.rebaseInProgress = data.rebase_in_progress;
this.approvalsLeft = !data.approved;
this.rebasePath = data.rebase_path;
}
initGeo(data) {
this.isGeoSecondaryNode = this.isGeoSecondaryNode || data.is_geo_secondary_node;
this.geoSecondaryHelpPath = this.geoSecondaryHelpPath || data.geo_secondary_help_path;
......
import stateMaps from '~/vue_merge_request_widget/stores/state_maps';
stateMaps.stateToComponentMap.geoSecondaryNode = 'mr-widget-geo-secondary-node';
stateMaps.stateToComponentMap.rebase = 'mr-widget-rebase';
stateMaps.statesToShowHelpWidget.push('rebase');
export default {
stateToComponentMap: stateMaps.stateToComponentMap,
......
......@@ -3,17 +3,6 @@ module EE
module MergeRequestsController
extend ActiveSupport::Concern
prepended do
before_action :check_merge_request_rebase_available!, only: [:rebase]
before_action :check_user_can_push_to_source_branch!, only: [:rebase]
end
def rebase
RebaseWorker.perform_async(merge_request.id, current_user.id)
render nothing: true, status: 200
end
def approve
unless merge_request.can_approve?(current_user)
return render_404
......@@ -63,16 +52,6 @@ module EE
attrs
end
def check_user_can_push_to_source_branch!
return access_denied! unless merge_request.source_branch_exists?
access_check = ::Gitlab::UserAccess
.new(current_user, project: merge_request.source_project)
.can_push_to_branch?(merge_request.source_branch)
access_denied! unless access_check
end
end
end
end
......@@ -20,13 +20,6 @@ module EE
delegate :sha, to: :base_pipeline, prefix: :base_pipeline, allow_nil: true
end
def rebase_in_progress?
# The source project can be deleted
return false unless source_project
source_project.repository.rebase_in_progress?(id)
end
def squash_in_progress?
# The source project can be deleted
return false unless source_project
......
......@@ -419,11 +419,6 @@ module EE
super unless mirror?
end
def merge_requests_rebase_enabled
super && feature_available?(:merge_request_rebase)
end
alias_method :merge_requests_rebase_enabled?, :merge_requests_rebase_enabled
def merge_requests_ff_only_enabled
super
end
......
......@@ -31,13 +31,6 @@ module EE
refs.map { |sha| commit(sha.strip) }
end
def rebase(user, merge_request)
raw.rebase(user, merge_request.id, branch: merge_request.source_branch,
branch_sha: merge_request.source_branch_sha,
remote_repository: merge_request.target_project.repository.raw,
remote_branch: merge_request.target_branch)
end
def squash(user, merge_request)
raw.squash(user, merge_request.id, branch: merge_request.target_branch,
start_sha: merge_request.diff_start_sha,
......
......@@ -19,7 +19,6 @@ class License < ActiveRecord::Base
jenkins_integration
ldap_group_sync
merge_request_approvers
merge_request_rebase
merge_request_squash
multiple_ldap_servers
multiple_issue_assignees
......@@ -80,7 +79,6 @@ class License < ActiveRecord::Base
issue_weights
jenkins_integration
merge_request_approvers
merge_request_rebase
merge_request_squash
multiple_issue_assignees
multiple_issue_boards
......
- form = local_assigns.fetch(:form)
- project = local_assigns.fetch(:project)
.radio
= label_tag :project_merge_method_ff do
= form.radio_button :merge_method, :ff, class: "js-merge-method-radio"
%strong Fast-forward merge
%br
%span.descr
No merge commits are created and all merges are fast-forwarded, which means that merging is only allowed if the branch could be fast-forwarded.
- if project.feature_available?(:merge_request_rebase)
%br
%span.descr
When fast-forward merge is not possible, the user is given the option to rebase.
- form = local_assigns.fetch(:form)
.radio
= label_tag :project_merge_method_rebase_merge do
= form.radio_button :merge_method, :rebase_merge, class: "js-merge-method-radio"
%strong Merge commit with semi-linear history
%br
%span.descr
A merge commit is created for every merge, but merging is only allowed if fast-forward merge is possible.
This way you could make sure that if this merge request would build, after merging to target branch it would also build.
%br
%span.descr
When fast-forward merge is not possible, the user is given the option to rebase.
......@@ -11,9 +11,9 @@
%span.descr
A merge commit is created for every merge, and merging is allowed as long as there are no conflicts.
= render 'projects/ee/merge_request_rebase_settings', form: form
= render 'projects/merge_request_rebase_settings', form: form
= render 'projects/ee/merge_request_fast_forward_settings', project: @project, form: form
= render 'projects/merge_request_fast_forward_settings', project: @project, form: form
- if @project.feature_available?(:issuable_default_templates)
......
......@@ -685,4 +685,62 @@ describe Projects::MergeRequestsController do
format: :json
end
end
describe 'POST #rebase' do
let(:viewer) { user }
def post_rebase
post :rebase, namespace_id: project.namespace, project_id: project, id: merge_request
end
def expect_rebase_worker_for(user)
expect(RebaseWorker).to receive(:perform_async).with(merge_request.id, user.id)
end
context 'successfully' do
it 'enqeues a RebaseWorker' do
expect_rebase_worker_for(viewer)
post_rebase
expect(response.status).to eq(200)
end
end
context 'with a forked project' do
let(:fork_project) { create(:project, :repository, forked_from_project: project) }
let(:fork_owner) { fork_project.owner }
before do
merge_request.update!(source_project: fork_project)
fork_project.add_reporter(user)
end
context 'user cannot push to source branch' do
it 'returns 404' do
expect_rebase_worker_for(viewer).never
post_rebase
expect(response.status).to eq(404)
end
end
context 'user can push to source branch' do
before do
project.add_reporter(fork_owner)
sign_in(fork_owner)
end
it 'returns 200' do
expect_rebase_worker_for(fork_owner)
post_rebase
expect(response.status).to eq(200)
end
end
end
end
end
......@@ -367,16 +367,6 @@ describe Projects::MergeRequestsController do
expect(RebaseWorker).to receive(:perform_async).with(merge_request.id, user.id)
end
context 'successfully' do
it 'enqeues a RebaseWorker' do
expect_rebase_worker_for(viewer)
post_rebase
expect(response.status).to eq(200)
end
end
context 'approvals pending' do
let(:project) { create(:project, :repository, approvals_before_merge: 1) }
......@@ -399,43 +389,6 @@ describe Projects::MergeRequestsController do
end
it_behaves_like 'approvals'
context 'user cannot push to source branch' do
it 'returns 404' do
expect_rebase_worker_for(viewer).never
post_rebase
expect(response.status).to eq(404)
end
end
context 'user can push to source branch' do
before do
project.add_reporter(fork_owner)
sign_in(fork_owner)
end
it 'returns 200' do
expect_rebase_worker_for(fork_owner)
post_rebase
expect(response.status).to eq(200)
end
end
end
context 'rebase unavailable in license' do
it 'returns 404' do
stub_licensed_features(merge_request_rebase: false)
expect_rebase_worker_for(viewer).never
post_rebase
expect(response.status).to eq(404)
end
end
end
end
......@@ -13,66 +13,6 @@ describe MergeRequest do
it { is_expected.to have_many(:approver_groups).dependent(:delete_all) }
end
describe '#should_be_rebased?' do
subject { merge_request.should_be_rebased? }
context 'project forbids rebase' do
it { is_expected.to be_falsy }
end
context 'project allows rebase' do
let(:project) { create(:project, :repository, merge_requests_rebase_enabled: true) }
it 'returns false when the project feature is unavailable' do
expect(merge_request.target_project).to receive(:feature_available?).with(:merge_request_rebase).at_least(:once).and_return(false)
is_expected.to be_falsy
end
it 'returns true when the project feature is available' do
expect(merge_request.target_project).to receive(:feature_available?).with(:merge_request_rebase).at_least(:once).and_return(true)
is_expected.to be_truthy
end
end
end
describe '#rebase_in_progress?' do
# Create merge request and project before we stub file calls
before do
subject
end
it 'returns true when there is a current rebase directory' do
allow(File).to receive(:exist?).and_return(true)
allow(File).to receive(:mtime).and_return(Time.now)
expect(subject.rebase_in_progress?).to be_truthy
end
it 'returns false when there is no rebase directory' do
allow(File).to receive(:exist?).and_return(false)
expect(subject.rebase_in_progress?).to be_falsey
end
it 'returns false when the rebase directory has expired' do
allow(File).to receive(:exist?).and_return(true)
allow(File).to receive(:mtime).and_return(20.minutes.ago)
expect(subject.rebase_in_progress?).to be_falsey
end
it 'returns false when the source project has been removed' do
allow(subject).to receive(:source_project).and_return(nil)
allow(File).to receive(:exist?).and_return(true)
allow(File).to receive(:mtime).and_return(Time.now)
expect(File).not_to have_received(:exist?)
expect(subject.rebase_in_progress?).to be_falsey
end
end
describe '#squash_in_progress?' do
# Create merge request and project before we stub file calls
before do
......
......@@ -1066,31 +1066,6 @@ describe Project do
end
end
describe '#merge_method' do
where(:ff, :rebase, :rebase_licensed, :method) do
true | true | true | :ff
true | true | false | :ff
true | false | true | :ff
true | false | false | :ff
false | true | true | :rebase_merge
false | true | false | :merge
false | false | true | :merge
false | false | false | :merge
end
with_them do
let(:project) { build(:project, merge_requests_rebase_enabled: rebase, merge_requests_ff_only_enabled: ff) }
subject { project.merge_method }
before do
stub_licensed_features(merge_request_rebase: rebase_licensed)
end
it { is_expected.to eq(method) }
end
end
describe '#rename_repo' do
context 'when running on a primary node' do
set(:primary) { create(:geo_node, :primary) }
......
......@@ -105,14 +105,14 @@
"merge_ongoing": { "type": "boolean" },
"ff_only_enabled": { "type": ["boolean", false] },
"should_be_rebased": { "type": "boolean" },
// EE-specific
"rebase_commit_sha": { "type": ["string", "null"] },
"approvals_before_merge": { "type": ["integer", "null"] },
"squash": { "type": "boolean" },
"rebase_in_progress": { "type": "boolean" },
"can_push_to_source_branch": { "type": "boolean" },
"rebase_path": { "type": ["string", "null"] },
// EE-specific
"approvals_before_merge": { "type": ["integer", "null"] },
"squash": { "type": "boolean" },
"approved": { "type": "boolean" },
"approvals_path": { "type": ["string", "null"] },
"codeclimate": {
......
import Vue from 'vue';
import eventHub from '~/vue_merge_request_widget/event_hub';
import component from 'ee/vue_merge_request_widget/components/states/mr_widget_rebase.vue';
import component from '~/vue_merge_request_widget/components/states/mr_widget_rebase.vue';
import mountComponent from '../../helpers/vue_mount_component_helper';
describe('Merge request widget rebase component', () => {
......
......@@ -2346,4 +2346,50 @@ describe MergeRequest do
end
end
end
describe '#should_be_rebased?' do
let(:project) { create(:project, :repository) }
it 'returns false for the same source and target branches' do
merge_request = create(:merge_request, source_project: project, target_project: project)
expect(merge_request.should_be_rebased?).to be_falsey
end
end
describe '#rebase_in_progress?' do
# Create merge request and project before we stub file calls
before do
subject
end
it 'returns true when there is a current rebase directory' do
allow(File).to receive(:exist?).and_return(true)
allow(File).to receive(:mtime).and_return(Time.now)
expect(subject.rebase_in_progress?).to be_truthy
end
it 'returns false when there is no rebase directory' do
allow(File).to receive(:exist?).and_return(false)
expect(subject.rebase_in_progress?).to be_falsey
end
it 'returns false when the rebase directory has expired' do
allow(File).to receive(:exist?).and_return(true)
allow(File).to receive(:mtime).and_return(20.minutes.ago)
expect(subject.rebase_in_progress?).to be_falsey
end
it 'returns false when the source project has been removed' do
allow(subject).to receive(:source_project).and_return(nil)
allow(File).to receive(:exist?).and_return(true)
allow(File).to receive(:mtime).and_return(Time.now)
expect(File).not_to have_received(:exist?)
expect(subject.rebase_in_progress?).to be_falsey
end
end
end
......@@ -456,14 +456,21 @@ describe Project do
end
describe '#merge_method' do
it 'returns "ff" merge_method when ff is enabled' do
project = build(:project, merge_requests_ff_only_enabled: true)
expect(project.merge_method).to be :ff
using RSpec::Parameterized::TableSyntax
where(:ff, :rebase, :method) do
true | true | :ff
true | false | :ff
false | true | :rebase_merge
false | false | :merge
end
it 'returns "merge" merge_method when ff is disabled' do
project = build(:project, merge_requests_ff_only_enabled: false)
expect(project.merge_method).to be :merge
with_them do
let(:project) { build(:project, merge_requests_rebase_enabled: rebase, merge_requests_ff_only_enabled: ff) }
subject { project.merge_method }
it { is_expected.to eq(method) }
end
end
......
......@@ -536,4 +536,67 @@ describe MergeRequestPresenter do
.to eq("<a href=\"/#{resource.source_project.full_path}/tree/#{resource.source_branch}\">#{resource.source_branch}</a>")
end
end
describe '#rebase_path' do
before do
allow(resource).to receive(:rebase_in_progress?) { rebase_in_progress }
allow(resource).to receive(:should_be_rebased?) { should_be_rebased }
allow_any_instance_of(Gitlab::UserAccess::RequestCacheExtension)
.to receive(:can_push_to_branch?)
.with(resource.source_branch)
.and_return(can_push_to_branch)
end
subject do
described_class.new(resource, current_user: user).rebase_path
end
context 'when can rebase' do
let(:rebase_in_progress) { false }
let(:can_push_to_branch) { true }
let(:should_be_rebased) { true }
before do
allow(resource).to receive(:source_branch_exists?) { true }
end
it 'returns path' do
is_expected
.to eq("/#{project.full_path}/merge_requests/#{resource.iid}/rebase")
end
end
context 'when cannot rebase' do
context 'when rebase in progress' do
let(:rebase_in_progress) { true }
let(:can_push_to_branch) { true }
let(:should_be_rebased) { true }
it 'returns nil' do
is_expected.to be_nil
end
end
context 'when user cannot merge' do
let(:rebase_in_progress) { false }
let(:can_push_to_branch) { false }
let(:should_be_rebased) { true }
it 'returns nil' do
is_expected.to be_nil
end
end
context 'should not be rebased' do
let(:rebase_in_progress) { false }
let(:can_push_to_branch) { true }
let(:should_be_rebased) { false }
it 'returns nil' do
is_expected.to be_nil
end
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