Commit 0033e821 authored by Alex Kalderimis's avatar Alex Kalderimis Committed by Stan Hu

Add diff stats fields to merge request type

This exposes the diff_stats field on merge requests, along with a
summary containing summed values.
parent ba0e7fd9
# frozen_string_literal: true
module Types
# rubocop: disable Graphql/AuthorizeTypes
# Types that use DiffStatsType should have their own authorization
class DiffStatsSummaryType < BaseObject
graphql_name 'DiffStatsSummary'
description 'Aggregated summary of changes'
field :additions, GraphQL::INT_TYPE, null: false,
description: 'Number of lines added'
field :deletions, GraphQL::INT_TYPE, null: false,
description: 'Number of lines deleted'
field :changes, GraphQL::INT_TYPE, null: false,
description: 'Number of lines changed'
def changes
object[:additions] + object[:deletions]
end
end
# rubocop: enable Graphql/AuthorizeTypes
end
# frozen_string_literal: true
module Types
# rubocop: disable Graphql/AuthorizeTypes
# Types that use DiffStatsType should have their own authorization
class DiffStatsType < BaseObject
graphql_name 'DiffStats'
description 'Changes to a single file'
field :path, GraphQL::STRING_TYPE, null: false,
description: 'File path, relative to repository root'
field :additions, GraphQL::INT_TYPE, null: false,
description: 'Number of lines added to this file'
field :deletions, GraphQL::INT_TYPE, null: false,
description: 'Number of lines deleted from this file'
end
# rubocop: enable Graphql/AuthorizeTypes
end
......@@ -54,6 +54,13 @@ module Types
description: 'Indicates if the merge has been set to be merged when its pipeline succeeds (MWPS)'
field :diff_head_sha, GraphQL::STRING_TYPE, null: true,
description: 'Diff head SHA of the merge request'
field :diff_stats, [Types::DiffStatsType], null: true, calls_gitaly: true,
description: 'Details about which files were changed in this merge request' do
argument :path, GraphQL::STRING_TYPE, required: false, description: 'A specific file-path'
end
field :diff_stats_summary, Types::DiffStatsSummaryType, null: true, calls_gitaly: true,
description: 'Summary of which files were changed in this merge request'
field :merge_commit_sha, GraphQL::STRING_TYPE, null: true,
description: 'SHA of the merge request commit (set once merged)'
field :user_notes_count, GraphQL::INT_TYPE, null: true,
......@@ -134,5 +141,24 @@ module Types
end
field :task_completion_status, Types::TaskCompletionStatus, null: false,
description: Types::TaskCompletionStatus.description
def diff_stats(path: nil)
stats = Array.wrap(object.diff_stats&.to_a)
if path.present?
stats.select { |s| s.path == path }
else
stats
end
end
def diff_stats_summary
nil_stats = { additions: 0, deletions: 0 }
return nil_stats unless object.diff_stats.present?
object.diff_stats.each_with_object(nil_stats) do |status, hash|
hash.merge!(additions: status.additions, deletions: status.deletions) { |_, x, y| x + y }
end
end
end
end
---
title: Add diff stats fields to merge request type
merge_request: 34966
author:
type: added
......@@ -3076,6 +3076,46 @@ type DiffRefs {
startSha: String!
}
"""
Changes to a single file
"""
type DiffStats {
"""
Number of lines added to this file
"""
additions: Int!
"""
Number of lines deleted from this file
"""
deletions: Int!
"""
File path, relative to repository root
"""
path: String!
}
"""
Aggregated summary of changes
"""
type DiffStatsSummary {
"""
Number of lines added
"""
additions: Int!
"""
Number of lines changed
"""
changes: Int!
"""
Number of lines deleted
"""
deletions: Int!
}
type Discussion implements ResolvableInterface {
"""
Timestamp of the discussion's creation
......@@ -6665,6 +6705,21 @@ type MergeRequest implements Noteable {
"""
diffRefs: DiffRefs
"""
Details about which files were changed in this merge request
"""
diffStats(
"""
A specific file-path
"""
path: String
): [DiffStats!]
"""
Summary of which files were changed in this merge request
"""
diffStatsSummary: DiffStatsSummary
"""
Indicates if comments on the merge request are locked to members only
"""
......
......@@ -8508,6 +8508,140 @@
"enumValues": null,
"possibleTypes": null
},
{
"kind": "OBJECT",
"name": "DiffStats",
"description": "Changes to a single file",
"fields": [
{
"name": "additions",
"description": "Number of lines added to this file",
"args": [
],
"type": {
"kind": "NON_NULL",
"name": null,
"ofType": {
"kind": "SCALAR",
"name": "Int",
"ofType": null
}
},
"isDeprecated": false,
"deprecationReason": null
},
{
"name": "deletions",
"description": "Number of lines deleted from this file",
"args": [
],
"type": {
"kind": "NON_NULL",
"name": null,
"ofType": {
"kind": "SCALAR",
"name": "Int",
"ofType": null
}
},
"isDeprecated": false,
"deprecationReason": null
},
{
"name": "path",
"description": "File path, relative to repository root",
"args": [
],
"type": {
"kind": "NON_NULL",
"name": null,
"ofType": {
"kind": "SCALAR",
"name": "String",
"ofType": null
}
},
"isDeprecated": false,
"deprecationReason": null
}
],
"inputFields": null,
"interfaces": [
],
"enumValues": null,
"possibleTypes": null
},
{
"kind": "OBJECT",
"name": "DiffStatsSummary",
"description": "Aggregated summary of changes",
"fields": [
{
"name": "additions",
"description": "Number of lines added",
"args": [
],
"type": {
"kind": "NON_NULL",
"name": null,
"ofType": {
"kind": "SCALAR",
"name": "Int",
"ofType": null
}
},
"isDeprecated": false,
"deprecationReason": null
},
{
"name": "changes",
"description": "Number of lines changed",
"args": [
],
"type": {
"kind": "NON_NULL",
"name": null,
"ofType": {
"kind": "SCALAR",
"name": "Int",
"ofType": null
}
},
"isDeprecated": false,
"deprecationReason": null
},
{
"name": "deletions",
"description": "Number of lines deleted",
"args": [
],
"type": {
"kind": "NON_NULL",
"name": null,
"ofType": {
"kind": "SCALAR",
"name": "Int",
"ofType": null
}
},
"isDeprecated": false,
"deprecationReason": null
}
],
"inputFields": null,
"interfaces": [
],
"enumValues": null,
"possibleTypes": null
},
{
"kind": "OBJECT",
"name": "Discussion",
......@@ -18512,6 +18646,51 @@
"isDeprecated": false,
"deprecationReason": null
},
{
"name": "diffStats",
"description": "Details about which files were changed in this merge request",
"args": [
{
"name": "path",
"description": "A specific file-path",
"type": {
"kind": "SCALAR",
"name": "String",
"ofType": null
},
"defaultValue": null
}
],
"type": {
"kind": "LIST",
"name": null,
"ofType": {
"kind": "NON_NULL",
"name": null,
"ofType": {
"kind": "OBJECT",
"name": "DiffStats",
"ofType": null
}
}
},
"isDeprecated": false,
"deprecationReason": null
},
{
"name": "diffStatsSummary",
"description": "Summary of which files were changed in this merge request",
"args": [
],
"type": {
"kind": "OBJECT",
"name": "DiffStatsSummary",
"ofType": null
},
"isDeprecated": false,
"deprecationReason": null
},
{
"name": "discussionLocked",
"description": "Indicates if comments on the merge request are locked to members only",
......@@ -517,6 +517,26 @@ Autogenerated return type of DestroySnippet
| `headSha` | String! | SHA of the HEAD at the time the comment was made |
| `startSha` | String! | SHA of the branch being compared against |
## DiffStats
Changes to a single file
| Name | Type | Description |
| --- | ---- | ---------- |
| `additions` | Int! | Number of lines added to this file |
| `deletions` | Int! | Number of lines deleted from this file |
| `path` | String! | File path, relative to repository root |
## DiffStatsSummary
Aggregated summary of changes
| Name | Type | Description |
| --- | ---- | ---------- |
| `additions` | Int! | Number of lines added |
| `changes` | Int! | Number of lines changed |
| `deletions` | Int! | Number of lines deleted |
## Discussion
| Name | Type | Description |
......@@ -1002,6 +1022,8 @@ Autogenerated return type of MarkAsSpamSnippet
| `descriptionHtml` | String | The GitLab Flavored Markdown rendering of `description` |
| `diffHeadSha` | String | Diff head SHA of the merge request |
| `diffRefs` | DiffRefs | References of the base SHA, the head SHA, and the start SHA for this merge request |
| `diffStats` | DiffStats! => Array | Details about which files were changed in this merge request |
| `diffStatsSummary` | DiffStatsSummary | Summary of which files were changed in this merge request |
| `discussionLocked` | Boolean! | Indicates if comments on the merge request are locked to members only |
| `downvotes` | Int! | Number of downvotes for the merge request |
| `forceRemoveSourceBranch` | Boolean | Indicates if the project settings will lead to source branch deletion after merge |
......
......@@ -15,7 +15,8 @@ RSpec.describe GitlabSchema.types['MergeRequest'] do
description_html state created_at updated_at source_project target_project
project project_id source_project_id target_project_id source_branch
target_branch work_in_progress merge_when_pipeline_succeeds diff_head_sha
merge_commit_sha user_notes_count should_remove_source_branch diff_refs
merge_commit_sha user_notes_count should_remove_source_branch
diff_refs diff_stats diff_stats_summary
force_remove_source_branch merge_status in_progress_merge_commit_sha
merge_error allow_collaboration should_be_rebased rebase_commit_sha
rebase_in_progress merge_commit_message default_merge_commit_message
......
......@@ -43,6 +43,54 @@ RSpec.describe 'getting merge request information nested in a project' do
expect(merge_request_graphql_data['author']['username']).to eq(merge_request.author.username)
end
it 'includes diff stats' do
be_natural = an_instance_of(Integer).and(be >= 0)
post_graphql(query, current_user: current_user)
sums = merge_request_graphql_data['diffStats'].reduce([0, 0, 0]) do |(a, d, c), node|
a_, d_ = node.values_at('additions', 'deletions')
[a + a_, d + d_, c + a_ + d_]
end
expect(merge_request_graphql_data).to include(
'diffStats' => all(a_hash_including('path' => String, 'additions' => be_natural, 'deletions' => be_natural)),
'diffStatsSummary' => a_hash_including('additions' => be_natural, 'deletions' => be_natural, 'changes' => be_natural)
)
# diff_stats is consistent with summary
expect(merge_request_graphql_data['diffStatsSummary']
.values_at('additions', 'deletions', 'changes')).to eq(sums)
# diff_stats_summary is internally consistent
expect(merge_request_graphql_data['diffStatsSummary']
.values_at('additions', 'deletions').sum)
.to eq(merge_request_graphql_data.dig('diffStatsSummary', 'changes'))
.and be_positive
end
context 'requesting a specific diff stat' do
let(:diff_stat) { merge_request.diff_stats.first }
let(:query) do
graphql_query_for(:project, { full_path: project.full_path },
query_graphql_field(:merge_request, { iid: merge_request.iid.to_s }, [
query_graphql_field(:diff_stats, { path: diff_stat.path }, all_graphql_fields_for('DiffStats'))
])
)
end
it 'includes only the requested stats' do
post_graphql(query, current_user: current_user)
expect(merge_request_graphql_data).to include(
'diffStats' => contain_exactly(
a_hash_including('path' => diff_stat.path, 'additions' => diff_stat.additions, 'deletions' => diff_stat.deletions)
)
)
end
end
it 'includes correct mergedAt value when merged' do
time = 1.week.ago
merge_request.mark_as_merged
......
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