Commit 40d127a3 authored by Shinya Maeda's avatar Shinya Maeda

Create ActiveRecordModel and table for Merge Train feature

This database table and AR model is going to be used for
the merge train feature.
parent abfac0dd
# 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