Commit cdac9ed8 authored by Douwe Maan's avatar Douwe Maan

Merge branch 'id-mr-widget-etag-caching' into 'master'

Split MR widget into etag-cached and non-cached serializers

See merge request gitlab-org/gitlab-ce!31354
parents 43b9be9d b99011af
...@@ -166,6 +166,7 @@ export default { ...@@ -166,6 +166,7 @@ export default {
ciEnvironmentsStatusPath: store.ciEnvironmentsStatusPath, ciEnvironmentsStatusPath: store.ciEnvironmentsStatusPath,
mergeRequestBasicPath: store.mergeRequestBasicPath, mergeRequestBasicPath: store.mergeRequestBasicPath,
mergeRequestWidgetPath: store.mergeRequestWidgetPath, mergeRequestWidgetPath: store.mergeRequestWidgetPath,
mergeRequestCachedWidgetPath: store.mergeRequestCachedWidgetPath,
mergeActionsContentPath: store.mergeActionsContentPath, mergeActionsContentPath: store.mergeActionsContentPath,
rebasePath: store.rebasePath, rebasePath: store.rebasePath,
}; };
...@@ -176,8 +177,7 @@ export default { ...@@ -176,8 +177,7 @@ export default {
checkStatus(cb, isRebased) { checkStatus(cb, isRebased) {
return this.service return this.service
.checkStatus() .checkStatus()
.then(res => res.data) .then(({ data }) => {
.then(data => {
this.handleNotification(data); this.handleNotification(data);
this.mr.setData(data, isRebased); this.mr.setData(data, isRebased);
this.setFaviconHelper(); this.setFaviconHelper();
......
...@@ -34,7 +34,16 @@ export default class MRWidgetService { ...@@ -34,7 +34,16 @@ export default class MRWidgetService {
} }
checkStatus() { checkStatus() {
return axios.get(this.endpoints.mergeRequestWidgetPath); // two endpoints are requested in order to get MR info:
// one which is etag-cached and invalidated and another one which is not cached
// the idea is to move all the fields to etag-cached endpoint and then perform only one request
// https://gitlab.com/gitlab-org/gitlab-ce/issues/61559#note_188801390
const getData = axios.get(this.endpoints.mergeRequestWidgetPath);
const getCachedData = axios.get(this.endpoints.mergeRequestCachedWidgetPath);
return axios
.all([getData, getCachedData])
.then(axios.spread((res, cachedRes) => ({ data: Object.assign(res.data, cachedRes.data) })));
} }
fetchMergeActionsContent() { fetchMergeActionsContent() {
......
...@@ -10,6 +10,8 @@ export default class MergeRequestStore { ...@@ -10,6 +10,8 @@ export default class MergeRequestStore {
this.sha = data.diff_head_sha; this.sha = data.diff_head_sha;
this.gitlabLogo = data.gitlabLogo; this.gitlabLogo = data.gitlabLogo;
this.setPaths(data);
this.setData(data); this.setData(data);
} }
...@@ -18,13 +20,9 @@ export default class MergeRequestStore { ...@@ -18,13 +20,9 @@ export default class MergeRequestStore {
this.sha = data.diff_head_sha; this.sha = data.diff_head_sha;
} }
const currentUser = data.current_user;
const pipelineStatus = data.pipeline ? data.pipeline.details.status : null; const pipelineStatus = data.pipeline ? data.pipeline.details.status : null;
this.squash = data.squash; this.squash = data.squash;
this.squashBeforeMergeHelpPath =
this.squashBeforeMergeHelpPath || data.squash_before_merge_help_path;
this.troubleshootingDocsPath = this.troubleshootingDocsPath || data.troubleshooting_docs_path;
this.enableSquashBeforeMerge = this.enableSquashBeforeMerge || true; this.enableSquashBeforeMerge = this.enableSquashBeforeMerge || true;
this.iid = data.iid; this.iid = data.iid;
...@@ -33,8 +31,6 @@ export default class MergeRequestStore { ...@@ -33,8 +31,6 @@ export default class MergeRequestStore {
this.targetBranchSha = data.target_branch_sha; this.targetBranchSha = data.target_branch_sha;
this.sourceBranch = data.source_branch; this.sourceBranch = data.source_branch;
this.sourceBranchProtected = data.source_branch_protected; this.sourceBranchProtected = data.source_branch_protected;
this.conflictsDocsPath = data.conflicts_docs_path;
this.mergeRequestPipelinesHelpPath = data.merge_request_pipelines_docs_path;
this.mergeStatus = data.merge_status; this.mergeStatus = data.merge_status;
this.commitMessage = data.default_merge_commit_message; this.commitMessage = data.default_merge_commit_message;
this.shortMergeCommitSha = data.short_merge_commit_sha; this.shortMergeCommitSha = data.short_merge_commit_sha;
...@@ -48,7 +44,7 @@ export default class MergeRequestStore { ...@@ -48,7 +44,7 @@ export default class MergeRequestStore {
this.postMergeDeployments = this.postMergeDeployments || []; this.postMergeDeployments = this.postMergeDeployments || [];
this.commits = data.commits_without_merge_commits || []; this.commits = data.commits_without_merge_commits || [];
this.squashCommitMessage = data.default_squash_commit_message; this.squashCommitMessage = data.default_squash_commit_message;
this.initRebase(data); this.rebaseInProgress = data.rebase_in_progress;
if (data.issues_links) { if (data.issues_links) {
const links = data.issues_links; const links = data.issues_links;
...@@ -66,14 +62,7 @@ export default class MergeRequestStore { ...@@ -66,14 +62,7 @@ export default class MergeRequestStore {
this.setToAutoMergeBy = MergeRequestStore.formatUserObject(data.merge_user || {}); this.setToAutoMergeBy = MergeRequestStore.formatUserObject(data.merge_user || {});
this.mergeUserId = data.merge_user_id; this.mergeUserId = data.merge_user_id;
this.currentUserId = gon.current_user_id; this.currentUserId = gon.current_user_id;
this.sourceBranchPath = data.source_branch_path;
this.sourceBranchLink = data.source_branch_with_namespace_link;
this.mergeError = data.merge_error; this.mergeError = data.merge_error;
this.targetBranchPath = data.target_branch_commits_path;
this.targetBranchTreePath = data.target_branch_tree_path;
this.conflictResolutionPath = data.conflict_resolution_path;
this.cancelAutoMergePath = data.cancel_auto_merge_path;
this.removeWIPPath = data.remove_wip_path;
this.sourceBranchRemoved = !data.source_branch_exists; this.sourceBranchRemoved = !data.source_branch_exists;
this.shouldRemoveSourceBranch = data.remove_source_branch || false; this.shouldRemoveSourceBranch = data.remove_source_branch || false;
this.onlyAllowMergeIfPipelineSucceeds = data.only_allow_merge_if_pipeline_succeeds || false; this.onlyAllowMergeIfPipelineSucceeds = data.only_allow_merge_if_pipeline_succeeds || false;
...@@ -83,46 +72,23 @@ export default class MergeRequestStore { ...@@ -83,46 +72,23 @@ export default class MergeRequestStore {
this.preferredAutoMergeStrategy = MergeRequestStore.getPreferredAutoMergeStrategy( this.preferredAutoMergeStrategy = MergeRequestStore.getPreferredAutoMergeStrategy(
this.availableAutoMergeStrategies, this.availableAutoMergeStrategies,
); );
this.mergePath = data.merge_path;
this.ffOnlyEnabled = data.ff_only_enabled; this.ffOnlyEnabled = data.ff_only_enabled;
this.shouldBeRebased = Boolean(data.should_be_rebased); this.shouldBeRebased = Boolean(data.should_be_rebased);
this.mergeRequestBasicPath = data.merge_request_basic_path;
this.mergeRequestWidgetPath = data.merge_request_widget_path;
this.emailPatchesPath = data.email_patches_path;
this.plainDiffPath = data.plain_diff_path;
this.newBlobPath = data.new_blob_path;
this.createIssueToResolveDiscussionsPath = data.create_issue_to_resolve_discussions_path;
this.mergeCheckPath = data.merge_check_path;
this.mergeActionsContentPath = data.commit_change_content_path;
this.mergeCommitPath = data.merge_commit_path;
this.isRemovingSourceBranch = this.isRemovingSourceBranch || false; this.isRemovingSourceBranch = this.isRemovingSourceBranch || false;
this.isOpen = data.state === 'opened'; this.isOpen = data.state === 'opened';
this.hasMergeableDiscussionsState = data.mergeable_discussions_state === false; this.hasMergeableDiscussionsState = data.mergeable_discussions_state === false;
this.canRemoveSourceBranch = currentUser.can_remove_source_branch || false;
this.canMerge = Boolean(data.merge_path);
this.canCreateIssue = currentUser.can_create_issue || false;
this.canCancelAutomaticMerge = Boolean(data.cancel_auto_merge_path);
this.isSHAMismatch = this.sha !== data.diff_head_sha; this.isSHAMismatch = this.sha !== data.diff_head_sha;
this.canBeMerged = data.can_be_merged || false; this.canBeMerged = data.can_be_merged || false;
this.isMergeAllowed = data.mergeable || false; this.isMergeAllowed = data.mergeable || false;
this.mergeOngoing = data.merge_ongoing; this.mergeOngoing = data.merge_ongoing;
this.allowCollaboration = data.allow_collaboration; this.allowCollaboration = data.allow_collaboration;
this.targetProjectFullPath = data.target_project_full_path;
this.sourceProjectFullPath = data.source_project_full_path;
this.sourceProjectId = data.source_project_id; this.sourceProjectId = data.source_project_id;
this.targetProjectId = data.target_project_id; this.targetProjectId = data.target_project_id;
this.mergePipelinesEnabled = Boolean(data.merge_pipelines_enabled); this.mergePipelinesEnabled = Boolean(data.merge_pipelines_enabled);
this.mergeTrainsCount = data.merge_trains_count || 0; this.mergeTrainsCount = data.merge_trains_count || 0;
this.mergeTrainIndex = data.merge_train_index; this.mergeTrainIndex = data.merge_train_index;
// Cherry-pick and Revert actions related
this.canCherryPickInCurrentMR = currentUser.can_cherry_pick_on_current_merge_request || false;
this.canRevertInCurrentMR = currentUser.can_revert_on_current_merge_request || false;
this.cherryPickInForkPath = currentUser.cherry_pick_in_fork_path;
this.revertInForkPath = currentUser.revert_in_fork_path;
// CI related // CI related
this.ciEnvironmentsStatusPath = data.ci_environments_status_path;
this.hasCI = data.has_ci; this.hasCI = data.has_ci;
this.ciStatus = data.ci_status; this.ciStatus = data.ci_status;
this.isPipelineFailed = this.ciStatus === 'failed' || this.ciStatus === 'canceled'; this.isPipelineFailed = this.ciStatus === 'failed' || this.ciStatus === 'canceled';
...@@ -133,8 +99,33 @@ export default class MergeRequestStore { ...@@ -133,8 +99,33 @@ export default class MergeRequestStore {
this.isPipelineActive = data.pipeline ? data.pipeline.active : false; this.isPipelineActive = data.pipeline ? data.pipeline.active : false;
this.isPipelineBlocked = pipelineStatus ? pipelineStatus.group === 'manual' : false; this.isPipelineBlocked = pipelineStatus ? pipelineStatus.group === 'manual' : false;
this.ciStatusFaviconPath = pipelineStatus ? pipelineStatus.favicon : null; this.ciStatusFaviconPath = pipelineStatus ? pipelineStatus.favicon : null;
this.testResultsPath = data.test_reports_path; this.testResultsPath = data.test_reports_path;
this.cancelAutoMergePath = data.cancel_auto_merge_path;
this.canCancelAutomaticMerge = Boolean(data.cancel_auto_merge_path);
this.newBlobPath = data.new_blob_path;
this.sourceBranchPath = data.source_branch_path;
this.sourceBranchLink = data.source_branch_with_namespace_link;
this.rebasePath = data.rebase_path;
this.targetBranchPath = data.target_branch_commits_path;
this.targetBranchTreePath = data.target_branch_tree_path;
this.conflictResolutionPath = data.conflict_resolution_path;
this.removeWIPPath = data.remove_wip_path;
this.createIssueToResolveDiscussionsPath = data.create_issue_to_resolve_discussions_path;
this.mergePath = data.merge_path;
this.canMerge = Boolean(data.merge_path);
this.mergeCommitPath = data.merge_commit_path;
this.canPushToSourceBranch = data.can_push_to_source_branch;
const currentUser = data.current_user;
this.cherryPickInForkPath = currentUser.cherry_pick_in_fork_path;
this.revertInForkPath = currentUser.revert_in_fork_path;
this.canRemoveSourceBranch = currentUser.can_remove_source_branch || false;
this.canCreateIssue = currentUser.can_create_issue || false;
this.canCherryPickInCurrentMR = currentUser.can_cherry_pick_on_current_merge_request || false;
this.canRevertInCurrentMR = currentUser.can_revert_on_current_merge_request || false;
this.setState(data); this.setState(data);
} }
...@@ -161,6 +152,24 @@ export default class MergeRequestStore { ...@@ -161,6 +152,24 @@ export default class MergeRequestStore {
} }
} }
setPaths(data) {
// Paths are set on the first load of the page and not auto-refreshed
this.squashBeforeMergeHelpPath = data.squash_before_merge_help_path;
this.troubleshootingDocsPath = data.troubleshooting_docs_path;
this.mergeRequestBasicPath = data.merge_request_basic_path;
this.mergeRequestWidgetPath = data.merge_request_widget_path;
this.mergeRequestCachedWidgetPath = data.merge_request_cached_widget_path;
this.emailPatchesPath = data.email_patches_path;
this.plainDiffPath = data.plain_diff_path;
this.mergeCheckPath = data.merge_check_path;
this.mergeActionsContentPath = data.commit_change_content_path;
this.targetProjectFullPath = data.target_project_full_path;
this.sourceProjectFullPath = data.source_project_full_path;
this.mergeRequestPipelinesHelpPath = data.merge_request_pipelines_docs_path;
this.conflictsDocsPath = data.conflicts_docs_path;
this.ciEnvironmentsStatusPath = data.ci_environments_status_path;
}
get isNothingToMergeState() { get isNothingToMergeState() {
return this.state === stateKey.nothingToMerge; return this.state === stateKey.nothingToMerge;
} }
...@@ -169,13 +178,6 @@ export default class MergeRequestStore { ...@@ -169,13 +178,6 @@ export default class MergeRequestStore {
return this.state === stateKey.merged; return this.state === stateKey.merged;
} }
initRebase(data) {
this.canPushToSourceBranch = data.can_push_to_source_branch;
this.rebaseInProgress = data.rebase_in_progress;
this.approvalsLeft = !data.approved;
this.rebasePath = data.rebase_path;
}
static buildMetrics(metrics) { static buildMetrics(metrics) {
if (!metrics) { if (!metrics) {
return {}; return {};
......
...@@ -7,16 +7,33 @@ class Projects::MergeRequests::ContentController < Projects::MergeRequests::Appl ...@@ -7,16 +7,33 @@ class Projects::MergeRequests::ContentController < Projects::MergeRequests::Appl
# for other types of serialization # for other types of serialization
before_action :close_merge_request_if_no_source_project before_action :close_merge_request_if_no_source_project
before_action :set_polling_header
around_action :allow_gitaly_ref_name_caching around_action :allow_gitaly_ref_name_caching
def widget def widget
respond_to do |format| respond_to do |format|
format.json do format.json do
Gitlab::PollingInterval.set_header(response, interval: 10_000) render json: serializer(MergeRequestPollWidgetEntity)
end
end
end
serializer = MergeRequestSerializer.new(current_user: current_user, project: merge_request.project) def cached_widget
render json: serializer.represent(merge_request, serializer: 'widget') respond_to do |format|
format.json do
render json: serializer(MergeRequestPollCachedWidgetEntity)
end end
end end
end end
private
def set_polling_header
Gitlab::PollingInterval.set_header(response, interval: 10_000)
end
def serializer(entity)
serializer = MergeRequestSerializer.new(current_user: current_user, project: merge_request.project)
serializer.represent(merge_request, {}, entity)
end
end end
...@@ -64,7 +64,7 @@ class Issue < ApplicationRecord ...@@ -64,7 +64,7 @@ class Issue < ApplicationRecord
scope :public_only, -> { where(confidential: false) } scope :public_only, -> { where(confidential: false) }
scope :confidential_only, -> { where(confidential: true) } scope :confidential_only, -> { where(confidential: true) }
after_save :expire_etag_cache after_commit :expire_etag_cache
after_save :ensure_metrics, unless: :imported? after_save :ensure_metrics, unless: :imported?
attr_spammable :title, spam_title: true attr_spammable :title, spam_title: true
......
...@@ -73,6 +73,7 @@ class MergeRequest < ApplicationRecord ...@@ -73,6 +73,7 @@ class MergeRequest < ApplicationRecord
after_update :clear_memoized_shas after_update :clear_memoized_shas
after_update :reload_diff_if_branch_changed after_update :reload_diff_if_branch_changed
after_save :ensure_metrics after_save :ensure_metrics
after_commit :expire_etag_cache
# When this attribute is true some MR validation is ignored # When this attribute is true some MR validation is ignored
# It allows us to close or modify broken merge requests # It allows us to close or modify broken merge requests
...@@ -389,6 +390,10 @@ class MergeRequest < ApplicationRecord ...@@ -389,6 +390,10 @@ class MergeRequest < ApplicationRecord
def merge_async(user_id, params) def merge_async(user_id, params)
jid = MergeWorker.perform_async(id, user_id, params.to_h) jid = MergeWorker.perform_async(id, user_id, params.to_h)
update_column(:merge_jid, jid) update_column(:merge_jid, jid)
# merge_ongoing? depends on merge_jid
# expire etag cache since the attribute is changed without triggering callbacks
expire_etag_cache
end end
# Set off a rebase asynchronously, atomically updating the `rebase_jid` of # Set off a rebase asynchronously, atomically updating the `rebase_jid` of
...@@ -409,6 +414,10 @@ class MergeRequest < ApplicationRecord ...@@ -409,6 +414,10 @@ class MergeRequest < ApplicationRecord
update_column(:rebase_jid, jid) update_column(:rebase_jid, jid)
end end
# rebase_in_progress? depends on rebase_jid
# expire etag cache since the attribute is changed without triggering callbacks
expire_etag_cache
end end
def merge_participants def merge_participants
...@@ -1429,4 +1438,11 @@ class MergeRequest < ApplicationRecord ...@@ -1429,4 +1438,11 @@ class MergeRequest < ApplicationRecord
variables.append(key: 'CI_MERGE_REQUEST_SOURCE_BRANCH_NAME', value: source_branch.to_s) variables.append(key: 'CI_MERGE_REQUEST_SOURCE_BRANCH_NAME', value: source_branch.to_s)
end end
end end
def expire_etag_cache
return unless project.namespace
key = Gitlab::Routing.url_helpers.cached_widget_project_json_merge_request_path(project, self, format: :json)
Gitlab::EtagCaching::Store.new.touch(key)
end
end end
...@@ -208,14 +208,6 @@ class MergeRequestPresenter < Gitlab::View::Presenter::Delegated ...@@ -208,14 +208,6 @@ class MergeRequestPresenter < Gitlab::View::Presenter::Delegated
merge_request.subscribed?(current_user, merge_request.target_project) merge_request.subscribed?(current_user, merge_request.target_project)
end end
def conflicts_docs_path
help_page_path('user/project/merge_requests/resolve_conflicts.md')
end
def merge_request_pipelines_docs_path
help_page_path('ci/merge_request_pipelines/index.md')
end
def source_branch_link def source_branch_link
if source_branch_exists? if source_branch_exists?
link_to(source_branch, source_branch_commits_path, class: 'ref-name') link_to(source_branch, source_branch_commits_path, class: 'ref-name')
......
# frozen_string_literal: true
class MergeRequestPollCachedWidgetEntity < IssuableEntity
expose :auto_merge_enabled
expose :state
expose :merge_commit_sha
expose :short_merge_commit_sha
expose :merge_error
expose :merge_status
expose :merge_user_id
expose :source_branch
expose :source_project_id
expose :target_branch
expose :target_branch_sha
expose :target_project_id
expose :squash
expose :rebase_in_progress?, as: :rebase_in_progress
expose :default_squash_commit_message
expose :commits_count
expose :merge_ongoing?, as: :merge_ongoing
expose :work_in_progress?, as: :work_in_progress
expose :cannot_be_merged?, as: :has_conflicts
expose :can_be_merged?, as: :can_be_merged
expose :remove_source_branch?, as: :remove_source_branch
expose :source_branch_exists?, as: :source_branch_exists
expose :branch_missing?, as: :branch_missing
expose :commits_without_merge_commits, using: MergeRequestWidgetCommitEntity do |merge_request|
merge_request.commits.without_merge_commits
end
expose :diff_head_sha do |merge_request|
merge_request.diff_head_sha.presence
end
expose :metrics do |merge_request|
metrics = build_metrics(merge_request)
MergeRequestMetricsEntity.new(metrics).as_json
end
expose :diverged_commits_count do |merge_request|
if merge_request.open? && merge_request.diverged_from_target_branch?
merge_request.diverged_commits_count
else
0
end
end
# Paths
#
expose :target_branch_commits_path do |merge_request|
presenter(merge_request).target_branch_commits_path
end
expose :target_branch_tree_path do |merge_request|
presenter(merge_request).target_branch_tree_path
end
expose :merge_commit_path do |merge_request|
if merge_request.merge_commit_sha
project_commit_path(merge_request.project, merge_request.merge_commit_sha)
end
end
expose :source_branch_path do |merge_request|
presenter(merge_request).source_branch_path
end
expose :source_branch_with_namespace_link do |merge_request|
presenter(merge_request).source_branch_with_namespace_link
end
private
delegate :current_user, to: :request
def presenter(merge_request)
@presenters ||= {}
@presenters[merge_request] ||= MergeRequestPresenter.new(merge_request, current_user: current_user) # rubocop: disable CodeReuse/Presenter
end
# Once SchedulePopulateMergeRequestMetricsWithEventsData fully runs,
# we can remove this method and just serialize MergeRequest#metrics
# instead. See https://gitlab.com/gitlab-org/gitlab-ce/issues/41587
def build_metrics(merge_request)
# There's no need to query and serialize metrics data for merge requests that are not
# merged or closed.
return unless merge_request.merged? || merge_request.closed?
return merge_request.metrics if merge_request.merged? && merge_request.metrics&.merged_by_id
return merge_request.metrics if merge_request.closed? && merge_request.metrics&.latest_closed_by_id
build_metrics_from_events(merge_request)
end
def build_metrics_from_events(merge_request)
closed_event = merge_request.closed_event
merge_event = merge_request.merge_event
MergeRequest::Metrics.new(latest_closed_at: closed_event&.updated_at,
latest_closed_by: closed_event&.author,
merged_at: merge_event&.updated_at,
merged_by: merge_event&.author)
end
end
# frozen_string_literal: true
class MergeRequestPollWidgetEntity < IssuableEntity
expose :auto_merge_strategy
expose :available_auto_merge_strategies do |merge_request|
AutoMergeService.new(merge_request.project, current_user).available_strategies(merge_request) # rubocop: disable CodeReuse/ServiceClass
end
expose :source_branch_protected do |merge_request|
merge_request.source_project.present? && ProtectedBranch.protected?(merge_request.source_project, merge_request.source_branch)
end
expose :allow_collaboration
expose :should_be_rebased?, as: :should_be_rebased
expose :ff_only_enabled do |merge_request|
merge_request.project.merge_requests_ff_only_enabled
end
# User entities
expose :merge_user, using: UserEntity
expose :actual_head_pipeline, with: PipelineDetailsEntity, as: :pipeline, if: -> (mr, _) { presenter(mr).can_read_pipeline? }
expose :merge_pipeline, with: PipelineDetailsEntity, if: ->(mr, _) { mr.merged? && can?(request.current_user, :read_pipeline, mr.target_project)}
expose :default_merge_commit_message
expose :mergeable?, as: :mergeable
expose :default_merge_commit_message_with_description do |merge_request|
merge_request.default_merge_commit_message(include_description: true)
end
# Booleans
expose :mergeable_discussions_state?, as: :mergeable_discussions_state do |merge_request|
# This avoids calling MergeRequest#mergeable_discussions_state without
# considering the state of the MR first. If a MR isn't mergeable, we can
# safely short-circuit it.
if merge_request.mergeable_state?(skip_ci_check: true, skip_discussions_check: true)
merge_request.mergeable_discussions_state?
else
false
end
end
expose :project_archived do |merge_request|
merge_request.project.archived?
end
expose :only_allow_merge_if_pipeline_succeeds do |merge_request|
merge_request.project.only_allow_merge_if_pipeline_succeeds?
end
# CI related
expose :has_ci?, as: :has_ci
expose :ci_status do |merge_request|
presenter(merge_request).ci_status
end
expose :cancel_auto_merge_path do |merge_request|
presenter(merge_request).cancel_auto_merge_path
end
expose :test_reports_path do |merge_request|
if merge_request.has_test_reports?
test_reports_project_merge_request_path(merge_request.project, merge_request, format: :json)
end
end
expose :supports_suggestion?, as: :can_receive_suggestion
expose :create_issue_to_resolve_discussions_path do |merge_request|
presenter(merge_request).create_issue_to_resolve_discussions_path
end
expose :current_user do
expose :can_remove_source_branch do |merge_request|
presenter(merge_request).can_remove_source_branch?
end
expose :can_revert_on_current_merge_request do |merge_request|
presenter(merge_request).can_revert_on_current_merge_request?
end
expose :can_cherry_pick_on_current_merge_request do |merge_request|
presenter(merge_request).can_cherry_pick_on_current_merge_request?
end
expose :can_create_note do |merge_request|
can?(current_user, :create_note, merge_request)
end
expose :can_create_issue do |merge_request|
can?(current_user, :create_issue, merge_request.project)
end
expose :can_update do |merge_request|
can?(current_user, :update_merge_request, merge_request)
end
end
expose :can_push_to_source_branch do |merge_request|
presenter(merge_request).can_push_to_source_branch?
end
expose :new_blob_path do |merge_request|
if presenter(merge_request).can_push_to_source_branch?
project_new_blob_path(merge_request.source_project, merge_request.source_branch)
end
end
expose :rebase_path do |merge_request|
presenter(merge_request).rebase_path
end
expose :conflict_resolution_path do |merge_request|
presenter(merge_request).conflict_resolution_path
end
expose :remove_wip_path do |merge_request|
presenter(merge_request).remove_wip_path
end
expose :merge_path do |merge_request|
presenter(merge_request).merge_path
end
expose :cherry_pick_in_fork_path do |merge_request|
presenter(merge_request).cherry_pick_in_fork_path
end
expose :revert_in_fork_path do |merge_request|
presenter(merge_request).revert_in_fork_path
end
private
delegate :current_user, to: :request
def presenter(merge_request)
@presenters ||= {}
@presenters[merge_request] ||= MergeRequestPresenter.new(merge_request, current_user: current_user) # rubocop: disable CodeReuse/Presenter
end
end
...@@ -4,8 +4,8 @@ class MergeRequestSerializer < BaseSerializer ...@@ -4,8 +4,8 @@ class MergeRequestSerializer < BaseSerializer
# This overrided method takes care of which entity should be used # This overrided method takes care of which entity should be used
# to serialize the `merge_request` based on `serializer` key in `opts` param. # to serialize the `merge_request` based on `serializer` key in `opts` param.
# Hence, `entity` doesn't need to be declared on the class scope. # Hence, `entity` doesn't need to be declared on the class scope.
def represent(merge_request, opts = {}) def represent(merge_request, opts = {}, entity = nil)
entity = entity ||=
case opts[:serializer] case opts[:serializer]
when 'sidebar' when 'sidebar'
IssuableSidebarBasicEntity IssuableSidebarBasicEntity
......
# frozen_string_literal: true # frozen_string_literal: true
class MergeRequestWidgetEntity < IssuableEntity class MergeRequestWidgetEntity < Grape::Entity
expose :state include RequestAwareEntity
expose :in_progress_merge_commit_sha
expose :merge_commit_sha # Currently this attr is exposed to be used in app/assets/javascripts/notes/stores/getters.js
expose :short_merge_commit_sha # in order to determine whether a noteable is an issue or an MR
expose :merge_error
expose :merge_params expose :merge_params
expose :merge_status
expose :merge_user_id
expose :auto_merge_enabled
expose :auto_merge_strategy
expose :available_auto_merge_strategies do |merge_request|
AutoMergeService.new(merge_request.project, current_user).available_strategies(merge_request) # rubocop: disable CodeReuse/ServiceClass
end
expose :source_branch
expose :source_branch_protected do |merge_request|
merge_request.source_project.present? && ProtectedBranch.protected?(merge_request.source_project, merge_request.source_branch)
end
expose :source_project_id
expose :source_project_full_path do |merge_request| expose :source_project_full_path do |merge_request|
merge_request.source_project&.full_path merge_request.source_project&.full_path
end end
expose :squash
expose :target_branch
expose :target_branch_sha
expose :target_project_id
expose :target_project_full_path do |merge_request| expose :target_project_full_path do |merge_request|
merge_request.project&.full_path merge_request.project&.full_path
end end
expose :allow_collaboration
expose :should_be_rebased?, as: :should_be_rebased
expose :ff_only_enabled do |merge_request|
merge_request.project.merge_requests_ff_only_enabled
end
expose :metrics do |merge_request|
metrics = build_metrics(merge_request)
MergeRequestMetricsEntity.new(metrics).as_json
end
expose :rebase_commit_sha
expose :rebase_in_progress?, as: :rebase_in_progress
expose :can_push_to_source_branch do |merge_request|
presenter(merge_request).can_push_to_source_branch?
end
expose :rebase_path do |merge_request|
presenter(merge_request).rebase_path
end
# User entities
expose :merge_user, using: UserEntity
# Diff sha's
expose :diff_head_sha do |merge_request|
merge_request.diff_head_sha.presence
end
expose :actual_head_pipeline, with: PipelineDetailsEntity, as: :pipeline, if: -> (mr, _) { presenter(mr).can_read_pipeline? }
expose :merge_pipeline, with: PipelineDetailsEntity, if: ->(mr, _) { mr.merged? && can?(request.current_user, :read_pipeline, mr.target_project)}
expose :default_squash_commit_message
expose :default_merge_commit_message
expose :default_merge_commit_message_with_description do |merge_request|
merge_request.default_merge_commit_message(include_description: true)
end
expose :commits_without_merge_commits, using: MergeRequestWidgetCommitEntity do |merge_request|
merge_request.commits.without_merge_commits
end
expose :commits_count
# Booleans
expose :merge_ongoing?, as: :merge_ongoing
expose :work_in_progress?, as: :work_in_progress
expose :source_branch_exists?, as: :source_branch_exists
expose :mergeable_discussions_state?, as: :mergeable_discussions_state do |merge_request|
# This avoids calling MergeRequest#mergeable_discussions_state without
# considering the state of the MR first. If a MR isn't mergeable, we can
# safely short-circuit it.
if merge_request.mergeable_state?(skip_ci_check: true, skip_discussions_check: true)
merge_request.mergeable_discussions_state?
else
false
end
end
expose :branch_missing?, as: :branch_missing
expose :cannot_be_merged?, as: :has_conflicts
expose :can_be_merged?, as: :can_be_merged
expose :mergeable?, as: :mergeable
expose :remove_source_branch?, as: :remove_source_branch
expose :project_archived do |merge_request|
merge_request.project.archived?
end
expose :only_allow_merge_if_pipeline_succeeds do |merge_request|
merge_request.project.only_allow_merge_if_pipeline_succeeds?
end
# CI related
expose :has_ci?, as: :has_ci
expose :ci_status do |merge_request|
presenter(merge_request).ci_status
end
# Rendering and redacting Markdown can be expensive. These links are
# just nice to have in the merge request widget, so only
# include them if they are explicitly requested on first load.
expose :issues_links, if: -> (_, opts) { opts[:issues_links] } do
expose :assign_to_closing do |merge_request|
presenter(merge_request).assign_to_closing_issues_link
end
expose :closing do |merge_request|
presenter(merge_request).closing_issues_links
end
expose :mentioned_but_not_closing do |merge_request|
presenter(merge_request).mentioned_issues_links
end
end
expose :source_branch_with_namespace_link do |merge_request|
presenter(merge_request).source_branch_with_namespace_link
end
expose :source_branch_path do |merge_request|
presenter(merge_request).source_branch_path
end
expose :current_user do
expose :can_remove_source_branch do |merge_request|
presenter(merge_request).can_remove_source_branch?
end
expose :can_revert_on_current_merge_request do |merge_request|
presenter(merge_request).can_revert_on_current_merge_request?
end
expose :can_cherry_pick_on_current_merge_request do |merge_request|
presenter(merge_request).can_cherry_pick_on_current_merge_request?
end
expose :can_create_note do |merge_request|
can?(request.current_user, :create_note, merge_request)
end
expose :can_create_issue do |merge_request|
can?(current_user, :create_issue, merge_request.project)
end
expose :can_update do |merge_request|
can?(request.current_user, :update_merge_request, merge_request)
end
end
# Paths
#
expose :target_branch_commits_path do |merge_request|
presenter(merge_request).target_branch_commits_path
end
expose :target_branch_tree_path do |merge_request|
presenter(merge_request).target_branch_tree_path
end
expose :new_blob_path do |merge_request|
if presenter(merge_request).can_push_to_source_branch?
project_new_blob_path(merge_request.source_project, merge_request.source_branch)
end
end
expose :conflict_resolution_path do |merge_request|
presenter(merge_request).conflict_resolution_path
end
expose :remove_wip_path do |merge_request|
presenter(merge_request).remove_wip_path
end
expose :cancel_auto_merge_path do |merge_request|
presenter(merge_request).cancel_auto_merge_path
end
expose :create_issue_to_resolve_discussions_path do |merge_request|
presenter(merge_request).create_issue_to_resolve_discussions_path
end
expose :merge_path do |merge_request|
presenter(merge_request).merge_path
end
expose :cherry_pick_in_fork_path do |merge_request|
presenter(merge_request).cherry_pick_in_fork_path
end
expose :revert_in_fork_path do |merge_request|
presenter(merge_request).revert_in_fork_path
end
expose :email_patches_path do |merge_request| expose :email_patches_path do |merge_request|
project_merge_request_path(merge_request.project, merge_request, format: :patch) project_merge_request_path(merge_request.project, merge_request, format: :patch)
...@@ -225,16 +31,8 @@ class MergeRequestWidgetEntity < IssuableEntity ...@@ -225,16 +31,8 @@ class MergeRequestWidgetEntity < IssuableEntity
widget_project_json_merge_request_path(merge_request.target_project, merge_request, format: :json) widget_project_json_merge_request_path(merge_request.target_project, merge_request, format: :json)
end end
expose :ci_environments_status_path do |merge_request| expose :merge_request_cached_widget_path do |merge_request|
ci_environments_status_project_merge_request_path(merge_request.project, merge_request) cached_widget_project_json_merge_request_path(merge_request.target_project, merge_request, format: :json)
end
expose :diverged_commits_count do |merge_request|
if merge_request.open? && merge_request.diverged_from_target_branch?
merge_request.diverged_commits_count
else
0
end
end end
expose :create_note_path do |merge_request| expose :create_note_path do |merge_request|
...@@ -249,57 +47,45 @@ class MergeRequestWidgetEntity < IssuableEntity ...@@ -249,57 +47,45 @@ class MergeRequestWidgetEntity < IssuableEntity
preview_markdown_path(merge_request.project, target_type: 'MergeRequest', target_id: merge_request.iid) preview_markdown_path(merge_request.project, target_type: 'MergeRequest', target_id: merge_request.iid)
end end
expose :merge_commit_path do |merge_request| expose :conflicts_docs_path do |merge_request|
if merge_request.merge_commit_sha help_page_path('user/project/merge_requests/resolve_conflicts.md')
project_commit_path(merge_request.project, merge_request.merge_commit_sha)
end end
expose :merge_request_pipelines_docs_path do |merge_request|
help_page_path('ci/merge_request_pipelines/index.md')
end end
expose :test_reports_path do |merge_request| expose :ci_environments_status_path do |merge_request|
if merge_request.has_test_reports? ci_environments_status_project_merge_request_path(merge_request.project, merge_request)
test_reports_project_merge_request_path(merge_request.project, merge_request, format: :json)
end end
# Rendering and redacting Markdown can be expensive. These links are
# just nice to have in the merge request widget, so only
# include them if they are explicitly requested on first load.
expose :issues_links, if: -> (_, opts) { opts[:issues_links] } do
expose :assign_to_closing do |merge_request|
presenter(merge_request).assign_to_closing_issues_link
end end
expose :supports_suggestion?, as: :can_receive_suggestion expose :closing do |merge_request|
presenter(merge_request).closing_issues_links
end
expose :conflicts_docs_path do |merge_request| expose :mentioned_but_not_closing do |merge_request|
presenter(merge_request).conflicts_docs_path presenter(merge_request).mentioned_issues_links
end
end end
expose :merge_request_pipelines_docs_path do |merge_request| def as_json(options = {})
presenter(merge_request).merge_request_pipelines_docs_path super(options)
.merge(MergeRequestPollCachedWidgetEntity.new(object, **@options.opts_hash).as_json(options))
.merge(MergeRequestPollWidgetEntity.new(object, **@options.opts_hash).as_json(options))
end end
private private
delegate :current_user, to: :request
def presenter(merge_request) def presenter(merge_request)
@presenters ||= {} @presenters ||= {}
@presenters[merge_request] ||= MergeRequestPresenter.new(merge_request, current_user: current_user) # rubocop: disable CodeReuse/Presenter @presenters[merge_request] ||= MergeRequestPresenter.new(merge_request, current_user: request.current_user) # rubocop: disable CodeReuse/Presenter
end
# Once SchedulePopulateMergeRequestMetricsWithEventsData fully runs,
# we can remove this method and just serialize MergeRequest#metrics
# instead. See https://gitlab.com/gitlab-org/gitlab-ce/issues/41587
def build_metrics(merge_request)
# There's no need to query and serialize metrics data for merge requests that are not
# merged or closed.
return unless merge_request.merged? || merge_request.closed?
return merge_request.metrics if merge_request.merged? && merge_request.metrics&.merged_by_id
return merge_request.metrics if merge_request.closed? && merge_request.metrics&.latest_closed_by_id
build_metrics_from_events(merge_request)
end
def build_metrics_from_events(merge_request)
closed_event = merge_request.closed_event
merge_event = merge_request.merge_event
MergeRequest::Metrics.new(latest_closed_at: closed_event&.updated_at,
latest_closed_by: closed_event&.author,
merged_at: merge_event&.updated_at,
merged_by: merge_event&.author)
end end
end end
---
title: Split MR widget into etag-cached and non-cached serializers
merge_request: 31354
author:
type: performance
...@@ -267,6 +267,7 @@ constraints(::Constraints::ProjectUrlConstrainer.new) do ...@@ -267,6 +267,7 @@ constraints(::Constraints::ProjectUrlConstrainer.new) do
get :pipelines get :pipelines
get :diffs, to: 'merge_requests/diffs#show' get :diffs, to: 'merge_requests/diffs#show'
get :widget, to: 'merge_requests/content#widget' get :widget, to: 'merge_requests/content#widget'
get :cached_widget, to: 'merge_requests/content#cached_widget'
end end
get :diff_for_path, controller: 'merge_requests/diffs' get :diff_for_path, controller: 'merge_requests/diffs'
......
...@@ -61,6 +61,10 @@ module Gitlab ...@@ -61,6 +61,10 @@ module Gitlab
Gitlab::EtagCaching::Router::Route.new( Gitlab::EtagCaching::Router::Route.new(
%r(#{RESERVED_WORDS_PREFIX}/import/gitea/realtime_changes\.json\z), %r(#{RESERVED_WORDS_PREFIX}/import/gitea/realtime_changes\.json\z),
'realtime_changes_import_gitea' 'realtime_changes_import_gitea'
),
Gitlab::EtagCaching::Router::Route.new(
%r(#{RESERVED_WORDS_PREFIX}/merge_requests/\d+/cached_widget\.json\z),
'merge_request_widget'
) )
].freeze ].freeze
......
...@@ -11,8 +11,8 @@ describe Projects::MergeRequests::ContentController do ...@@ -11,8 +11,8 @@ describe Projects::MergeRequests::ContentController do
sign_in(user) sign_in(user)
end end
def do_request def do_request(action = :cached_widget)
get :widget, params: { get action, params: {
namespace_id: project.namespace.to_param, namespace_id: project.namespace.to_param,
project_id: project, project_id: project,
id: merge_request.iid, id: merge_request.iid,
...@@ -20,7 +20,6 @@ describe Projects::MergeRequests::ContentController do ...@@ -20,7 +20,6 @@ describe Projects::MergeRequests::ContentController do
} }
end end
describe 'GET widget' do
context 'user has access to the project' do context 'user has access to the project' do
before do before do
expect(::Gitlab::GitalyClient).to receive(:allow_ref_name_caching).and_call_original expect(::Gitlab::GitalyClient).to receive(:allow_ref_name_caching).and_call_original
...@@ -28,33 +27,58 @@ describe Projects::MergeRequests::ContentController do ...@@ -28,33 +27,58 @@ describe Projects::MergeRequests::ContentController do
project.add_maintainer(user) project.add_maintainer(user)
end end
describe 'GET cached_widget' do
it 'renders widget MR entity as json' do it 'renders widget MR entity as json' do
do_request do_request
expect(response).to match_response_schema('entities/merge_request_widget') expect(response).to match_response_schema('entities/merge_request_poll_cached_widget')
end end
it 'closes an MR with moved source project' do
merge_request.update_column(:source_project_id, nil)
expect { do_request }.to change { merge_request.reload.open? }.from(true).to(false)
end
end
describe 'GET widget' do
it 'checks whether the MR can be merged' do it 'checks whether the MR can be merged' do
controller.instance_variable_set(:@merge_request, merge_request) controller.instance_variable_set(:@merge_request, merge_request)
expect(merge_request).to receive(:check_mergeability) expect(merge_request).to receive(:check_mergeability)
do_request do_request(:widget)
end end
it 'closes an MR with moved source project' do context 'merged merge request' do
merge_request.update_column(:source_project_id, nil) let(:merge_request) do
create(:merged_merge_request, :with_test_reports, target_project: project, source_project: project)
end
expect { do_request }.to change { merge_request.reload.open? }.from(true).to(false) it 'renders widget MR entity as json' do
do_request(:widget)
expect(response).to match_response_schema('entities/merge_request_poll_widget')
end
end
end end
end end
context 'user does not have access to the project' do context 'user does not have access to the project' do
it 'renders widget MR entity as json' do describe 'GET cached_widget' do
it 'returns 404' do
do_request do_request
expect(response).to have_http_status(:not_found) expect(response).to have_http_status(:not_found)
end end
end end
describe 'GET widget' do
it 'returns 404' do
do_request(:widget)
expect(response).to have_http_status(:not_found)
end
end
end end
end end
{
"type": "object",
"properties" : {
"id": { "type": "integer" },
"iid": { "type": "integer" },
"description": { "type": ["string", "null"] },
"title": { "type": "string" },
"auto_merge_enabled": { "type": "boolean" },
"state": { "type": "string" },
"merge_commit_sha": { "type": ["string", "null"] },
"short_merge_commit_sha": { "type": ["string", "null"] },
"merge_error": { "type": ["string", "null"] },
"merge_status": { "type": "string" },
"merge_user_id": { "type": ["integer", "null"] },
"source_branch": { "type": "string" },
"source_project_id": { "type": "integer" },
"target_branch": { "type": "string" },
"target_branch_sha": { "type": "string" },
"target_project_id": { "type": "integer" },
"squash": { "type": "boolean" },
"rebase_in_progress": { "type": "boolean" },
"default_squash_commit_message": { "type": "string" },
"commits_count": { "type": ["integer", "null"] },
"merge_ongoing": { "type": "boolean" },
"work_in_progress": { "type": "boolean" },
"has_conflicts": { "type": "boolean" },
"can_be_merged": { "type": "boolean" },
"remove_source_branch": { "type": ["boolean", "null"] },
"source_branch_exists": { "type": "boolean" },
"branch_missing": { "type": "boolean" },
"commits_without_merge_commits": { "type": "array" },
"diff_head_sha": { "type": ["string", "null"] },
"metrics": {
"oneOf": [
{ "type": "null" },
{ "$ref": "merge_request_metrics.json" }
]
},
"diverged_commits_count": { "type": "integer" },
"target_branch_commits_path": { "type": "string" },
"target_branch_tree_path": { "type": "string" },
"merge_commit_path": { "type": ["string", "null"] },
"source_branch_with_namespace_link": { "type": "string" },
"source_branch_path": { "type": "string" }
}
}
{
"type": "object",
"properties" : {
"id": { "type": "integer" },
"iid": { "type": "integer" },
"description": { "type": ["string", "null"] },
"title": { "type": "string" },
"auto_merge_strategy": { "type": ["string", "null"] },
"available_auto_merge_strategies": { "type": "array" },
"source_branch_protected": { "type": "boolean" },
"allow_collaboration": { "type": "boolean"},
"should_be_rebased": { "type": "boolean" },
"ff_only_enabled": { "type": ["boolean", false] },
"merge_user": { "type": ["object", "null"] },
"pipeline": { "type": ["object", "null"] },
"merge_pipeline": { "type": ["object", "null"] },
"default_merge_commit_message": { "type": ["string", "null"] },
"mergeable": { "type": "boolean" },
"default_merge_commit_message_with_description": { "type": "string" },
"mergeable_discussions_state": { "type": "boolean" },
"project_archived": { "type": "boolean" },
"only_allow_merge_if_pipeline_succeeds": { "type": "boolean" },
"has_ci": { "type": "boolean" },
"ci_status": { "type": ["string", "null"] },
"cancel_auto_merge_path": { "type": ["string", "null"] },
"test_reports_path": { "type": ["string", "null"] },
"can_receive_suggestion": { "type": "boolean" },
"create_issue_to_resolve_discussions_path": { "type": ["string", "null"] },
"current_user": {
"type": "object",
"required": [
"can_remove_source_branch",
"can_revert_on_current_merge_request",
"can_cherry_pick_on_current_merge_request"
],
"properties": {
"can_remove_source_branch": { "type": "boolean" },
"can_revert_on_current_merge_request": { "type": ["boolean", "null"] },
"can_cherry_pick_on_current_merge_request": { "type": ["boolean", "null"] },
"can_create_note": { "type": "boolean" },
"can_create_issue": { "type": "boolean" },
"can_update": { "type": "boolean" }
},
"additionalProperties": false
},
"can_push_to_source_branch": { "type": "boolean" },
"new_blob_path": { "type": ["string", "null"] },
"rebase_path": { "type": ["string", "null"] },
"conflict_resolution_path": { "type": ["string", "null"] },
"remove_wip_path": { "type": ["string", "null"] },
"merge_path": { "type": ["string", "null"] },
"cherry_pick_in_fork_path": { "type": ["string", "null"] },
"revert_in_fork_path": { "type": ["string", "null"] }
}
}
{ {
"type": "object", "type": "object",
"allOf": [
{ "$ref": "merge_request_poll_cached_widget.json" },
{ "$ref": "merge_request_poll_widget.json" },
{
"properties" : { "properties" : {
"id": { "type": "integer" },
"iid": { "type": "integer" },
"author_id": { "type": "integer" },
"description": { "type": ["string", "null"] },
"lock_version": { "type": ["string", "null"] },
"milestone_id": { "type": ["string", "null"] },
"position": { "type": "integer" },
"state": { "type": "string" },
"title": { "type": "string" },
"updated_by_id": { "type": ["string", "null"] },
"created_at": { "type": "string" },
"updated_at": { "type": "string" },
"time_estimate": { "type": "integer" },
"total_time_spent": { "type": "integer" },
"human_time_estimate": { "type": ["integer", "null"] },
"human_total_time_spent": { "type": ["integer", "null"] },
"milestone": { "type": ["object", "null"] },
"labels": { "type": ["array", "null"] },
"in_progress_merge_commit_sha": { "type": ["string", "null"] },
"merge_error": { "type": ["string", "null"] },
"merge_commit_sha": { "type": ["string", "null"] },
"short_merge_commit_sha": { "type": ["string", "null"] },
"merge_params": { "type": ["object", "null"] }, "merge_params": { "type": ["object", "null"] },
"merge_status": { "type": "string" },
"merge_user_id": { "type": ["integer", "null"] },
"merge_when_pipeline_succeeds": { "type": "boolean" },
"source_branch": { "type": "string" },
"source_project_id": { "type": "integer" },
"source_project_full_path": { "type": ["string", "null"]}, "source_project_full_path": { "type": ["string", "null"]},
"target_branch": { "type": "string" },
"target_project_id": { "type": "integer" },
"target_project_full_path": { "type": ["string", "null"]}, "target_project_full_path": { "type": ["string", "null"]},
"allow_collaboration": { "type": "boolean"}, "email_patches_path": { "type": "string" },
"metrics": { "plain_diff_path": { "type": "string" },
"oneOf": [ "merge_request_basic_path": { "type": "string" },
{ "type": "null" }, "merge_request_widget_path": { "type": "string" },
{ "$ref": "merge_request_metrics.json" } "merge_request_cached_widget_path": { "type": "string" },
] "create_note_path": { "type": ["string", "null"] },
}, "commit_change_content_path": { "type": "string" },
"author": { "type": ["object", "null"] }, "preview_note_path": { "type": ["string", "null"] },
"merge_user": { "type": ["object", "null"] }, "conflicts_docs_path": { "type": ["string", "null"] },
"diff_head_sha": { "type": ["string", "null"] }, "merge_request_pipelines_docs_path": { "type": ["string", "null"] },
"diff_head_commit_short_id": { "type": ["string", "null"] }, "ci_environments_status_path": { "type": "string" },
"default_merge_commit_message": { "type": ["string", "null"] },
"pipeline": { "type": ["object", "null"] },
"merge_pipeline": { "type": ["object", "null"] },
"work_in_progress": { "type": "boolean" },
"source_branch_exists": { "type": "boolean" },
"mergeable_discussions_state": { "type": "boolean" },
"conflicts_can_be_resolved_in_ui": { "type": "boolean" },
"branch_missing": { "type": "boolean" },
"commits_count": { "type": ["integer", "null"] },
"has_conflicts": { "type": "boolean" },
"can_be_merged": { "type": "boolean" },
"mergeable": { "type": "boolean" },
"project_archived": { "type": "boolean" },
"only_allow_merge_if_pipeline_succeeds": { "type": "boolean" },
"has_ci": { "type": "boolean" },
"ci_status": { "type": ["string", "null"] },
"issues_links": { "issues_links": {
"type": "object", "type": "object",
"required": ["closing", "mentioned_but_not_closing", "assign_to_closing"], "required": ["closing", "mentioned_but_not_closing", "assign_to_closing"],
...@@ -69,64 +28,8 @@ ...@@ -69,64 +28,8 @@
"assign_to_closing": { "type": ["string", "null"] } "assign_to_closing": { "type": ["string", "null"] }
}, },
"additionalProperties": false "additionalProperties": false
},
"source_branch_with_namespace_link": { "type": "string" },
"current_user": {
"type": "object",
"required": [
"can_remove_source_branch",
"can_revert_on_current_merge_request",
"can_cherry_pick_on_current_merge_request"
],
"properties": {
"can_remove_source_branch": { "type": "boolean" },
"can_revert_on_current_merge_request": { "type": ["boolean", "null"] },
"can_cherry_pick_on_current_merge_request": { "type": ["boolean", "null"] },
"can_create_note": { "type": "boolean" },
"can_create_issue": { "type": "boolean" },
"can_update": { "type": "boolean" }
},
"additionalProperties": false
},
"target_branch_commits_path": { "type": "string" },
"target_branch_tree_path": { "type": "string" },
"source_branch_path": { "type": "string" },
"conflict_resolution_path": { "type": ["string", "null"] },
"cancel_merge_when_pipeline_succeeds_path": { "type": ["string", "null"] },
"create_issue_to_resolve_discussions_path": { "type": ["string", "null"] },
"merge_path": { "type": ["string", "null"] },
"cherry_pick_in_fork_path": { "type": ["string", "null"] },
"revert_in_fork_path": { "type": ["string", "null"] },
"email_patches_path": { "type": "string" },
"plain_diff_path": { "type": "string" },
"merge_request_basic_path": { "type": "string" },
"merge_request_widget_path": { "type": "string" },
"new_blob_path": { "type": ["string", "null"] },
"merge_check_path": { "type": "string" },
"ci_environments_status_path": { "type": "string" },
"default_merge_commit_message_with_description": { "type": "string" },
"default_squash_commit_message": { "type": "string" },
"commits_without_merge_commits": { "type": "array" },
"diverged_commits_count": { "type": "integer" },
"commit_change_content_path": { "type": "string" },
"merge_commit_path": { "type": ["string", "null"] },
"remove_wip_path": { "type": ["string", "null"] },
"commits_count": { "type": "integer" },
"remove_source_branch": { "type": ["boolean", "null"] },
"merge_ongoing": { "type": "boolean" },
"ff_only_enabled": { "type": ["boolean", false] },
"should_be_rebased": { "type": "boolean" },
"create_note_path": { "type": ["string", "null"] },
"preview_note_path": { "type": ["string", "null"] },
"rebase_commit_sha": { "type": ["string", "null"] },
"rebase_in_progress": { "type": "boolean" },
"can_push_to_source_branch": { "type": "boolean" },
"rebase_path": { "type": ["string", "null"] },
"squash": { "type": "boolean" },
"test_reports_path": { "type": ["string", "null"] },
"can_receive_suggestion": { "type": "boolean" },
"source_branch_protected": { "type": "boolean" },
"conflicts_docs_path": { "type": ["string", "null"] },
"merge_request_pipelines_docs_path": { "type": ["string", "null"] }
} }
}
}
]
} }
...@@ -1988,6 +1988,7 @@ describe MergeRequest do ...@@ -1988,6 +1988,7 @@ describe MergeRequest do
params = {} params = {}
merge_jid = 'hash-123' merge_jid = 'hash-123'
expect(merge_request).to receive(:expire_etag_cache)
expect(MergeWorker).to receive(:perform_async).with(merge_request.id, user_id, params) do expect(MergeWorker).to receive(:perform_async).with(merge_request.id, user_id, params) do
merge_jid merge_jid
end end
...@@ -2011,6 +2012,7 @@ describe MergeRequest do ...@@ -2011,6 +2012,7 @@ describe MergeRequest do
.with(merge_request.id, user_id) .with(merge_request.id, user_id)
.and_return(rebase_jid) .and_return(rebase_jid)
expect(merge_request).to receive(:expire_etag_cache)
expect(merge_request).to receive(:lock!).and_call_original expect(merge_request).to receive(:lock!).and_call_original
execute execute
......
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