Commit 4a526800 authored by Aishwarya Subramanian's avatar Aishwarya Subramanian

Backend changes for merge commit csv export

Adds a service for CSV export of Merge Commits.
This service is invoked in a controller action
that lets Group owners and administrators download
Merge Commits for a Group in the Compliance Dashboard.
parent e4695080
...@@ -264,10 +264,14 @@ class MergeRequest < ApplicationRecord ...@@ -264,10 +264,14 @@ class MergeRequest < ApplicationRecord
end end
scope :by_target_branch, ->(branch_name) { where(target_branch: branch_name) } scope :by_target_branch, ->(branch_name) { where(target_branch: branch_name) }
scope :preload_source_project, -> { preload(:source_project) } scope :preload_source_project, -> { preload(:source_project) }
scope :preload_target_project, -> { preload(:target_project) }
scope :preload_routables, -> do scope :preload_routables, -> do
preload(target_project: [:route, { namespace: :route }], preload(target_project: [:route, { namespace: :route }],
source_project: [:route, { namespace: :route }]) source_project: [:route, { namespace: :route }])
end end
scope :preload_author, -> { preload(:author) }
scope :preload_approved_by_users, -> { preload(:approved_by_users) }
scope :preload_metrics, -> (relation) { preload(metrics: relation) }
scope :with_auto_merge_enabled, -> do scope :with_auto_merge_enabled, -> do
with_state(:opened).where(auto_merge_enabled: true) with_state(:opened).where(auto_merge_enabled: true)
......
...@@ -26,8 +26,4 @@ class Groups::Security::ComplianceDashboardsController < Groups::ApplicationCont ...@@ -26,8 +26,4 @@ class Groups::Security::ComplianceDashboardsController < Groups::ApplicationCont
def serialize(merge_requests) def serialize(merge_requests)
MergeRequestSerializer.new(current_user: current_user).represent(merge_requests, serializer: 'compliance_dashboard') MergeRequestSerializer.new(current_user: current_user).represent(merge_requests, serializer: 'compliance_dashboard')
end end
def authorize_compliance_dashboard!
render_404 unless group_level_compliance_dashboard_available?(group)
end
end end
# frozen_string_literal: true
class Groups::Security::MergeCommitReportsController < Groups::ApplicationController
include Groups::SecurityFeaturesHelper
before_action :authorize_compliance_dashboard!
def index
csv_data = MergeCommits::ExportCsvService.new(current_user, group).csv_data
respond_to do |format|
format.csv do
send_data(
csv_data,
type: 'text/csv; charset=utf-8; header=present',
filename: merge_commits_csv_filename
)
end
end
end
private
def merge_commits_csv_filename
"#{group.id}-merge-commits-#{Time.current.to_i}.csv"
end
end
...@@ -10,6 +10,10 @@ module Groups::SecurityFeaturesHelper ...@@ -10,6 +10,10 @@ module Groups::SecurityFeaturesHelper
can?(current_user, :read_group_compliance_dashboard, group) can?(current_user, :read_group_compliance_dashboard, group)
end end
def authorize_compliance_dashboard!
render_404 unless group_level_compliance_dashboard_available?(group)
end
def group_level_credentials_inventory_available?(group) def group_level_credentials_inventory_available?(group)
can?(current_user, :read_group_credentials_inventory, group) && can?(current_user, :read_group_credentials_inventory, group) &&
group.feature_available?(:credentials_inventory) && group.feature_available?(:credentials_inventory) &&
......
# frozen_string_literal: true
module MergeCommits
class ExportCsvService
TARGET_FILESIZE = 15_000_000 # file size restricted to 15MB
def initialize(current_user, group)
@current_user = current_user
@group = group
end
def csv_data
csv_builder.render(TARGET_FILESIZE)
end
private
attr_reader :current_user, :group
def csv_builder
@csv_builder ||= CsvBuilder.new(data, header_to_value_hash)
end
def data
MergeRequestsFinder
.new(current_user, finder_options)
.execute
.preload_author
.preload_approved_by_users
.preload_target_project
.preload_metrics([:merged_by])
end
def finder_options
{
group_id: group.id,
state: 'merged'
}
end
def header_to_value_hash
{
'Merge Commit' => 'merge_commit_sha',
'Author' => -> (merge_request) { merge_request.author&.name },
'Merge Request' => 'id',
'Merged By' => -> (merge_request) { merge_request.metrics&.merged_by&.name },
'Pipeline' => -> (merge_request) { merge_request.metrics&.pipeline_id },
'Group' => -> (merge_request) { merge_request.project&.namespace&.name },
'Project' => -> (merge_request) { merge_request.project&.name },
'Approver(s)' => -> (merge_request) { merge_request.approved_by_users.map(&:name).sort.join(" | ") }
}
end
end
end
---
title: Provision for csv download for merge commits in a group
merge_request: 37980
author:
type: added
...@@ -152,6 +152,7 @@ constraints(::Constraints::GroupUrlConstrainer.new) do ...@@ -152,6 +152,7 @@ constraints(::Constraints::GroupUrlConstrainer.new) do
resources :vulnerable_projects, only: [:index] resources :vulnerable_projects, only: [:index]
resource :discover, only: [:show], controller: :discover resource :discover, only: [:show], controller: :discover
resources :credentials, only: [:index] resources :credentials, only: [:index]
resources :merge_commit_reports, only: [:index], constraints: { format: :csv }
end end
resource :push_rules, only: [:edit, :update] resource :push_rules, only: [:edit, :update]
......
# frozen_string_literal: true
require 'spec_helper'
RSpec.describe Groups::Security::MergeCommitReportsController do
let_it_be(:user) { create(:user, name: 'John Cena') }
let_it_be(:group) { create(:group, name: 'Kombucha lovers') }
before do
sign_in(user)
end
describe 'GET index' do
subject { get :index, params: { group_id: group.to_param }, format: :csv }
shared_examples 'returns not found' do
it { is_expected.to have_gitlab_http_status(:not_found) }
end
context 'when feature is enabled' do
context 'when user has access to dashboard' do
let(:csv_data) do
<<~CSV
Merge Commit,Author,Merge Request,Merged By,Pipeline,Group,Project,Approver(s)
12bsr67h,John Cena,10034,Brock Lesnar,2301,Kombucha lovers,Starter kit,Brock Lesnar | Kane
CSV
end
let(:export_csv_service) { instance_spy(MergeCommits::ExportCsvService, csv_data: csv_data) }
before_all do
group.add_owner(user)
end
before do
stub_licensed_features(group_level_compliance_dashboard: true)
allow(MergeCommits::ExportCsvService).to receive(:new).and_return(export_csv_service)
end
it 'returns a csv file in response' do
subject
expect(response).to have_gitlab_http_status(:ok)
expect(response.headers['Content-Type']).to eq('text/csv; charset=utf-8')
end
context 'data validation' do
it do
subject
expect(csv_response).to eq([
[
'Merge Commit',
'Author',
'Merge Request',
'Merged By',
'Pipeline',
'Group',
'Project',
'Approver(s)'
],
[
'12bsr67h',
'John Cena',
'10034',
'Brock Lesnar',
'2301',
'Kombucha lovers',
'Starter kit',
'Brock Lesnar | Kane'
]
])
end
end
end
context 'when user does not have access to dashboard' do
it_behaves_like 'returns not found'
end
end
context 'when feature is not enabled' do
it_behaves_like 'returns not found'
end
def csv_response
CSV.parse(response.body)
end
end
end
# frozen_string_literal: true
require 'spec_helper'
RSpec.describe MergeCommits::ExportCsvService do
subject { described_class.new(user, group) }
let_it_be(:group) { create(:group, name: 'Kombucha lovers') }
let_it_be(:user) { create(:user, name: 'John Cena') }
let_it_be(:project) { create(:project, :repository, namespace: group, name: 'Starter kit') }
let_it_be(:merge_user) { create(:user, name: 'Brock Lesnar') }
let_it_be(:merge_request) { create(:merge_request_with_diffs, :with_merged_metrics, merged_by: merge_user, source_project: project, target_project: project, author: user, merge_commit_sha: '347yrv45') }
let_it_be(:approval) { create(:approval, merge_request: merge_request, user: merge_user) }
let_it_be(:approval2) { create(:approval, merge_request: merge_request, user_id: create(:user, name: 'Kane').id) }
let_it_be(:open_merge_request) { create(:merge_request, source_project: project, target_project: project, author: user) }
before_all do
project.add_maintainer(user)
end
it 'includes the appropriate headers' do
expect(csv.headers).to eq(['Merge Commit', 'Author', 'Merge Request', 'Merged By', 'Pipeline', 'Group', 'Project', 'Approver(s)'])
end
context 'data verification' do
specify 'Merge Commit' do
expect(csv[0]['Merge Commit']).to eq '347yrv45'
end
specify 'Author' do
expect(csv[0]['Author']).to eq 'John Cena'
end
specify 'Merge Request' do
expect(csv[0]['Merge Request']).to eq merge_request.id.to_s
end
specify 'Merged By' do
expect(csv[0]['Merged By']).to eq 'Brock Lesnar'
end
specify 'Pipeline' do
expect(csv[0]['Pipeline']).to eq merge_request.metrics.pipeline_id.to_s
end
specify 'Group' do
expect(csv[0]['Group']).to eq 'Kombucha lovers'
end
specify 'Project' do
expect(csv[0]['Project']).to eq 'Starter kit'
end
specify 'Approver(s)' do
expect(csv[0]['Approver(s)']).to eq 'Brock Lesnar | Kane'
end
end
context 'with multiple merge requests' do
let_it_be(:merge_request_2) { create(:merge_request_with_diffs, source_project: project, target_project: project, state: :merged) }
it do
expect(csv.count).to eq 2
end
end
def csv
CSV.parse(subject.csv_data, headers: true)
end
end
...@@ -43,6 +43,21 @@ FactoryBot.define do ...@@ -43,6 +43,21 @@ FactoryBot.define do
state_id { MergeRequest.available_states[:merged] } state_id { MergeRequest.available_states[:merged] }
end end
trait :with_merged_metrics do
merged
transient do
merged_by { author }
end
after(:build) do |merge_request, evaluator|
metrics = merge_request.build_metrics
metrics.merged_at = 1.week.ago
metrics.merged_by = evaluator.merged_by
metrics.pipeline = create(:ci_empty_pipeline)
end
end
trait :merged_target do trait :merged_target do
source_branch { "merged-target" } source_branch { "merged-target" }
target_branch { "improve/awesome" } target_branch { "improve/awesome" }
......
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