Commit 083a9f83 authored by Andreas Brandl's avatar Andreas Brandl

Merge branch 'create-merge-request-pipeline-on-chained-merge-ref' into 'master'

Create ActiveRecordModel and table for Merge Train feature

See merge request gitlab-org/gitlab-ee!11204
parents 5d6d05ee 40d127a3
# frozen_string_literal: true
class CreateMergeRequestTrainsTable < ActiveRecord::Migration[5.1]
include Gitlab::Database::MigrationHelpers
DOWNTIME = false
def change
create_table :merge_trains, id: :bigserial do |t|
t.references :merge_request, foreign_key: { on_delete: :cascade }, type: :integer, index: false, null: false
t.references :user, foreign_key: { on_delete: :cascade }, type: :integer, null: false
t.references :pipeline, foreign_key: { to_table: :ci_pipelines, on_delete: :nullify }, type: :integer
t.timestamps_with_timezone null: false
t.index [:merge_request_id], unique: true
end
end
end
......@@ -1947,6 +1947,17 @@ ActiveRecord::Schema.define(version: 20190426180107) do
t.index ["merge_request_id"], name: "index_merge_requests_closing_issues_on_merge_request_id", using: :btree
end
create_table "merge_trains", force: :cascade do |t|
t.integer "merge_request_id", null: false
t.integer "user_id", null: false
t.integer "pipeline_id"
t.datetime_with_timezone "created_at", null: false
t.datetime_with_timezone "updated_at", null: false
t.index ["merge_request_id"], name: "index_merge_trains_on_merge_request_id", unique: true, using: :btree
t.index ["pipeline_id"], name: "index_merge_trains_on_pipeline_id", using: :btree
t.index ["user_id"], name: "index_merge_trains_on_user_id", using: :btree
end
create_table "milestones", id: :serial, force: :cascade do |t|
t.string "title", null: false
t.integer "project_id"
......@@ -3636,6 +3647,9 @@ ActiveRecord::Schema.define(version: 20190426180107) do
add_foreign_key "merge_requests", "users", column: "updated_by_id", name: "fk_641731faff", on_delete: :nullify
add_foreign_key "merge_requests_closing_issues", "issues", on_delete: :cascade
add_foreign_key "merge_requests_closing_issues", "merge_requests", on_delete: :cascade
add_foreign_key "merge_trains", "ci_pipelines", column: "pipeline_id", on_delete: :nullify
add_foreign_key "merge_trains", "merge_requests", on_delete: :cascade
add_foreign_key "merge_trains", "users", on_delete: :cascade
add_foreign_key "milestones", "namespaces", column: "group_id", name: "fk_95650a40d4", on_delete: :cascade
add_foreign_key "milestones", "projects", name: "fk_9bd0a0c791", on_delete: :cascade
add_foreign_key "namespace_statistics", "namespaces", on_delete: :cascade
......
......@@ -21,6 +21,7 @@ module EE
has_many :approver_groups, as: :target, dependent: :delete_all # rubocop:disable Cop/ActiveRecordDependent
has_many :approval_rules, class_name: 'ApprovalMergeRequestRule', inverse_of: :merge_request
has_many :draft_notes
has_one :merge_train
validate :validate_approval_rule_source
......@@ -53,6 +54,18 @@ module EE
super
end
def get_on_train!(user)
create_merge_train!(user: user)
end
def get_off_train!
merge_train.destroy!
end
def on_train?
merge_train.present?
end
def allows_multiple_assignees?
project.multiple_mr_assignees_enabled? &&
project.feature_available?(:multiple_merge_request_assignees)
......
......@@ -61,6 +61,7 @@ module EE
accepts_nested_attributes_for :software_license_policies, allow_destroy: true
has_many :packages, class_name: 'Packages::Package'
has_many :package_files, through: :packages, class_name: 'Packages::PackageFile'
has_many :merge_trains
has_many :sourced_pipelines, class_name: 'Ci::Sources::Pipeline', foreign_key: :source_project_id
......
# frozen_string_literal: true
class MergeTrain < ApplicationRecord
belongs_to :merge_request
belongs_to :user
belongs_to :pipeline, class_name: 'Ci::Pipeline'
class << self
def all_in_train(merge_request)
joined_merge_requests(merge_request).order('merge_trains.id ASC')
end
def first_in_train(merge_request)
all_in_train(merge_request).first
end
def joined_merge_requests(merge_request)
MergeRequest.joins(:merge_train)
.where('merge_requests.target_project_id = ?', merge_request.target_project_id)
.where('merge_requests.target_branch = ?', merge_request.target_branch)
end
end
def all_next
self.class.all_in_train(merge_request).where('merge_trains.id > ?', id)
end
def first_in_train?
!follower_in_train?
end
def follower_in_train?
self.class.all_in_train(merge_request).where('merge_trains.id < ?', id).exists?
end
end
---
title: Create ActiveRecordModel and table for Merge Train feature
merge_request: 11204
author:
type: added
......@@ -8,6 +8,16 @@ FactoryBot.modify do
end
end
trait :on_train do
transient do
train_creator { author }
end
after :create do |merge_request, evaluator|
merge_request.get_on_train!(evaluator.train_creator)
end
end
transient do
approval_groups []
approval_users []
......
# frozen_string_literal: true
FactoryBot.define do
factory :merge_train do
merge_request
user
pipeline factory: :ci_pipeline
end
end
......@@ -14,6 +14,7 @@ merge_requests:
- approver_groups
- approved_by_users
- draft_notes
- merge_train
ci_pipelines:
- source_pipeline
- source_bridge
......@@ -81,6 +82,7 @@ project:
- webide_pipelines
- reviews
- incident_management_setting
- merge_trains
prometheus_metrics:
- project
- prometheus_alerts
......@@ -103,3 +105,6 @@ reviews:
- notes
incident_management_setting:
- project
merge_trains:
- project
- merge_request
......@@ -15,6 +15,7 @@ describe MergeRequest do
it { is_expected.to have_many(:approver_users).through(:approvers) }
it { is_expected.to have_many(:approver_groups).dependent(:delete_all) }
it { is_expected.to have_many(:approved_by_users) }
it { is_expected.to have_one(:merge_train) }
end
it_behaves_like 'an editable mentionable with EE-specific mentions' do
......@@ -948,4 +949,64 @@ describe MergeRequest do
it_behaves_like 'merge pipelines project option is disabled'
end
end
describe '#get_on_train!' do
subject { merge_request.get_on_train!(user) }
let(:user) { create(:user) }
it 'gets on the train' do
expect { subject }.to change { MergeTrain.count }.by(1)
end
context 'when the merge request is already on a merge train' do
before do
merge_request.get_on_train!(user)
end
it 'raises an exception' do
expect { merge_request.get_on_train!(user) }.to raise_exception(ActiveRecord::RecordNotUnique)
end
end
end
describe '#get_off_train!' do
subject { merge_request.get_off_train! }
let!(:merge_request) do
create(:merge_request, :on_train, source_project: project, target_project: project)
end
it 'gets off from the train' do
expect { subject }.to change { MergeTrain.count }.by(-1)
end
context 'when the merge request is not on a merge train yet' do
let(:merge_request) { create(:merge_request, source_project: project, target_project: project) }
it 'raises an exception' do
expect { subject }.to raise_exception(NoMethodError)
end
end
end
describe '#on_train?' do
subject { merge_request.on_train? }
context 'when the merge request is on a merge train' do
let(:merge_request) do
create(:merge_request, :on_train, source_project: project, target_project: project)
end
it { is_expected.to be_truthy }
end
context 'when the merge request is not on a merge train' do
let(:merge_request) do
create(:merge_request, source_project: project, target_project: project)
end
it { is_expected.to be_falsy }
end
end
end
# frozen_string_literal: true
require "spec_helper"
describe MergeTrain do
include ProjectForksHelper
set(:project) { create(:project, :repository) }
it { is_expected.to belong_to(:merge_request) }
it { is_expected.to belong_to(:user) }
it { is_expected.to belong_to(:pipeline) }
describe '.all_in_train' do
subject { described_class.all_in_train(merge_request) }
let!(:merge_request) { create_merge_request_on_train }
it 'returns the merge request' do
is_expected.to eq([merge_request])
end
context 'when the other merge request is on the merge train' do
let!(:merge_request_2) { create_merge_request_on_train(source_branch: 'improve/awesome') }
it 'returns the merge request' do
is_expected.to eq([merge_request, merge_request_2])
end
end
context 'when the merge request is not on merge train' do
let(:merge_request) { create(:merge_request) }
it 'returns empty array' do
is_expected.to be_empty
end
end
end
describe '.first_in_train' do
subject { described_class.first_in_train(merge_request) }
let!(:merge_request) { create_merge_request_on_train }
it 'returns the merge request' do
is_expected.to eq(merge_request)
end
context 'when the other merge request is on the merge train' do
let!(:merge_request_2) { create_merge_request_on_train(source_branch: 'improve/awesome') }
it 'returns the merge request' do
is_expected.to eq(merge_request)
end
end
context 'when the merge request is not on merge train' do
let(:merge_request) { create(:merge_request) }
it 'returns empty array' do
is_expected.to be_nil
end
end
end
describe '#all_next' do
subject { merge_train.all_next }
let(:merge_train) { merge_request.merge_train }
let!(:merge_request) { create_merge_request_on_train }
it 'returns nil' do
is_expected.to be_empty
end
context 'when the other merge request is on the merge train' do
let!(:merge_request_2) { create_merge_request_on_train(source_branch: 'improve/awesome') }
it 'returns the next merge requests' do
is_expected.to eq([merge_request_2])
end
end
end
describe '#first_in_train?' do
subject { merge_train.first_in_train? }
let(:merge_train) { merge_request.merge_train }
let!(:merge_request) { create_merge_request_on_train }
it { is_expected.to be_truthy }
context 'when the other merge request is on the merge train' do
let(:merge_train) { merge_request_2.merge_train }
let!(:merge_request_2) { create_merge_request_on_train(source_branch: 'improve/awesome') }
it { is_expected.to be_falsy }
end
end
describe '#follower_in_train?' do
subject { merge_train.follower_in_train? }
let(:merge_train) { merge_request.merge_train }
let!(:merge_request) { create_merge_request_on_train }
it { is_expected.to be_falsy }
context 'when the other merge request is on the merge train' do
let(:merge_train) { merge_request_2.merge_train }
let!(:merge_request_2) { create_merge_request_on_train(source_branch: 'improve/awesome') }
it { is_expected.to be_truthy }
end
end
def create_merge_request_on_train(target_project: project, target_branch: 'master', source_project: project, source_branch: 'feature')
create(:merge_request,
:on_train,
target_branch: target_branch,
target_project: target_project,
source_branch: source_branch,
source_project: source_project)
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