Read about our upcoming Code of Conduct on this issue

Commit 85730430 authored by Shinya Maeda's avatar Shinya Maeda
Browse files

Merge request pipelines

parent fdbace108b98
......@@ -16,6 +16,7 @@ class Pipeline < ActiveRecord::Base
belongs_to :user
belongs_to :auto_canceled_by, class_name: 'Ci::Pipeline'
belongs_to :pipeline_schedule, class_name: 'Ci::PipelineSchedule'
belongs_to :merge_request, class_name: 'MergeRequest'
has_internal_id :iid, scope: :project, presence: false, init: ->(s) do
s&.project&.pipelines&.maximum(:iid) || s&.project&.pipelines&.count
......@@ -50,6 +51,9 @@ class Pipeline < ActiveRecord::Base
validates :sha, presence: { unless: :importing? }
validates :ref, presence: { unless: :importing? }
validates :merge_request, presence: { if: :merge_request? }
validates :merge_request, absence: { unless: :merge_request? }
validates :tag, inclusion: { in: [false], if: :merge_request? }
validates :status, presence: { unless: :importing? }
validate :valid_commit_sha, unless: :importing?
......@@ -171,6 +175,13 @@ class Pipeline < ActiveRecord::Base
scope :internal, -> { where(source: internal_sources) }
scope :sort_by_merge_request_pipelines, -> do
sql = 'CASE ci_pipelines.source WHEN (?) THEN 0 ELSE 1 END, ci_pipelines.id DESC'
query = ActiveRecord::Base.send(:sanitize_sql_array, [sql, sources[:merge_request]]) # rubocop:disable GitlabSecurity/PublicSend
order(query)
end
scope :for_user, -> (user) { where(user: user) }
# Returns the pipelines in descending order (= newest first), optionally
......@@ -372,7 +383,7 @@ def commit
end
def branch?
!tag?
!tag? && !merge_request?
end
def stuck?
......@@ -619,7 +630,12 @@ def execute_hooks
# All the merge requests for which the current pipeline runs/ran against
def all_merge_requests
@all_merge_requests ||= project.merge_requests.where(source_branch: ref)
@all_merge_requests ||=
if merge_request?
project.merge_requests.where(id: merge_request.id)
else
project.merge_requests.where(source_branch: ref)
end
end
def detailed_status(current_user)
......@@ -696,6 +712,8 @@ def push_details
def git_ref
if branch?
Gitlab::Git::BRANCH_REF_PREFIX + ref.to_s
elsif merge_request?
Gitlab::Git::BRANCH_REF_PREFIX + ref.to_s
elsif tag?
Gitlab::Git::TAG_REF_PREFIX + ref.to_s
else
......
......@@ -21,7 +21,8 @@ def self.sources
trigger: 3,
schedule: 4,
api: 5,
external: 6
external: 6,
merge_request: 9
}
end
end
......
......@@ -63,6 +63,7 @@ def merge_request_diff(*args)
dependent: :delete_all # rubocop:disable Cop/ActiveRecordDependent
has_many :cached_closes_issues, through: :merge_requests_closing_issues, source: :issue
has_many :merge_request_pipelines, foreign_key: 'merge_request_id', class_name: 'Ci::Pipeline'
belongs_to :assignee, class_name: "User"
......@@ -1052,12 +1053,17 @@ def diverged_from_target_branch?
diverged_commits_count > 0
end
def all_pipelines
def all_pipelines(shas: all_commit_shas)
return Ci::Pipeline.none unless source_project
@all_pipelines ||= source_project.pipelines
.where(sha: all_commit_shas, ref: source_branch)
.order(id: :desc)
.where(sha: shas, ref: source_branch)
.where(merge_request: [nil, self])
.sort_by_merge_request_pipelines
end
def merge_request_pipeline_exists?
merge_request_pipelines.exists?(sha: diff_head_sha)
end
def has_test_reports?
......
......@@ -48,6 +48,7 @@ class PipelineEntity < Grape::Entity
expose :tag?, as: :tag
expose :branch?, as: :branch
expose :merge_request?, as: :merge_request
end
expose :commit, using: CommitEntity
......
......@@ -14,7 +14,7 @@ class CreatePipelineService < BaseService
Gitlab::Ci::Pipeline::Chain::Populate,
Gitlab::Ci::Pipeline::Chain::Create].freeze
def execute(source, ignore_skip_ci: false, save_on_errors: true, trigger_request: nil, schedule: nil, &block)
def execute(source, ignore_skip_ci: false, save_on_errors: true, trigger_request: nil, schedule: nil, merge_request: nil, &block)
@pipeline = Ci::Pipeline.new
command = Gitlab::Ci::Pipeline::Chain::Command.new(
......@@ -25,6 +25,7 @@ def execute(source, ignore_skip_ci: false, save_on_errors: true, trigger_request
before_sha: params[:before],
trigger_request: trigger_request,
schedule: schedule,
merge_request: merge_request,
ignore_skip_ci: ignore_skip_ci,
save_incompleted: save_on_errors,
seeds_block: block,
......
......@@ -54,6 +54,24 @@ def create_assignee_note(merge_request)
merge_request, merge_request.project, current_user, merge_request.assignee)
end
def create_merge_request_pipeline(merge_request, user)
return unless Feature.enabled?(:ci_merge_request_pipeline,
merge_request.source_project,
default_enabled: true)
##
# UpdateMergeRequestsWorker could be retried by an exception.
# MR pipelines should not be recreated in such case.
return if merge_request.merge_request_pipeline_exists?
Ci::CreatePipelineService
.new(merge_request.source_project, user, ref: merge_request.source_branch)
.execute(:merge_request,
ignore_skip_ci: true,
save_on_errors: false,
merge_request: merge_request)
end
# Returns all origin and fork merge requests from `@project` satisfying passed arguments.
# rubocop: disable CodeReuse/ActiveRecord
def merge_requests_for(source_branch, mr_states: [:opened])
......
......@@ -25,6 +25,7 @@ def before_create(merge_request)
def after_create(issuable)
todo_service.new_merge_request(issuable, current_user)
issuable.cache_merge_request_closes_issues!(current_user)
create_merge_request_pipeline(issuable, current_user)
update_merge_requests_head_pipeline(issuable)
super
......@@ -49,18 +50,14 @@ def update_merge_requests_head_pipeline(merge_request)
merge_request.update(head_pipeline_id: pipeline.id) if pipeline
end
# rubocop: disable CodeReuse/ActiveRecord
def head_pipeline_for(merge_request)
return unless merge_request.source_project
sha = merge_request.source_branch_sha
return unless sha
pipelines = merge_request.source_project.pipelines.where(ref: merge_request.source_branch, sha: sha)
pipelines.order(id: :desc).first
merge_request.all_pipelines(shas: sha).first
end
# rubocop: enable CodeReuse/ActiveRecord
def set_projects!
# @project is used to determine whether the user can set the merge request's
......
......@@ -92,6 +92,7 @@ def reload_merge_requests
end
merge_request.mark_as_unchecked
create_merge_request_pipeline(merge_request, current_user)
UpdateHeadPipelineForMergeRequestWorker.perform_async(merge_request.id)
end
......
......@@ -6,10 +6,11 @@ class UpdateHeadPipelineForMergeRequestWorker
queue_namespace :pipeline_processing
# rubocop: disable CodeReuse/ActiveRecord
def perform(merge_request_id)
merge_request = MergeRequest.find(merge_request_id)
pipeline = Ci::Pipeline.where(project: merge_request.source_project, ref: merge_request.source_branch).last
sha = merge_request.diff_head_sha
pipeline = merge_request.all_pipelines(shas: sha).first
return unless pipeline && pipeline.latest?
......@@ -21,7 +22,6 @@ def perform(merge_request_id)
merge_request.update_attribute(:head_pipeline_id, pipeline.id)
end
# rubocop: enable CodeReuse/ActiveRecord
def log_error_message_for(merge_request)
Rails.logger.error(
......
---
title: Merge request pipelines
merge_request: 23217
author:
type: added
# frozen_string_literal: true
class AddMergeRequestIdToCiPipelines < ActiveRecord::Migration
DOWNTIME = false
def up
add_column :ci_pipelines, :merge_request_id, :integer
end
def down
remove_column :ci_pipelines, :merge_request_id, :integer
end
end
# frozen_string_literal: true
class AddForeignKeyToCiPipelinesMergeRequests < ActiveRecord::Migration
include Gitlab::Database::MigrationHelpers
DOWNTIME = false
disable_ddl_transaction!
def up
add_concurrent_index :ci_pipelines, :merge_request_id
add_concurrent_foreign_key :ci_pipelines, :merge_requests, column: :merge_request_id, on_delete: :cascade
end
def down
if foreign_key_exists?(:ci_pipelines, :merge_requests, column: :merge_request_id)
remove_foreign_key :ci_pipelines, :merge_requests
end
remove_concurrent_index :ci_pipelines, :merge_request_id
end
end
......@@ -474,7 +474,9 @@
t.boolean "protected"
t.integer "failure_reason"
t.integer "iid"
t.integer "merge_request_id"
t.index ["auto_canceled_by_id"], name: "index_ci_pipelines_on_auto_canceled_by_id", using: :btree
t.index ["merge_request_id"], name: "index_ci_pipelines_on_merge_request_id", using: :btree
t.index ["pipeline_schedule_id"], name: "index_ci_pipelines_on_pipeline_schedule_id", using: :btree
t.index ["project_id", "iid"], name: "index_ci_pipelines_on_project_id_and_iid", unique: true, where: "(iid IS NOT NULL)", using: :btree
t.index ["project_id", "ref", "status", "id"], name: "index_ci_pipelines_on_project_id_and_ref_and_status_and_id", using: :btree
......@@ -2292,6 +2294,7 @@
add_foreign_key "ci_pipeline_variables", "ci_pipelines", column: "pipeline_id", name: "fk_f29c5f4380", on_delete: :cascade
add_foreign_key "ci_pipelines", "ci_pipeline_schedules", column: "pipeline_schedule_id", name: "fk_3d34ab2e06", on_delete: :nullify
add_foreign_key "ci_pipelines", "ci_pipelines", column: "auto_canceled_by_id", name: "fk_262d4c2d19", on_delete: :nullify
add_foreign_key "ci_pipelines", "merge_requests", name: "fk_a23be95014", on_delete: :cascade
add_foreign_key "ci_pipelines", "projects", name: "fk_86635dbd80", on_delete: :cascade
add_foreign_key "ci_runner_namespaces", "ci_runners", column: "runner_id", on_delete: :cascade
add_foreign_key "ci_runner_namespaces", "namespaces", on_delete: :cascade
......
......@@ -16,6 +16,7 @@ def perform!
trigger_requests: Array(@command.trigger_request),
user: @command.current_user,
pipeline_schedule: @command.schedule,
merge_request: @command.merge_request,
protected: @command.protected_ref?,
variables_attributes: Array(@command.variables_attributes)
)
......
......@@ -8,7 +8,7 @@ module Chain
Command = Struct.new(
:source, :project, :current_user,
:origin_ref, :checkout_sha, :after_sha, :before_sha,
:trigger_request, :schedule,
:trigger_request, :schedule, :merge_request,
:ignore_skip_ci, :save_incompleted,
:seeds_block, :variables_attributes
) do
......
# frozen_string_literal: true
require 'rails_helper'
describe 'Merge request > User sees merge request pipelines', :js do
include ProjectForksHelper
include TestReportsHelper
let(:project) { create(:project, :public, :repository) }
let(:user) { project.creator }
let(:config) do
{
build: {
script: 'build'
},
test: {
script: 'test',
only: ['merge_requests']
},
deploy: {
script: 'deploy',
except: ['merge_requests']
}
}
end
before do
stub_application_setting(auto_devops_enabled: false)
stub_feature_flags(ci_merge_request_pipeline: true)
stub_ci_pipeline_yaml_file(YAML.dump(config))
project.add_maintainer(user)
sign_in(user)
end
context 'when a user created a merge request in the parent project' do
let(:merge_request) do
create(:merge_request,
source_project: project,
target_project: project,
source_branch: 'feature',
target_branch: 'master')
end
let!(:push_pipeline) do
Ci::CreatePipelineService.new(project, user, ref: 'feature')
.execute(:push)
end
let!(:merge_request_pipeline) do
Ci::CreatePipelineService.new(project, user, ref: 'feature')
.execute(:merge_request, merge_request: merge_request)
end
before do
visit project_merge_request_path(project, merge_request)
page.within('.merge-request-tabs') do
click_link('Pipelines')
end
end
it 'sees branch pipelines and merge request pipelines in correct order' do
page.within('.ci-table') do
expect(page).to have_selector('.ci-pending', count: 2)
expect(first('.js-pipeline-url-link')).to have_content("##{merge_request_pipeline.id}")
end
end
it 'sees the latest merge request pipeline as the head pipeline' do
page.within('.ci-widget-content') do
expect(page).to have_content("##{merge_request_pipeline.id}")
end
end
context 'when a user updated a merge request in the parent project' do
let!(:push_pipeline_2) do
Ci::CreatePipelineService.new(project, user, ref: 'feature')
.execute(:push)
end
let!(:merge_request_pipeline_2) do
Ci::CreatePipelineService.new(project, user, ref: 'feature')
.execute(:merge_request, merge_request: merge_request)
end
before do
visit project_merge_request_path(project, merge_request)
page.within('.merge-request-tabs') do
click_link('Pipelines')
end
end
it 'sees branch pipelines and merge request pipelines in correct order' do
page.within('.ci-table') do
expect(page).to have_selector('.ci-pending', count: 4)
expect(all('.js-pipeline-url-link')[0])
.to have_content("##{merge_request_pipeline_2.id}")
expect(all('.js-pipeline-url-link')[1])
.to have_content("##{merge_request_pipeline.id}")
expect(all('.js-pipeline-url-link')[2])
.to have_content("##{push_pipeline_2.id}")
expect(all('.js-pipeline-url-link')[3])
.to have_content("##{push_pipeline.id}")
end
end
it 'sees the latest merge request pipeline as the head pipeline' do
page.within('.ci-widget-content') do
expect(page).to have_content("##{merge_request_pipeline_2.id}")
end
end
end
context 'when a user merges a merge request in the parent project' do
before do
click_button 'Merge when pipeline succeeds'
wait_for_requests
end
context 'when merge request pipeline is pending' do
it 'waits the head pipeline' do
expect(page).to have_content('to be merged automatically when the pipeline succeeds')
expect(page).to have_link('Cancel automatic merge')
end
end
context 'when merge request pipeline succeeds' do
before do
merge_request_pipeline.succeed!
wait_for_requests
end
it 'merges the merge request' do
expect(page).to have_content('Merged by')
expect(page).to have_link('Revert')
end
end
context 'when branch pipeline succeeds' do
before do
push_pipeline.succeed!
wait_for_requests
end
it 'waits the head pipeline' do
expect(page).to have_content('to be merged automatically when the pipeline succeeds')
expect(page).to have_link('Cancel automatic merge')
end
end
end
context 'when there are no `merge_requests` keyword in .gitlab-ci.yml' do
let(:config) do
{
build: {
script: 'build'
},
test: {
script: 'test'
},
deploy: {
script: 'deploy'
}
}
end
it 'sees a branch pipeline in pipeline tab' do
page.within('.ci-table') do
expect(page).to have_selector('.ci-pending', count: 1)
expect(first('.js-pipeline-url-link')).to have_content("##{push_pipeline.id}")
end
end
it 'sees the latest branch pipeline as the head pipeline' do
page.within('.ci-widget-content') do
expect(page).to have_content("##{push_pipeline.id}")
end
end
end
end
context 'when a user created a merge request from a forked project to the parent project' do
let(:merge_request) do
create(:merge_request,
source_project: forked_project,
target_project: project,
source_branch: 'feature',
target_branch: 'master')
end
let!(:push_pipeline) do
Ci::CreatePipelineService.new(forked_project, user2, ref: 'feature')
.execute(:push)
end
let!(:merge_request_pipeline) do
Ci::CreatePipelineService.new(forked_project, user2, ref: 'feature')
.execute(:merge_request, merge_request: merge_request)
end
let(:forked_project) { fork_project(project, user2, repository: true) }
let(:user2) { create(:user) }
before do
forked_project.add_maintainer(user2)
visit project_merge_request_path(project, merge_request)
page.within('.merge-request-tabs') do
click_link('Pipelines')
end
end
it 'sees branch pipelines and merge request pipelines in correct order' do
page.within('.ci-table') do
expect(page).to have_selector('.ci-pending', count: 2)
expect(first('.js-pipeline-url-link')).to have_content("##{merge_request_pipeline.id}")
end
end
it 'sees the latest merge request pipeline as the head pipeline' do
page.within('.ci-widget-content') do
expect(page).to have_content("##{merge_request_pipeline.id}")
end
end
it 'sees pipeline list in forked project' do
visit project_pipelines_path(forked_project)
expect(page).to have_selector('.ci-pending', count: 2)
end
context 'when a user updated a merge request from a forked project to the parent project' do
let!(:push_pipeline_2) do
Ci::CreatePipelineService.new(forked_project, user2, ref: 'feature')
.execute(:push)
end
let!(:merge_request_pipeline_2) do
Ci::CreatePipelineService.new(forked_project, user2, ref: 'feature')
.execute(:merge_request, merge_request: merge_request)
end
before do
visit project_merge_request_path(project, merge_request)
page.within('.merge-request-tabs') do
click_link('Pipelines')
end
end
it 'sees branch pipelines and merge request pipelines in correct order' do
page.within('.ci-table') do
expect(page).to have_selector('.ci-pending', count: 4)
expect(all('.js-pipeline-url-link')[0])
.to have_content("##{merge_request_pipeline_2.id}")
expect(all('.js-pipeline-url-link')[1])
.to have_content("##{merge_request_pipeline.id}")
expect(all('.js-pipeline-url-link')[2])
.to have_content("##{push_pipeline_2.id}")
expect(all('.js-pipeline-url-link')[3])
.to have_content("##{push_pipeline.id}")
end
end
it 'sees the latest merge request pipeline as the head pipeline' do
page.within('.ci-widget-content') do
expect(page).to have_content("##{merge_request_pipeline_2.id}")
end
end
it 'sees pipeline list in forked project' do
visit project_pipelines_path(forked_project)
expect(page).to have_selector('.ci-pending', count: 4)
end
end
context 'when a user merges a merge request from a forked project to the parent project' do
before do
click_button 'Merge when pipeline succeeds'
wait_for_requests
end