Expose merge requests count based on user access
Count issues related merge requests based on user access level. And issue can have related MRs from projects where user does not have access so the number of related merge requests should be adjusted based on user's ability to access the related MRs. https://gitlab.com/gitlab-org/gitlab-ce/issues/59581
This commit is contained in:
parent
e5ca7b8b0b
commit
09163e423a
19 changed files with 192 additions and 54 deletions
|
@ -42,7 +42,7 @@ module IssuableCollections
|
|||
|
||||
@issuables = @issuables.page(params[:page])
|
||||
@issuables = per_page_for_relative_position if params[:sort] == 'relative_position'
|
||||
@issuable_meta_data = issuable_meta_data(@issuables, collection_type)
|
||||
@issuable_meta_data = issuable_meta_data(@issuables, collection_type, current_user)
|
||||
@total_pages = issuable_page_count
|
||||
end
|
||||
# rubocop:enable Gitlab/ModuleWithInstanceVariables
|
||||
|
|
|
@ -11,7 +11,7 @@ module IssuableCollectionsAction
|
|||
.non_archived
|
||||
.page(params[:page])
|
||||
|
||||
@issuable_meta_data = issuable_meta_data(@issues, collection_type)
|
||||
@issuable_meta_data = issuable_meta_data(@issues, collection_type, current_user)
|
||||
|
||||
respond_to do |format|
|
||||
format.html
|
||||
|
@ -22,7 +22,7 @@ module IssuableCollectionsAction
|
|||
def merge_requests
|
||||
@merge_requests = issuables_collection.page(params[:page])
|
||||
|
||||
@issuable_meta_data = issuable_meta_data(@merge_requests, collection_type)
|
||||
@issuable_meta_data = issuable_meta_data(@merge_requests, collection_type, current_user)
|
||||
end
|
||||
# rubocop:enable Gitlab/ModuleWithInstanceVariables
|
||||
|
||||
|
|
|
@ -298,7 +298,7 @@ class ProjectsController < Projects::ApplicationController
|
|||
elsif @project.feature_available?(:issues, current_user)
|
||||
@issues = issuables_collection.page(params[:page])
|
||||
@collection_type = 'Issue'
|
||||
@issuable_meta_data = issuable_meta_data(@issues, @collection_type)
|
||||
@issuable_meta_data = issuable_meta_data(@issues, @collection_type, current_user)
|
||||
end
|
||||
|
||||
render :show
|
||||
|
|
|
@ -280,7 +280,7 @@ module IssuablesHelper
|
|||
initialTaskStatus: issuable.task_status
|
||||
}
|
||||
|
||||
data[:hasClosingMergeRequest] = issuable.merge_requests_count != 0 if issuable.is_a?(Issue)
|
||||
data[:hasClosingMergeRequest] = issuable.merge_requests_count(current_user) != 0 if issuable.is_a?(Issue)
|
||||
|
||||
if parent.is_a?(Group)
|
||||
data[:groupPath] = parent.path
|
||||
|
|
|
@ -29,7 +29,11 @@ module Issuable
|
|||
# This object is used to gather issuable meta data for displaying
|
||||
# upvotes, downvotes, notes and closing merge requests count for issues and merge requests
|
||||
# lists avoiding n+1 queries and improving performance.
|
||||
IssuableMeta = Struct.new(:upvotes, :downvotes, :user_notes_count, :merge_requests_count)
|
||||
IssuableMeta = Struct.new(:upvotes, :downvotes, :user_notes_count, :mrs_count) do
|
||||
def merge_requests_count(user = nil)
|
||||
mrs_count
|
||||
end
|
||||
end
|
||||
|
||||
included do
|
||||
cache_markdown_field :title, pipeline: :single_line
|
||||
|
|
|
@ -250,8 +250,8 @@ class Issue < ApplicationRecord
|
|||
end
|
||||
# rubocop: enable CodeReuse/ServiceClass
|
||||
|
||||
def merge_requests_count
|
||||
merge_requests_closing_issues.count
|
||||
def merge_requests_count(user = nil)
|
||||
::MergeRequestsClosingIssues.count_for_issue(self.id, user)
|
||||
end
|
||||
|
||||
private
|
||||
|
|
|
@ -7,11 +7,38 @@ class MergeRequestsClosingIssues < ApplicationRecord
|
|||
validates :merge_request_id, uniqueness: { scope: :issue_id }, presence: true
|
||||
validates :issue_id, presence: true
|
||||
|
||||
scope :with_issues, ->(ids) { where(issue_id: ids) }
|
||||
scope :with_merge_requests_enabled, -> do
|
||||
joins(:merge_request)
|
||||
.joins('INNER JOIN project_features ON merge_requests.target_project_id = project_features.project_id')
|
||||
.where('project_features.merge_requests_access_level >= :access', access: ProjectFeature::ENABLED)
|
||||
end
|
||||
|
||||
scope :accessible_by, ->(user) do
|
||||
joins(:merge_request)
|
||||
.joins('INNER JOIN project_features ON merge_requests.target_project_id = project_features.project_id')
|
||||
.where('project_features.merge_requests_access_level >= :access OR EXISTS(:authorizations)',
|
||||
access: ProjectFeature::ENABLED,
|
||||
authorizations: user.authorizations_for_projects(min_access_level: Gitlab::Access::REPORTER, related_project_column: "merge_requests.target_project_id")
|
||||
)
|
||||
end
|
||||
|
||||
class << self
|
||||
def count_for_collection(ids)
|
||||
group(:issue_id)
|
||||
.where(issue_id: ids)
|
||||
.pluck('issue_id', 'COUNT(*) as count')
|
||||
def count_for_collection(ids, current_user)
|
||||
closing_merge_requests(ids, current_user).group(:issue_id).pluck('issue_id', 'COUNT(*) as count')
|
||||
end
|
||||
|
||||
def count_for_issue(id, current_user)
|
||||
closing_merge_requests(id, current_user).count
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def closing_merge_requests(ids, current_user)
|
||||
return with_issues(ids) if current_user&.admin?
|
||||
return with_issues(ids).with_merge_requests_enabled if current_user.blank?
|
||||
|
||||
with_issues(ids).accessible_by(current_user)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
|
@ -2,7 +2,7 @@
|
|||
- issue_votes = @issuable_meta_data[issuable.id]
|
||||
- upvotes, downvotes = issue_votes.upvotes, issue_votes.downvotes
|
||||
- issuable_url = @collection_type == "Issue" ? issue_path(issuable, anchor: 'notes') : merge_request_path(issuable, anchor: 'notes')
|
||||
- issuable_mr = @issuable_meta_data[issuable.id].merge_requests_count
|
||||
- issuable_mr = @issuable_meta_data[issuable.id].merge_requests_count(current_user)
|
||||
|
||||
- if issuable_mr > 0
|
||||
%li.issuable-mr.d-none.d-sm-block.has-tooltip{ title: _('Related merge requests') }
|
||||
|
|
|
@ -0,0 +1,5 @@
|
|||
---
|
||||
title: Expose merge requests count based on user access
|
||||
merge_request:
|
||||
author:
|
||||
type: security
|
|
@ -498,9 +498,9 @@ module API
|
|||
expose :state, :created_at, :updated_at
|
||||
|
||||
# Avoids an N+1 query when metadata is included
|
||||
def issuable_metadata(subject, options, method)
|
||||
def issuable_metadata(subject, options, method, args = nil)
|
||||
cached_subject = options.dig(:issuable_metadata, subject.id)
|
||||
(cached_subject || subject).public_send(method) # rubocop: disable GitlabSecurity/PublicSend
|
||||
(cached_subject || subject).public_send(method, *args) # rubocop: disable GitlabSecurity/PublicSend
|
||||
end
|
||||
end
|
||||
|
||||
|
@ -564,7 +564,7 @@ module API
|
|||
end
|
||||
|
||||
expose(:user_notes_count) { |issue, options| issuable_metadata(issue, options, :user_notes_count) }
|
||||
expose(:merge_requests_count) { |issue, options| issuable_metadata(issue, options, :merge_requests_count) }
|
||||
expose(:merge_requests_count) { |issue, options| issuable_metadata(issue, options, :merge_requests_count, options[:current_user]) }
|
||||
expose(:upvotes) { |issue, options| issuable_metadata(issue, options, :upvotes) }
|
||||
expose(:downvotes) { |issue, options| issuable_metadata(issue, options, :downvotes) }
|
||||
expose :due_date
|
||||
|
|
|
@ -96,7 +96,7 @@ module API
|
|||
with: Entities::Issue,
|
||||
with_labels_details: declared_params[:with_labels_details],
|
||||
current_user: current_user,
|
||||
issuable_metadata: issuable_meta_data(issues, 'Issue')
|
||||
issuable_metadata: issuable_meta_data(issues, 'Issue', current_user)
|
||||
}
|
||||
|
||||
present issues, options
|
||||
|
@ -122,7 +122,7 @@ module API
|
|||
with: Entities::Issue,
|
||||
with_labels_details: declared_params[:with_labels_details],
|
||||
current_user: current_user,
|
||||
issuable_metadata: issuable_meta_data(issues, 'Issue')
|
||||
issuable_metadata: issuable_meta_data(issues, 'Issue', current_user)
|
||||
}
|
||||
|
||||
present issues, options
|
||||
|
@ -161,7 +161,7 @@ module API
|
|||
with_labels_details: declared_params[:with_labels_details],
|
||||
current_user: current_user,
|
||||
project: user_project,
|
||||
issuable_metadata: issuable_meta_data(issues, 'Issue')
|
||||
issuable_metadata: issuable_meta_data(issues, 'Issue', current_user)
|
||||
}
|
||||
|
||||
present issues, options
|
||||
|
|
|
@ -72,7 +72,7 @@ module API
|
|||
if params[:view] == 'simple'
|
||||
options[:with] = Entities::MergeRequestSimple
|
||||
else
|
||||
options[:issuable_metadata] = issuable_meta_data(merge_requests, 'MergeRequest')
|
||||
options[:issuable_metadata] = issuable_meta_data(merge_requests, 'MergeRequest', current_user)
|
||||
end
|
||||
|
||||
options
|
||||
|
|
|
@ -65,7 +65,7 @@ module API
|
|||
next unless collection
|
||||
|
||||
targets = collection.map(&:target)
|
||||
options[type] = { issuable_metadata: issuable_meta_data(targets, type) }
|
||||
options[type] = { issuable_metadata: issuable_meta_data(targets, type, current_user) }
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
|
@ -2,7 +2,7 @@
|
|||
|
||||
module Gitlab
|
||||
module IssuableMetadata
|
||||
def issuable_meta_data(issuable_collection, collection_type)
|
||||
def issuable_meta_data(issuable_collection, collection_type, user = nil)
|
||||
# ActiveRecord uses Object#extend for null relations.
|
||||
if !(issuable_collection.singleton_class < ActiveRecord::NullRelation) &&
|
||||
issuable_collection.respond_to?(:limit_value) &&
|
||||
|
@ -23,7 +23,7 @@ module Gitlab
|
|||
issuable_votes_count = ::AwardEmoji.votes_for_collection(issuable_ids, collection_type)
|
||||
issuable_merge_requests_count =
|
||||
if collection_type == 'Issue'
|
||||
::MergeRequestsClosingIssues.count_for_collection(issuable_ids)
|
||||
::MergeRequestsClosingIssues.count_for_collection(issuable_ids, user)
|
||||
else
|
||||
[]
|
||||
end
|
||||
|
|
|
@ -7,11 +7,11 @@ describe Gitlab::IssuableMetadata do
|
|||
subject { Class.new { include Gitlab::IssuableMetadata }.new }
|
||||
|
||||
it 'returns an empty Hash if an empty collection is provided' do
|
||||
expect(subject.issuable_meta_data(Issue.none, 'Issue')).to eq({})
|
||||
expect(subject.issuable_meta_data(Issue.none, 'Issue', user)).to eq({})
|
||||
end
|
||||
|
||||
it 'raises an error when given a collection with no limit' do
|
||||
expect { subject.issuable_meta_data(Issue.all, 'Issue') }.to raise_error(/must have a limit/)
|
||||
expect { subject.issuable_meta_data(Issue.all, 'Issue', user) }.to raise_error(/must have a limit/)
|
||||
end
|
||||
|
||||
context 'issues' do
|
||||
|
@ -23,7 +23,7 @@ describe Gitlab::IssuableMetadata do
|
|||
let!(:closing_issues) { create(:merge_requests_closing_issues, issue: issue, merge_request: merge_request) }
|
||||
|
||||
it 'aggregates stats on issues' do
|
||||
data = subject.issuable_meta_data(Issue.all.limit(10), 'Issue')
|
||||
data = subject.issuable_meta_data(Issue.all.limit(10), 'Issue', user)
|
||||
|
||||
expect(data.count).to eq(2)
|
||||
expect(data[issue.id].upvotes).to eq(1)
|
||||
|
@ -46,7 +46,7 @@ describe Gitlab::IssuableMetadata do
|
|||
let!(:note) { create(:note_on_merge_request, author: user, project: project, noteable: merge_request, note: "a comment on a MR") }
|
||||
|
||||
it 'aggregates stats on merge requests' do
|
||||
data = subject.issuable_meta_data(MergeRequest.all.limit(10), 'MergeRequest')
|
||||
data = subject.issuable_meta_data(MergeRequest.all.limit(10), 'MergeRequest', user)
|
||||
|
||||
expect(data.count).to eq(2)
|
||||
expect(data[merge_request.id].upvotes).to eq(1)
|
||||
|
|
|
@ -23,7 +23,11 @@ describe API::Issues do
|
|||
|
||||
describe 'GET /groups/:id/issues' do
|
||||
let!(:group) { create(:group) }
|
||||
let!(:group_project) { create(:project, :public, creator_id: user.id, namespace: group) }
|
||||
let!(:group_project) { create(:project, :public, :repository, creator_id: user.id, namespace: group) }
|
||||
let!(:private_mrs_project) do
|
||||
create(:project, :public, :repository, creator_id: user.id, namespace: group, merge_requests_access_level: ProjectFeature::PRIVATE)
|
||||
end
|
||||
|
||||
let!(:group_closed_issue) do
|
||||
create :closed_issue,
|
||||
author: user,
|
||||
|
@ -234,6 +238,30 @@ describe API::Issues do
|
|||
it_behaves_like 'group issues statistics'
|
||||
end
|
||||
end
|
||||
|
||||
context "when returns issue merge_requests_count for different access levels" do
|
||||
let!(:merge_request1) do
|
||||
create(:merge_request,
|
||||
:simple,
|
||||
author: user,
|
||||
source_project: private_mrs_project,
|
||||
target_project: private_mrs_project,
|
||||
description: "closes #{group_issue.to_reference(private_mrs_project)}")
|
||||
end
|
||||
let!(:merge_request2) do
|
||||
create(:merge_request,
|
||||
:simple,
|
||||
author: user,
|
||||
source_project: group_project,
|
||||
target_project: group_project,
|
||||
description: "closes #{group_issue.to_reference}")
|
||||
end
|
||||
|
||||
it_behaves_like 'accessible merge requests count' do
|
||||
let(:api_url) { base_url }
|
||||
let(:target_issue) { group_issue }
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
|
|
|
@ -4,8 +4,9 @@ require 'spec_helper'
|
|||
|
||||
describe API::Issues do
|
||||
set(:user) { create(:user) }
|
||||
set(:project) do
|
||||
create(:project, :public, creator_id: user.id, namespace: user.namespace)
|
||||
set(:project) { create(:project, :public, :repository, creator_id: user.id, namespace: user.namespace) }
|
||||
set(:private_mrs_project) do
|
||||
create(:project, :public, :repository, creator_id: user.id, namespace: user.namespace, merge_requests_access_level: ProjectFeature::PRIVATE)
|
||||
end
|
||||
|
||||
let(:user2) { create(:user) }
|
||||
|
@ -60,9 +61,28 @@ describe API::Issues do
|
|||
let(:no_milestone_title) { 'None' }
|
||||
let(:any_milestone_title) { 'Any' }
|
||||
|
||||
let!(:merge_request1) do
|
||||
create(:merge_request,
|
||||
:simple,
|
||||
author: user,
|
||||
source_project: project,
|
||||
target_project: project,
|
||||
description: "closes #{issue.to_reference}")
|
||||
end
|
||||
let!(:merge_request2) do
|
||||
create(:merge_request,
|
||||
:simple,
|
||||
author: user,
|
||||
source_project: private_mrs_project,
|
||||
target_project: private_mrs_project,
|
||||
description: "closes #{issue.to_reference(private_mrs_project)}")
|
||||
end
|
||||
|
||||
before(:all) do
|
||||
project.add_reporter(user)
|
||||
project.add_guest(guest)
|
||||
private_mrs_project.add_reporter(user)
|
||||
private_mrs_project.add_guest(guest)
|
||||
end
|
||||
|
||||
before do
|
||||
|
@ -257,6 +277,11 @@ describe API::Issues do
|
|||
expect_paginated_array_response(issue.id)
|
||||
end
|
||||
|
||||
it_behaves_like 'accessible merge requests count' do
|
||||
let(:api_url) { "/projects/#{project.id}/issues" }
|
||||
let(:target_issue) { issue }
|
||||
end
|
||||
|
||||
context 'with labeled issues' do
|
||||
let(:label_b) { create(:label, title: 'foo', project: project) }
|
||||
let(:label_c) { create(:label, title: 'bar', project: project) }
|
||||
|
@ -636,34 +661,26 @@ describe API::Issues do
|
|||
expect(json_response['iid']).to eq(confidential_issue.iid)
|
||||
end
|
||||
end
|
||||
|
||||
it_behaves_like 'accessible merge requests count' do
|
||||
let(:api_url) { "/projects/#{project.id}/issues/#{issue.iid}" }
|
||||
let(:target_issue) { issue }
|
||||
end
|
||||
end
|
||||
|
||||
describe 'GET :id/issues/:issue_iid/closed_by' do
|
||||
let(:merge_request) do
|
||||
create(:merge_request,
|
||||
:simple,
|
||||
author: user,
|
||||
source_project: project,
|
||||
target_project: project,
|
||||
description: "closes #{issue.to_reference}")
|
||||
end
|
||||
|
||||
before do
|
||||
create(:merge_requests_closing_issues, issue: issue, merge_request: merge_request)
|
||||
end
|
||||
|
||||
context 'when unauthenticated' do
|
||||
it 'return public project issues' do
|
||||
get api("/projects/#{project.id}/issues/#{issue.iid}/closed_by")
|
||||
|
||||
expect_paginated_array_response(merge_request.id)
|
||||
expect_paginated_array_response(merge_request1.id)
|
||||
end
|
||||
end
|
||||
|
||||
it 'returns merge requests that will close issue on merge' do
|
||||
get api("/projects/#{project.id}/issues/#{issue.iid}/closed_by", user)
|
||||
|
||||
expect_paginated_array_response(merge_request.id)
|
||||
expect_paginated_array_response(merge_request1.id)
|
||||
end
|
||||
|
||||
context 'when no merge requests will close issue' do
|
||||
|
@ -721,13 +738,6 @@ describe API::Issues do
|
|||
end
|
||||
|
||||
it 'returns merge requests that mentioned a issue' do
|
||||
create(:merge_request,
|
||||
:simple,
|
||||
author: user,
|
||||
source_project: project,
|
||||
target_project: project,
|
||||
description: 'Some description')
|
||||
|
||||
get_related_merge_requests(project.id, issue.iid, user)
|
||||
|
||||
expect_paginated_array_response(related_mr.id)
|
||||
|
|
|
@ -4,8 +4,9 @@ require 'spec_helper'
|
|||
|
||||
describe API::Issues do
|
||||
set(:user) { create(:user) }
|
||||
set(:project) do
|
||||
create(:project, :public, creator_id: user.id, namespace: user.namespace)
|
||||
set(:project) { create(:project, :public, :repository, creator_id: user.id, namespace: user.namespace) }
|
||||
set(:private_mrs_project) do
|
||||
create(:project, :public, :repository, creator_id: user.id, namespace: user.namespace, merge_requests_access_level: ProjectFeature::PRIVATE)
|
||||
end
|
||||
|
||||
let(:user2) { create(:user) }
|
||||
|
@ -63,6 +64,8 @@ describe API::Issues do
|
|||
before(:all) do
|
||||
project.add_reporter(user)
|
||||
project.add_guest(guest)
|
||||
private_mrs_project.add_reporter(user)
|
||||
private_mrs_project.add_guest(guest)
|
||||
end
|
||||
|
||||
before do
|
||||
|
@ -725,6 +728,30 @@ describe API::Issues do
|
|||
end
|
||||
end
|
||||
end
|
||||
|
||||
context "when returns issue merge_requests_count for different access levels" do
|
||||
let!(:merge_request1) do
|
||||
create(:merge_request,
|
||||
:simple,
|
||||
author: user,
|
||||
source_project: private_mrs_project,
|
||||
target_project: private_mrs_project,
|
||||
description: "closes #{issue.to_reference(private_mrs_project)}")
|
||||
end
|
||||
let!(:merge_request2) do
|
||||
create(:merge_request,
|
||||
:simple,
|
||||
author: user,
|
||||
source_project: project,
|
||||
target_project: project,
|
||||
description: "closes #{issue.to_reference}")
|
||||
end
|
||||
|
||||
it_behaves_like 'accessible merge requests count' do
|
||||
let(:api_url) { "/issues" }
|
||||
let(:target_issue) { issue }
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
describe 'DELETE /projects/:id/issues/:issue_iid' do
|
||||
|
|
|
@ -0,0 +1,37 @@
|
|||
def get_issue
|
||||
json_response.is_a?(Array) ? json_response.detect {|issue| issue['id'] == target_issue.id} : json_response
|
||||
end
|
||||
|
||||
shared_examples 'accessible merge requests count' do
|
||||
it 'returns anonymous accessible merge requests count' do
|
||||
get api(api_url), params: { scope: 'all' }
|
||||
|
||||
issue = get_issue
|
||||
expect(issue).not_to be_nil
|
||||
expect(issue['merge_requests_count']).to eq(1)
|
||||
end
|
||||
|
||||
it 'returns guest accessible merge requests count' do
|
||||
get api(api_url, guest), params: { scope: 'all' }
|
||||
|
||||
issue = get_issue
|
||||
expect(issue).not_to be_nil
|
||||
expect(issue['merge_requests_count']).to eq(1)
|
||||
end
|
||||
|
||||
it 'returns reporter accessible merge requests count' do
|
||||
get api(api_url, user), params: { scope: 'all' }
|
||||
|
||||
issue = get_issue
|
||||
expect(issue).not_to be_nil
|
||||
expect(issue['merge_requests_count']).to eq(2)
|
||||
end
|
||||
|
||||
it 'returns admin accessible merge requests count' do
|
||||
get api(api_url, admin), params: { scope: 'all' }
|
||||
|
||||
issue = get_issue
|
||||
expect(issue).not_to be_nil
|
||||
expect(issue['merge_requests_count']).to eq(2)
|
||||
end
|
||||
end
|
Loading…
Reference in a new issue