Commit b4415c01 authored by Sean McGivern's avatar Sean McGivern

Merge branch 'issue_44821' into 'master'

Retrieve merge request closing issues from database cache

Closes #44821

See merge request gitlab-org/gitlab-ce!20911
parents 415b2f94 fdb5f285
......@@ -60,6 +60,8 @@ class MergeRequest < ActiveRecord::Base
class_name: 'MergeRequestsClosingIssues',
dependent: :delete_all # rubocop:disable Cop/ActiveRecordDependent
has_many :cached_closes_issues, through: :merge_requests_closing_issues, source: :issue
belongs_to :assignee, class_name: "User"
serialize :merge_params, Hash # rubocop:disable Cop/ActiveRecordSerialize
......@@ -763,8 +765,9 @@ class MergeRequest < ActiveRecord::Base
# Calculating this information for a number of merge requests requires
# running `ReferenceExtractor` on each of them separately.
# This optimization does not apply to issues from external sources.
def cache_merge_request_closes_issues!(current_user)
def cache_merge_request_closes_issues!(current_user = self.author)
return unless project.issues_enabled?
return if closed? || merged?
transaction do
self.merge_requests_closing_issues.delete_all
......@@ -777,6 +780,18 @@ class MergeRequest < ActiveRecord::Base
end
end
def visible_closing_issues_for(current_user = self.author)
strong_memoize(:visible_closing_issues_for) do
if self.target_project.has_external_issue_tracker?
closes_issues(current_user)
else
cached_closes_issues.select do |issue|
Ability.allowed?(current_user, :read_issue, issue)
end
end
end
end
# Return the set of issues that will be closed if this merge request is accepted.
def closes_issues(current_user = self.author)
if target_branch == project.default_branch
......@@ -796,7 +811,7 @@ class MergeRequest < ActiveRecord::Base
ext = Gitlab::ReferenceExtractor.new(project, current_user)
ext.analyze("#{title}\n#{description}")
ext.issues - closes_issues(current_user)
ext.issues - visible_closing_issues_for(current_user)
end
def target_project_path
......@@ -844,7 +859,7 @@ class MergeRequest < ActiveRecord::Base
end
def merge_commit_message(include_description: false)
closes_issues_references = closes_issues.map do |issue|
closes_issues_references = visible_closing_issues_for.map do |issue|
issue.to_reference(target_project)
end
......
......@@ -206,7 +206,7 @@ class MergeRequestPresenter < Gitlab::View::Presenter::Delegated
end
def closing_issues
@closing_issues ||= closes_issues(current_user)
@closing_issues ||= visible_closing_issues_for(current_user)
end
def pipeline
......
......@@ -25,7 +25,7 @@ module MergeRequests
def close_issues(merge_request)
return unless merge_request.target_branch == project.default_branch
closed_issues = merge_request.closes_issues(current_user)
closed_issues = merge_request.visible_closing_issues_for(current_user)
closed_issues.each do |issue|
if can?(current_user, :update_issue, issue)
......
......@@ -14,6 +14,7 @@ module MergeRequests
merge_request.mark_as_unchecked
invalidate_cache_counts(merge_request, users: merge_request.assignees)
merge_request.update_project_counter_caches
merge_request.cache_merge_request_closes_issues!(current_user)
end
merge_request
......
---
title: Retrieve merge request closing issues from database cache
merge_request: 20911
author:
type: fixed
......@@ -380,7 +380,7 @@ module API
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 = ::Kaminari.paginate_array(merge_request.visible_closing_issues_for(current_user))
issues = paginate(issues)
external_issues, internal_issues = issues.partition { |issue| issue.is_a?(ExternalIssue) }
......
......@@ -112,6 +112,10 @@ FactoryBot.define do
end
end
after(:create) do |merge_request, evaluator|
merge_request.cache_merge_request_closes_issues!
end
factory :merged_merge_request, traits: [:merged]
factory :closed_merge_request, traits: [:closed]
factory :reopened_merge_request, traits: [:opened]
......
......@@ -89,6 +89,7 @@ merge_requests:
- merge_request_diff
- events
- merge_requests_closing_issues
- cached_closes_issues
- metrics
- timelogs
- head_pipeline
......
......@@ -311,6 +311,51 @@ describe MergeRequest do
end
end
describe '#visible_closing_issues_for' do
let(:guest) { create(:user) }
let(:developer) { create(:user) }
let(:issue_1) { create(:issue, project: subject.source_project) }
let(:issue_2) { create(:issue, project: subject.source_project) }
let(:confidential_issue) { create(:issue, :confidential, project: subject.source_project) }
before do
subject.project.add_developer(subject.author)
subject.target_branch = subject.project.default_branch
commit = double('commit1', safe_message: "Fixes #{issue_1.to_reference} #{issue_2.to_reference} #{confidential_issue.to_reference}")
allow(subject).to receive(:commits).and_return([commit])
end
it 'shows only allowed issues to guest' do
subject.project.add_guest(guest)
subject.cache_merge_request_closes_issues!
expect(subject.visible_closing_issues_for(guest)).to match_array([issue_1, issue_2])
end
it 'shows only allowed issues to developer' do
subject.project.add_developer(developer)
subject.cache_merge_request_closes_issues!
expect(subject.visible_closing_issues_for(developer)).to match_array([issue_1, confidential_issue, issue_2])
end
context 'when external issue tracker is enabled' do
before do
subject.project.has_external_issue_tracker = true
subject.project.save!
end
it 'calls non #closes_issues to retrieve data' do
expect(subject).to receive(:closes_issues)
expect(subject).not_to receive(:cached_closes_issues)
subject.visible_closing_issues_for
end
end
end
describe '#cache_merge_request_closes_issues!' do
before do
subject.project.add_developer(subject.author)
......@@ -325,6 +370,25 @@ describe MergeRequest do
expect { subject.cache_merge_request_closes_issues!(subject.author) }.to change(subject.merge_requests_closing_issues, :count).by(1)
end
it 'does not cache closed issues when merge request is closed' do
issue = create :issue, project: subject.project
commit = double('commit1', safe_message: "Fixes #{issue.to_reference}")
allow(subject).to receive(:commits).and_return([commit])
allow(subject).to receive(:state).and_return("closed")
expect { subject.cache_merge_request_closes_issues!(subject.author) }.not_to change(subject.merge_requests_closing_issues, :count)
end
it 'does not cache closed issues when merge request is merged' do
issue = create :issue, project: subject.project
commit = double('commit1', safe_message: "Fixes #{issue.to_reference}")
allow(subject).to receive(:commits).and_return([commit])
allow(subject).to receive(:state).and_return("merged")
expect { subject.cache_merge_request_closes_issues!(subject.author) }.not_to change(subject.merge_requests_closing_issues, :count)
end
context 'when both internal and external issue trackers are enabled' do
before do
subject.project.has_external_issue_tracker = true
......@@ -633,6 +697,7 @@ describe MergeRequest do
allow(subject).to receive(:commits).and_return([commit])
allow(subject.project).to receive(:default_branch)
.and_return(subject.target_branch)
subject.cache_merge_request_closes_issues!
expect(subject.issues_mentioned_but_not_closing(subject.author)).to match_array([mentioned_issue])
end
......@@ -650,6 +715,8 @@ describe MergeRequest do
end
it 'detects issues mentioned in description but not closed' do
subject.cache_merge_request_closes_issues!
expect(subject.issues_mentioned_but_not_closing(subject.author).map(&:to_s)).to match_array(['TEST-2'])
end
end
......@@ -780,9 +847,8 @@ describe MergeRequest do
subject.project.add_developer(subject.author)
subject.description = "This issue Closes #{issue.to_reference}"
allow(subject.project).to receive(:default_branch)
.and_return(subject.target_branch)
allow(subject.project).to receive(:default_branch).and_return(subject.target_branch)
subject.cache_merge_request_closes_issues!
expect(subject.merge_commit_message)
.to match("Closes #{issue.to_reference}")
......
......@@ -117,9 +117,9 @@ describe MergeRequestPresenter do
before do
project.add_developer(user)
allow(resource.project).to receive(:default_branch)
.and_return(resource.target_branch)
resource.cache_merge_request_closes_issues!
end
describe '#closing_issues_links' do
......
......@@ -954,6 +954,7 @@ describe API::MergeRequests do
issue = create(:issue, project: project)
mr = merge_request.tap do |mr|
mr.update_attribute(:description, "Closes #{issue.to_reference(mr.project)}")
mr.cache_merge_request_closes_issues!
end
get api("/projects/#{project.id}/merge_requests/#{mr.iid}/closes_issues", user)
......
......@@ -20,7 +20,7 @@ describe Issues::ReopenService do
end
end
context 'when user is authrized to reopen issue' do
context 'when user is authorized to reopen issue' do
let(:user) { create(:user) }
before do
......
......@@ -49,6 +49,7 @@ describe MergeRequests::MergeService do
issue = create :issue, project: project
commit = double('commit', safe_message: "Fixes #{issue.to_reference}")
allow(merge_request).to receive(:commits).and_return([commit])
merge_request.cache_merge_request_closes_issues!
service.execute(merge_request)
......
......@@ -53,7 +53,7 @@ describe MergeRequests::PostMergeService do
allow(project).to receive(:default_branch).and_return('foo')
issue = create(:issue, project: project)
allow(merge_request).to receive(:closes_issues).and_return([issue])
allow(merge_request).to receive(:visible_closing_issues_for).and_return([issue])
allow_any_instance_of(Issues::CloseService).to receive(:execute).with(issue, commit: merge_request).and_raise
expect { described_class.new(project, user, {}).execute(merge_request) }.to raise_error
......
......@@ -47,6 +47,12 @@ describe MergeRequests::ReopenService do
end
end
it 'caches merge request closing issues' do
expect(merge_request).to receive(:cache_merge_request_closes_issues!)
described_class.new(project, user, {}).execute(merge_request)
end
it 'updates metrics' do
metrics = merge_request.metrics
service = double(MergeRequestMetricsService)
......
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