gitlab-org--gitlab-foss/lib/api/merge_requests.rb

396 lines
17 KiB
Ruby
Raw Normal View History

module API
2012-10-21 07:00:27 -04:00
class MergeRequests < Grape::API
2016-12-04 12:11:19 -05:00
include PaginationParams
before { authenticate_non_get! }
2012-10-21 07:00:27 -04:00
helpers ::Gitlab::IssuableMetadata
# EE::API::MergeRequests would override the following helpers
helpers do
params :optional_params_ee do
end
end
def self.update_params_at_least_one_of
%i[
assignee_id
description
labels
milestone_id
remove_source_branch
state_event
target_branch
title
discussion_locked
2018-05-29 05:51:43 -04:00
squash
]
end
helpers do
def find_merge_requests(args = {})
args = declared_params.merge(args)
args[:milestone_title] = args.delete(:milestone)
args[:label_name] = args.delete(:labels)
args[:scope] = args[:scope].underscore if args[:scope]
merge_requests = MergeRequestsFinder.new(current_user, args).execute
.reorder(args[:order_by] => args[:sort])
merge_requests = paginate(merge_requests)
.preload(:source_project, :target_project)
return merge_requests if args[:view] == 'simple'
merge_requests
Use latest_merge_request_diff association Compared to the merge_request_diff association: 1. It's simpler to query. The query uses a foreign key to the merge_request_diffs table, so no ordering is necessary. 2. It's faster for preloading. The merge_request_diff association has to load every diff for the MRs in the set, then discard all but the most recent for each. This association means that Rails can just query for N diffs from N MRs. 3. It's more complicated to update. This is a bidirectional foreign key, so we need to update two tables when adding a diff record. This also means we need to handle this as a special case when importing a GitLab project. There is some juggling with this association in the merge request model: * `MergeRequest#latest_merge_request_diff` is _always_ the latest diff. * `MergeRequest#merge_request_diff` reuses `MergeRequest#latest_merge_request_diff` unless: * Arguments are passed. These are typically to force-reload the association. * It doesn't exist. That means we might be trying to implicitly create a diff. This only seems to happen in specs. * The association is already loaded. This is important for the reasons explained in the comment, which I'll reiterate here: if we a) load a non-latest diff, then b) get its `merge_request`, then c) get that MR's `merge_request_diff`, we should get the diff we loaded in c), even though that's not the latest diff. Basically, `MergeRequest#merge_request_diff` is the latest diff in most cases, but not quite all.
2017-11-15 12:22:18 -05:00
.preload(:notes, :author, :assignee, :milestone, :latest_merge_request_diff, :labels, :timelogs)
end
2018-01-10 12:05:34 -05:00
def merge_request_pipelines_with_access
authorize! :read_pipeline, user_project
mr = find_merge_request_with_access(params[:merge_request_iid])
mr.all_pipelines
end
def check_sha_param!(params, merge_request)
if params[:sha] && merge_request.diff_head_sha != params[:sha]
render_api_error!("SHA does not match HEAD of source branch: #{merge_request.diff_head_sha}", 409)
end
end
def serializer_options_for(merge_requests)
options = { with: Entities::MergeRequestBasic, current_user: current_user }
if params[:view] == 'simple'
options[:with] = Entities::MergeRequestSimple
else
options[:issuable_metadata] = issuable_meta_data(merge_requests, 'MergeRequest')
end
options
end
params :merge_requests_params do
2018-06-26 15:30:29 -04:00
optional :state, type: String, values: %w[opened closed locked merged all], default: 'all',
desc: 'Return opened, closed, locked, merged, or all merge requests'
optional :order_by, type: String, values: %w[created_at updated_at], default: 'created_at',
desc: 'Return merge requests ordered by `created_at` or `updated_at` fields.'
optional :sort, type: String, values: %w[asc desc], default: 'desc',
desc: 'Return merge requests sorted in `asc` or `desc` order.'
optional :milestone, type: String, desc: 'Return merge requests for a specific milestone'
optional :labels, type: String, desc: 'Comma-separated list of label names'
optional :created_after, type: DateTime, desc: 'Return merge requests created after the specified time'
optional :created_before, type: DateTime, desc: 'Return merge requests created before the specified time'
optional :updated_after, type: DateTime, desc: 'Return merge requests updated after the specified time'
optional :updated_before, type: DateTime, desc: 'Return merge requests updated before the specified time'
optional :view, type: String, values: %w[simple], desc: 'If simple, returns the `iid`, URL, title, description, and basic state of merge request'
optional :author_id, type: Integer, desc: 'Return merge requests which are authored by the user with the given ID'
optional :assignee_id, type: Integer, desc: 'Return merge requests which are assigned to the user with the given ID'
optional :scope, type: String, values: %w[created-by-me assigned-to-me created_by_me assigned_to_me all],
desc: 'Return merge requests for the given scope: `created_by_me`, `assigned_to_me` or `all`'
optional :my_reaction_emoji, type: String, desc: 'Return issues reacted by the authenticated user by the given emoji'
optional :source_branch, type: String, desc: 'Return merge requests with the given source branch'
optional :target_branch, type: String, desc: 'Return merge requests with the given target branch'
optional :search, type: String, desc: 'Search merge requests for text present in the title or description'
use :pagination
end
end
resource :merge_requests do
desc 'List merge requests' do
success Entities::MergeRequestBasic
end
params do
use :merge_requests_params
optional :scope, type: String, values: %w[created-by-me assigned-to-me created_by_me assigned_to_me all], default: 'created_by_me',
desc: 'Return merge requests for the given scope: `created_by_me`, `assigned_to_me` or `all`'
end
get do
authenticate! unless params[:scope] == 'all'
merge_requests = find_merge_requests
present merge_requests, serializer_options_for(merge_requests)
end
end
params do
requires :id, type: String, desc: 'The ID of a group'
end
resource :groups, requirements: API::PROJECT_ENDPOINT_REQUIREMENTS do
desc 'Get a list of group merge requests' do
success Entities::MergeRequestBasic
end
params do
use :merge_requests_params
end
get ":id/merge_requests" do
group = find_group!(params[:id])
merge_requests = find_merge_requests(group_id: group.id, include_subgroups: true)
present merge_requests, serializer_options_for(merge_requests)
end
end
2016-11-08 03:28:52 -05:00
params do
requires :id, type: String, desc: 'The ID of a project'
end
2017-08-31 07:44:49 -04:00
resource :projects, requirements: API::PROJECT_ENDPOINT_REQUIREMENTS do
include TimeTrackingEndpoints
helpers do
def handle_merge_request_errors!(errors)
if errors[:project_access].any?
error!(errors[:project_access], 422)
elsif errors[:branch_conflict].any?
error!(errors[:branch_conflict], 422)
elsif errors[:validate_fork].any?
error!(errors[:validate_fork], 422)
elsif errors[:validate_branches].any?
conflict!(errors[:validate_branches])
elsif errors[:base].any?
error!(errors[:base], 422)
end
render_api_error!(errors, 400)
end
2016-11-08 03:28:52 -05:00
params :optional_params do
2016-11-08 03:28:52 -05:00
optional :description, type: String, desc: 'The description of the merge request'
optional :assignee_id, type: Integer, desc: 'The ID of a user to assign the merge request'
optional :milestone_id, type: Integer, desc: 'The ID of a milestone to assign the merge request'
optional :labels, type: String, desc: 'Comma-separated list of label names'
2016-11-28 15:33:12 -05:00
optional :remove_source_branch, type: Boolean, desc: 'Remove source branch when merging'
optional :allow_collaboration, type: Boolean, desc: 'Allow commits from members who can merge to the target branch'
optional :allow_maintainer_to_push, type: Boolean, as: :allow_collaboration, desc: '[deprecated] See allow_collaboration'
2018-05-29 05:51:43 -04:00
optional :squash, type: Grape::API::Boolean, desc: 'When true, the commits will be squashed into a single commit on merge'
2017-04-05 12:31:15 -04:00
use :optional_params_ee
2017-04-05 12:31:15 -04:00
end
end
2016-11-08 03:28:52 -05:00
desc 'List merge requests' do
success Entities::MergeRequestBasic
2016-11-08 03:28:52 -05:00
end
params do
use :merge_requests_params
optional :iids, type: Array[Integer], desc: 'The IID array of merge requests'
2016-11-08 03:28:52 -05:00
end
2012-10-21 07:00:27 -04:00
get ":id/merge_requests" do
2012-10-21 09:13:39 -04:00
authorize! :read_merge_request, user_project
merge_requests = find_merge_requests(project_id: user_project.id)
2014-09-09 11:39:53 -04:00
options = serializer_options_for(merge_requests)
options[:project] = user_project
present merge_requests, options
2012-10-21 07:00:27 -04:00
end
2016-11-08 03:28:52 -05:00
desc 'Create a merge request' do
success Entities::MergeRequest
end
params do
requires :title, type: String, desc: 'The title of the merge request'
requires :source_branch, type: String, desc: 'The source branch'
requires :target_branch, type: String, desc: 'The target branch'
optional :target_project_id, type: Integer,
desc: 'The target project of the merge request defaults to the :id of the project'
use :optional_params
end
2012-10-21 07:00:27 -04:00
post ":id/merge_requests" do
Gitlab::QueryLimiting.whitelist('https://gitlab.com/gitlab-org/gitlab-ce/issues/42316')
authorize! :create_merge_request_from, user_project
2016-11-08 03:28:52 -05:00
2016-11-28 15:33:12 -05:00
mr_params = declared_params(include_missing: false)
mr_params[:force_remove_source_branch] = mr_params.delete(:remove_source_branch)
2016-11-08 03:28:52 -05:00
merge_request = ::MergeRequests::CreateService.new(user_project, current_user, mr_params).execute
if merge_request.valid?
present merge_request, with: Entities::MergeRequest, current_user: current_user, project: user_project
else
handle_merge_request_errors! merge_request.errors
2012-10-21 07:41:06 -04:00
end
2012-10-21 07:00:27 -04:00
end
2016-11-08 03:28:52 -05:00
desc 'Delete a merge request'
params do
requires :merge_request_iid, type: Integer, desc: 'The IID of a merge request'
2016-11-08 03:28:52 -05:00
end
delete ":id/merge_requests/:merge_request_iid" do
merge_request = find_project_merge_request(params[:merge_request_iid])
2016-03-18 14:11:25 -04:00
2016-03-21 09:12:52 -04:00
authorize!(:destroy_merge_request, merge_request)
destroy_conditionally!(merge_request) do |merge_request|
Issuable::DestroyService.new(user_project, current_user).execute(merge_request)
end
2016-02-26 03:55:43 -05:00
end
2016-11-08 03:28:52 -05:00
params do
requires :merge_request_iid, type: Integer, desc: 'The IID of a merge request'
optional :render_html, type: Boolean, desc: 'Returns the description and title rendered HTML'
2016-11-08 03:28:52 -05:00
end
desc 'Get a single merge request' do
success Entities::MergeRequest
end
get ':id/merge_requests/:merge_request_iid' do
merge_request = find_merge_request_with_access(params[:merge_request_iid])
present merge_request, with: Entities::MergeRequest, current_user: current_user, project: user_project, render_html: params[:render_html]
end
desc 'Get the participants of a merge request' do
success Entities::UserBasic
end
get ':id/merge_requests/:merge_request_iid/participants' do
merge_request = find_merge_request_with_access(params[:merge_request_iid])
participants = ::Kaminari.paginate_array(merge_request.participants)
present paginate(participants), with: Entities::UserBasic
end
desc 'Get the commits of a merge request' do
2017-10-05 04:48:05 -04:00
success Entities::Commit
end
get ':id/merge_requests/:merge_request_iid/commits' do
merge_request = find_merge_request_with_access(params[:merge_request_iid])
commits = ::Kaminari.paginate_array(merge_request.commits)
2017-10-05 04:48:05 -04:00
present paginate(commits), with: Entities::Commit
end
2015-11-18 05:17:41 -05:00
desc 'Show the merge request changes' do
success Entities::MergeRequestChanges
end
get ':id/merge_requests/:merge_request_iid/changes' do
merge_request = find_merge_request_with_access(params[:merge_request_iid])
2018-03-02 11:48:55 -05:00
present merge_request, with: Entities::MergeRequestChanges, current_user: current_user, project: user_project
end
desc 'Get the merge request pipelines' do
success Entities::PipelineBasic
end
get ':id/merge_requests/:merge_request_iid/pipelines' do
pipelines = merge_request_pipelines_with_access
present paginate(pipelines), with: Entities::PipelineBasic
end
desc 'Update a merge request' do
success Entities::MergeRequest
end
params do
optional :title, type: String, allow_blank: false, desc: 'The title of the merge request'
optional :target_branch, type: String, allow_blank: false, desc: 'The target branch'
optional :state_event, type: String, values: %w[close reopen],
desc: 'Status of the merge request'
2017-08-31 06:38:32 -04:00
optional :discussion_locked, type: Boolean, desc: 'Whether the MR discussion is locked'
2017-04-05 12:31:15 -04:00
use :optional_params
at_least_one_of(*::API::MergeRequests.update_params_at_least_one_of)
end
put ':id/merge_requests/:merge_request_iid' do
Gitlab::QueryLimiting.whitelist('https://gitlab.com/gitlab-org/gitlab-ce/issues/42318')
merge_request = find_merge_request_with_access(params.delete(:merge_request_iid), :update_merge_request)
mr_params = declared_params(include_missing: false)
mr_params[:force_remove_source_branch] = mr_params.delete(:remove_source_branch) if mr_params[:remove_source_branch].present?
2015-11-18 05:17:41 -05:00
merge_request = ::MergeRequests::UpdateService.new(user_project, current_user, mr_params).execute(merge_request)
if merge_request.valid?
present merge_request, with: Entities::MergeRequest, current_user: current_user, project: user_project
else
handle_merge_request_errors! merge_request.errors
end
end
desc 'Merge a merge request' do
success Entities::MergeRequest
end
params do
optional :merge_commit_message, type: String, desc: 'Custom merge commit message'
optional :should_remove_source_branch, type: Boolean,
desc: 'When true, the source branch will be deleted if possible'
optional :merge_when_pipeline_succeeds, type: Boolean,
desc: 'When true, this merge request will be merged when the pipeline succeeds'
optional :sha, type: String, desc: 'When present, must have the HEAD SHA of the source branch'
2018-05-29 05:51:43 -04:00
optional :squash, type: Grape::API::Boolean, desc: 'When true, the commits will be squashed into a single commit on merge'
end
put ':id/merge_requests/:merge_request_iid/merge' do
Gitlab::QueryLimiting.whitelist('https://gitlab.com/gitlab-org/gitlab-ce/issues/42317')
merge_request = find_project_merge_request(params[:merge_request_iid])
merge_when_pipeline_succeeds = to_boolean(params[:merge_when_pipeline_succeeds])
# Merge request can not be merged
# because user dont have permissions to push into target branch
unauthorized! unless merge_request.can_be_merged_by?(current_user)
not_allowed! unless merge_request.mergeable_state?(skip_ci_check: merge_when_pipeline_succeeds)
render_api_error!('Branch cannot be merged', 406) unless merge_request.mergeable?(skip_ci_check: merge_when_pipeline_succeeds)
check_sha_param!(params, merge_request)
2018-05-29 05:51:43 -04:00
merge_request.update(squash: params[:squash]) if params[:squash]
merge_params = {
commit_message: params[:merge_commit_message],
should_remove_source_branch: params[:should_remove_source_branch]
}
if merge_when_pipeline_succeeds && merge_request.head_pipeline && merge_request.head_pipeline.active?
::MergeRequests::MergeWhenPipelineSucceedsService
.new(merge_request.target_project, current_user, merge_params)
.execute(merge_request)
else
::MergeRequests::MergeService
.new(merge_request.target_project, current_user, merge_params)
.execute(merge_request)
end
present merge_request, with: Entities::MergeRequest, current_user: current_user, project: user_project
end
desc 'Cancel merge if "Merge When Pipeline Succeeds" is enabled' do
success Entities::MergeRequest
end
post ':id/merge_requests/:merge_request_iid/cancel_merge_when_pipeline_succeeds' do
merge_request = find_project_merge_request(params[:merge_request_iid])
unauthorized! unless merge_request.can_cancel_merge_when_pipeline_succeeds?(current_user)
::MergeRequests::MergeWhenPipelineSucceedsService
.new(merge_request.target_project, current_user)
.cancel(merge_request)
end
2012-10-25 06:38:55 -04:00
desc 'List issues that will be closed on merge' do
success Entities::MRNote
end
params do
use :pagination
end
get ':id/merge_requests/:merge_request_iid/closes_issues' do
merge_request = find_merge_request_with_access(params[:merge_request_iid])
issues = ::Kaminari.paginate_array(merge_request.closes_issues(current_user))
issues = paginate(issues)
external_issues, internal_issues = issues.partition { |issue| issue.is_a?(ExternalIssue) }
data = Entities::IssueBasic.represent(internal_issues, current_user: current_user)
data += Entities::ExternalIssue.represent(external_issues, current_user: current_user)
data.as_json
end
2012-10-21 07:00:27 -04:00
end
end
end