diff --git a/ee/app/models/merge_train.rb b/ee/app/models/merge_train.rb index dfa6105f3a7e74170de9dfacef12c59d06892a30..9891ff51fb0a0823cd8ff8678f6980126428489e 100644 --- a/ee/app/models/merge_train.rb +++ b/ee/app/models/merge_train.rb @@ -5,6 +5,8 @@ class MergeTrain < ApplicationRecord belongs_to :user belongs_to :pipeline, class_name: 'Ci::Pipeline' + delegate :project, to: :merge_request + class << self def all_in_train(merge_request) joined_merge_requests(merge_request).order('merge_trains.id ASC') @@ -25,6 +27,10 @@ def all_next self.class.all_in_train(merge_request).where('merge_trains.id > ?', id) end + def index + self.class.all_in_train(merge_request).where('merge_trains.id < ?', id).count + end + def first_in_train? !follower_in_train? end diff --git a/ee/app/serializers/ee/merge_request_widget_entity.rb b/ee/app/serializers/ee/merge_request_widget_entity.rb index b872aa7b9d301366d7c5877efa970bc7c903e1f8..e595ad178762b3be15ee567d5c1f777e6d5434ff 100644 --- a/ee/app/serializers/ee/merge_request_widget_entity.rb +++ b/ee/app/serializers/ee/merge_request_widget_entity.rb @@ -135,10 +135,12 @@ module MergeRequestWidgetEntity merge_request.target_project.merge_pipelines_enabled? end - expose :merge_trains_enabled?, as: :merge_trains_enabled do |merge_request| - merge_request.target_project.merge_trains_enabled? + expose :merge_trains_summary, using: ::MergeTrainsSummaryEntity, if: -> (*) { merge_trains_enabled? } do |merge_request| + merge_request end + expose :merge_train, if: -> (merge_request) { merge_request.on_train? }, using: ::MergeTrainEntity + expose :can_push_to_source_branch do |merge_request| presenter(merge_request).can_push_to_source_branch? end @@ -176,5 +178,9 @@ def base_pipeline_downloadable_path_for_report_type(file_type) object.base_pipeline&.present(current_user: current_user) &.downloadable_path_for_report_type(file_type) end + + def merge_trains_enabled? + object.target_project.merge_trains_enabled? + end end end diff --git a/ee/app/serializers/merge_train_entity.rb b/ee/app/serializers/merge_train_entity.rb new file mode 100644 index 0000000000000000000000000000000000000000..b6f9d82d82a52b4adbca60b3b3edc84019b3891a --- /dev/null +++ b/ee/app/serializers/merge_train_entity.rb @@ -0,0 +1,14 @@ +# frozen_string_literal: true + +class MergeTrainEntity < Grape::Entity + include RequestAwareEntity + + expose :index + expose :user, using: UserEntity + expose :pipeline, using: PipelineEntity + expose :created_at + + expose :cancel_path do |merge_train| + cancel_train_project_merge_request_path(merge_train.project, merge_train.merge_request) + end +end diff --git a/ee/app/serializers/merge_trains_summary_entity.rb b/ee/app/serializers/merge_trains_summary_entity.rb new file mode 100644 index 0000000000000000000000000000000000000000..28e4015ab786c158f359c7ad0abf9fc4bcc94f50 --- /dev/null +++ b/ee/app/serializers/merge_trains_summary_entity.rb @@ -0,0 +1,13 @@ +# frozen_string_literal: true + +class MergeTrainsSummaryEntity < Grape::Entity + include RequestAwareEntity + + expose :total_count do |merge_request| + MergeTrain.all_in_train(merge_request).count + end + + expose :create_path do |merge_request| + create_train_project_merge_request_path(merge_request.project, merge_request) + end +end diff --git a/ee/changelogs/unreleased/expose-merge-train-info.yml b/ee/changelogs/unreleased/expose-merge-train-info.yml new file mode 100644 index 0000000000000000000000000000000000000000..ca508ce280169de94d28d024f6e28cc4f6d5b0a1 --- /dev/null +++ b/ee/changelogs/unreleased/expose-merge-train-info.yml @@ -0,0 +1,5 @@ +--- +title: Expose merge train information +merge_request: 12966 +author: +type: added diff --git a/ee/lib/api/merge_trains.rb b/ee/lib/api/merge_trains.rb new file mode 100644 index 0000000000000000000000000000000000000000..02a159bad12936f5c52d4ee06a13366d4315f5b2 --- /dev/null +++ b/ee/lib/api/merge_trains.rb @@ -0,0 +1,66 @@ +# frozen_string_literal: true + +module API + class MergeTrains < ::Grape::API + before { authenticate! } + + helpers do + def merge_request + @merge_request ||= find_merge_request_with_access(params[:merge_request_iid]) + end + end + + params do + requires :id, type: String, desc: 'The ID of a project' + requires :merge_request_iid, type: Integer, desc: 'The IID of a merge request' + end + resource :projects, requirements: ::API::API::NAMESPACE_OR_PROJECT_REQUIREMENTS do + segment ':id/merge_requests/:merge_request_iid/trains' do + desc 'Get all entries on the merge train' do + success ::EE::API::Entities::MergeTrain + end + get do + merge_request = find_merge_request_with_access(params[:merge_request_iid]) + + present MergeTrain.all_in_train(merge_request), with: ::EE::API::Entities::MergeTrain + end + + desc 'Enqueue a merge request to a train' do + success ::EE::API::Entities::MergeTrain + end + post 'enqueue' do + begin + merge_request.get_on_train!(current_user) + + present merge_request.merge_train, with: ::EE::API::Entities::MergeTrain, status: :ok + rescue => e + present { message: e.full_messages }, status: :bad_request + end + end + + segment ':id/merge_requests/:merge_request_iid/train' do + desc 'Get the detail of the merge train' do + success ::EE::API::Entities::MergeTrain + end + get do + merge_request = find_merge_request_with_access(params[:merge_request_iid]) + + present merge_request.merge_train, with: ::EE::API::Entities::MergeTrain, status: :ok + end + + desc 'Dequeue a merge request from a train' do + success ::EE::API::Entities::MergeTrain + end + post 'dequeue' do + begin + merge_request.get_off_train! + + present merge_request.merge_train, with: ::EE::API::Entities::MergeTrain, status: :ok + rescue => e + present { message: e.full_messages }, status: :bad_request + end + end + end + end + end +end diff --git a/ee/lib/ee/api/endpoints.rb b/ee/lib/ee/api/endpoints.rb index f5efdabd89bcfc7252a694bb0ddce67f05e41df2..e81bd9c79487685843b2e3861a94bdeb381007a1 100644 --- a/ee/lib/ee/api/endpoints.rb +++ b/ee/lib/ee/api/endpoints.rb @@ -30,6 +30,7 @@ module Endpoints mount ::API::ManagedLicenses mount ::API::ProjectApprovals mount ::API::Vulnerabilities + mount ::API::MergeTrains version 'v3', using: :path do # Although the following endpoints are kept behind V3 namespace, diff --git a/ee/lib/ee/api/entities.rb b/ee/lib/ee/api/entities.rb index 3273f8fbc04107be19f674f3450077665fca2271..532c89e4447594a90432621305ebea7ec4d56a11 100644 --- a/ee/lib/ee/api/entities.rb +++ b/ee/lib/ee/api/entities.rb @@ -445,6 +445,27 @@ class ApprovalState < Grape::Entity end end + class MergeTrainsSummary < Grape::Entity + expose :total_count do |merge_request| + MergeTrain.all_in_train(merge_request).count + end + + expose :create_path do |merge_request| + create_train_project_merge_request_path(merge_request.project, merge_request) + end + end + + class MergeTrain < Grape::Entity + expose :index + expose :user, using: UserEntity + expose :pipeline, using: PipelineEntity + expose :created_at + + expose :cancel_path do |merge_train| + cancel_train_project_merge_request_path(merge_train.project, merge_train.merge_request) + end + end + class LdapGroup < Grape::Entity expose :cn end diff --git a/ee/spec/models/merge_train_spec.rb b/ee/spec/models/merge_train_spec.rb index 7f2fc3451af3ffeb2a5b5b16013413fe3fe3aad2..66b1aecda2a2e497ad642c858d7ebe3ce67aad72 100644 --- a/ee/spec/models/merge_train_spec.rb +++ b/ee/spec/models/merge_train_spec.rb @@ -114,6 +114,22 @@ end end + describe '#index' do + subject { merge_train.index } + + let(:merge_train) { merge_request.merge_train } + let!(:merge_request) { create_merge_request_on_train } + + it { is_expected.to eq(0) } + + context 'when the merge train is at the second queue' 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 eq(1) } + 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, diff --git a/ee/spec/serializers/merge_request_widget_entity_spec.rb b/ee/spec/serializers/merge_request_widget_entity_spec.rb index aa2c2da1536c8f4347d80d53a9c0321bbbf1c5cc..24354c956dd5f73710ece2f0b08f173279ace177 100644 --- a/ee/spec/serializers/merge_request_widget_entity_spec.rb +++ b/ee/spec/serializers/merge_request_widget_entity_spec.rb @@ -195,7 +195,42 @@ expect(subject.as_json).to include(:pipeline_id) end - it 'has merge trains flag' do - expect(subject.as_json).to include(:merge_trains_enabled) + describe 'Merge Trains' do + let!(:merge_train) { create(:merge_train, merge_request: merge_request) } + + before do + stub_licensed_features(merge_pipelines: true, merge_trains: true) + project.update!(merge_pipelines_enabled: true) + project.update!(merge_trains_enabled: true) + end + + it 'has merge train entity' do + expect(subject.as_json[:merge_train]).to include(:index) + expect(subject.as_json[:merge_train]).to include(:user) + expect(subject.as_json[:merge_train]).to include(:pipeline) + expect(subject.as_json[:merge_train]).to include(:created_at) + end + + it 'has merge train summary entity' do + expect(subject.as_json[:merge_trains_summary]).to include(:total_count) + end + + context 'when the merge request is not on a merge train' do + let!(:merge_train) { } + + it 'does not have merge_train entity' do + expect(subject.as_json).not_to include(:merge_train) + end + end + + context 'when the merge train feature is disabled' do + before do + project.update!(merge_trains_enabled: false) + end + + it 'does not have merge train summary entity' do + expect(subject.as_json).not_to include(:merge_trains_summary) + end + end end end diff --git a/ee/spec/serializers/merge_train_entity_spec.rb b/ee/spec/serializers/merge_train_entity_spec.rb new file mode 100644 index 0000000000000000000000000000000000000000..03052849ef90696d19229d617ed8aa57e295964f --- /dev/null +++ b/ee/spec/serializers/merge_train_entity_spec.rb @@ -0,0 +1,24 @@ +# frozen_string_literal: true + +require 'spec_helper' + +describe MergeTrainEntity do + let(:merge_train) { create(:merge_train) } + let(:entity) { described_class.new(merge_train, request: request) } + let(:request) { double('request') } + + before do + allow(request).to receive(:current_user).and_return(merge_train.user) + end + + describe '#as_json' do + subject { entity.as_json } + + it 'includes attributes' do + expect(subject[:index]).to eq(merge_train.index) + expect(subject[:user][:name]).to eq(merge_train.user.name) + expect(subject[:pipeline][:id]).to eq(merge_train.pipeline.id) + expect(subject[:created_at]).to eq(merge_train.created_at) + end + end +end diff --git a/ee/spec/serializers/merge_trains_summary_entity_spec.rb b/ee/spec/serializers/merge_trains_summary_entity_spec.rb new file mode 100644 index 0000000000000000000000000000000000000000..f5c93f6530915935b98fe0c015d36fb99c9556c8 --- /dev/null +++ b/ee/spec/serializers/merge_trains_summary_entity_spec.rb @@ -0,0 +1,16 @@ +# frozen_string_literal: true + +require 'spec_helper' + +describe MergeTrainsSummaryEntity do + let(:merge_train) { create(:merge_train) } + let(:entity) { described_class.new(merge_train.merge_request) } + + describe '#as_json' do + subject { entity.as_json } + + it 'includes attributes' do + expect(subject[:total_count]).to eq(1) + end + end +end