Skip to content
Projects
Groups
Snippets
Help
Loading...
Help
Support
Keyboard shortcuts
?
Submit feedback
Contribute to GitLab
Sign in / Register
Toggle navigation
G
gitlab-ce
Project overview
Project overview
Details
Activity
Releases
Repository
Repository
Files
Commits
Branches
Tags
Contributors
Graph
Compare
Issues
0
Issues
0
List
Boards
Labels
Milestones
Merge Requests
1
Merge Requests
1
Analytics
Analytics
Repository
Value Stream
Wiki
Wiki
Snippets
Snippets
Members
Members
Collapse sidebar
Close sidebar
Activity
Graph
Create a new issue
Commits
Issue Boards
Open sidebar
nexedi
gitlab-ce
Commits
197d88f6
Commit
197d88f6
authored
Nov 14, 2019
by
Stan Hu
Browse files
Options
Browse Files
Download
Plain Diff
Merge branch 'epics-count' into 'master'
Expose epic descendant counts See merge request gitlab-org/gitlab!19450
parents
e04920dd
96167244
Changes
15
Hide whitespace changes
Inline
Side-by-side
Showing
15 changed files
with
353 additions
and
3 deletions
+353
-3
app/models/issue.rb
app/models/issue.rb
+2
-0
doc/api/graphql/reference/gitlab_schema.graphql
doc/api/graphql/reference/gitlab_schema.graphql
+27
-0
doc/api/graphql/reference/gitlab_schema.json
doc/api/graphql/reference/gitlab_schema.json
+83
-0
doc/api/graphql/reference/index.md
doc/api/graphql/reference/index.md
+10
-0
ee/app/graphql/types/epic_descendant_count_type.rb
ee/app/graphql/types/epic_descendant_count_type.rb
+14
-0
ee/app/graphql/types/epic_type.rb
ee/app/graphql/types/epic_type.rb
+6
-0
ee/app/models/ee/epic.rb
ee/app/models/ee/epic.rb
+16
-0
ee/app/models/ee/issue.rb
ee/app/models/ee/issue.rb
+5
-0
ee/app/services/epics/descendant_count_service.rb
ee/app/services/epics/descendant_count_service.rb
+52
-0
ee/changelogs/unreleased/epics-count.yml
ee/changelogs/unreleased/epics-count.yml
+5
-0
ee/spec/graphql/types/epic_descendant_count_type_spec.rb
ee/spec/graphql/types/epic_descendant_count_type_spec.rb
+13
-0
ee/spec/graphql/types/epic_type_spec.rb
ee/spec/graphql/types/epic_type_spec.rb
+1
-0
ee/spec/models/epic_spec.rb
ee/spec/models/epic_spec.rb
+42
-3
ee/spec/models/issue_spec.rb
ee/spec/models/issue_spec.rb
+16
-0
ee/spec/services/epics/descendant_count_service_spec.rb
ee/spec/services/epics/descendant_count_service_spec.rb
+61
-0
No files found.
app/models/issue.rb
View file @
197d88f6
...
...
@@ -66,6 +66,8 @@ class Issue < ApplicationRecord
scope
:public_only
,
->
{
where
(
confidential:
false
)
}
scope
:confidential_only
,
->
{
where
(
confidential:
true
)
}
scope
:counts_by_state
,
->
{
reorder
(
nil
).
group
(
:state
).
count
}
after_commit
:expire_etag_cache
after_save
:ensure_metrics
,
unless: :imported?
...
...
doc/api/graphql/reference/gitlab_schema.graphql
View file @
197d88f6
...
...
@@ -1124,6 +1124,11 @@ type Epic implements Noteable {
)
:
EpicConnection
closedAt
:
Time
createdAt
:
Time
"""
Number
of
open
and
closed
descendant
epics
and
issues
"""
descendantCounts
:
EpicDescendantCount
description
:
String
"""
...
...
@@ -1304,6 +1309,28 @@ type EpicConnection {
pageInfo
:
PageInfo
!
}
type
EpicDescendantCount
{
"""
Number
of
closed
sub
-
epics
"""
closedEpics
:
Int
"""
Number
of
closed
epic
issues
"""
closedIssues
:
Int
"""
Number
of
opened
sub
-
epics
"""
openedEpics
:
Int
"""
Number
of
opened
epic
issues
"""
openedIssues
:
Int
}
"""
An edge in a connection.
"""
...
...
doc/api/graphql/reference/gitlab_schema.json
View file @
197d88f6
...
...
@@ -3518,6 +3518,20 @@
"isDeprecated"
:
false
,
"deprecationReason"
:
null
},
{
"name"
:
"descendantCounts"
,
"description"
:
"Number of open and closed descendant epics and issues"
,
"args"
:
[
],
"type"
:
{
"kind"
:
"OBJECT"
,
"name"
:
"EpicDescendantCount"
,
"ofType"
:
null
},
"isDeprecated"
:
false
,
"deprecationReason"
:
null
},
{
"name"
:
"description"
,
"description"
:
null
,
...
...
@@ -9619,6 +9633,75 @@
"enumValues"
:
null
,
"possibleTypes"
:
null
},
{
"kind"
:
"OBJECT"
,
"name"
:
"EpicDescendantCount"
,
"description"
:
null
,
"fields"
:
[
{
"name"
:
"closedEpics"
,
"description"
:
"Number of closed sub-epics"
,
"args"
:
[
],
"type"
:
{
"kind"
:
"SCALAR"
,
"name"
:
"Int"
,
"ofType"
:
null
},
"isDeprecated"
:
false
,
"deprecationReason"
:
null
},
{
"name"
:
"closedIssues"
,
"description"
:
"Number of closed epic issues"
,
"args"
:
[
],
"type"
:
{
"kind"
:
"SCALAR"
,
"name"
:
"Int"
,
"ofType"
:
null
},
"isDeprecated"
:
false
,
"deprecationReason"
:
null
},
{
"name"
:
"openedEpics"
,
"description"
:
"Number of opened sub-epics"
,
"args"
:
[
],
"type"
:
{
"kind"
:
"SCALAR"
,
"name"
:
"Int"
,
"ofType"
:
null
},
"isDeprecated"
:
false
,
"deprecationReason"
:
null
},
{
"name"
:
"openedIssues"
,
"description"
:
"Number of opened epic issues"
,
"args"
:
[
],
"type"
:
{
"kind"
:
"SCALAR"
,
"name"
:
"Int"
,
"ofType"
:
null
},
"isDeprecated"
:
false
,
"deprecationReason"
:
null
}
],
"inputFields"
:
null
,
"interfaces"
:
[
],
"enumValues"
:
null
,
"possibleTypes"
:
null
},
{
"kind"
:
"OBJECT"
,
"name"
:
"ProjectStatistics"
,
...
...
doc/api/graphql/reference/index.md
View file @
197d88f6
...
...
@@ -220,6 +220,16 @@ The API can be explored interactively using the [GraphiQL IDE](../index.md#graph
|
`relationPath`
| String | |
|
`reference`
| String! | |
|
`subscribed`
| Boolean! | Boolean flag for whether the currently logged in user is subscribed to this epic |
|
`descendantCounts`
| EpicDescendantCount | Number of open and closed descendant epics and issues |
### EpicDescendantCount
| Name | Type | Description |
| --- | ---- | ---------- |
|
`openedEpics`
| Int | Number of opened sub-epics |
|
`closedEpics`
| Int | Number of closed sub-epics |
|
`openedIssues`
| Int | Number of opened epic issues |
|
`closedIssues`
| Int | Number of closed epic issues |
### EpicIssue
...
...
ee/app/graphql/types/epic_descendant_count_type.rb
0 → 100644
View file @
197d88f6
# frozen_string_literal: true
module
Types
# rubocop: disable Graphql/AuthorizeTypes
class
EpicDescendantCountType
<
BaseObject
graphql_name
'EpicDescendantCount'
field
:opened_epics
,
GraphQL
::
INT_TYPE
,
null:
true
,
description:
'Number of opened sub-epics'
field
:closed_epics
,
GraphQL
::
INT_TYPE
,
null:
true
,
description:
'Number of closed sub-epics'
field
:opened_issues
,
GraphQL
::
INT_TYPE
,
null:
true
,
description:
'Number of opened epic issues'
field
:closed_issues
,
GraphQL
::
INT_TYPE
,
null:
true
,
description:
'Number of closed epic issues'
end
# rubocop: enable Graphql/AuthorizeTypes
end
ee/app/graphql/types/epic_type.rb
View file @
197d88f6
...
...
@@ -71,5 +71,11 @@ module Types
Types
::
EpicIssueType
.
connection_type
,
null:
true
,
resolver:
Resolvers
::
EpicIssuesResolver
field
:descendant_counts
,
Types
::
EpicDescendantCountType
,
null:
true
,
complexity:
10
,
description:
'Number of open and closed descendant epics and issues'
,
resolve:
->
(
epic
,
args
,
ctx
)
do
Epics
::
DescendantCountService
.
new
(
epic
,
ctx
[
:current_user
])
end
end
end
ee/app/models/ee/epic.rb
View file @
197d88f6
...
...
@@ -61,6 +61,7 @@ module EE
scope
:for_ids
,
->
(
ids
)
{
where
(
id:
ids
)
}
scope
:in_parents
,
->
(
parent_ids
)
{
where
(
parent_id:
parent_ids
)
}
scope
:inc_group
,
->
{
includes
(
:group
)
}
scope
:in_selected_groups
,
->
(
groups
)
{
where
(
group_id:
groups
)
}
scope
:in_milestone
,
->
(
milestone_id
)
{
joins
(
:issues
).
where
(
issues:
{
milestone_id:
milestone_id
})
}
scope
:in_issues
,
->
(
issues
)
{
joins
(
:epic_issues
).
where
(
epic_issues:
{
issue_id:
issues
}).
distinct
}
scope
:has_parent
,
->
{
where
.
not
(
parent_id:
nil
)
}
...
...
@@ -93,6 +94,8 @@ module EE
scope
:start_date_inherited
,
->
{
where
(
start_date_is_fixed:
[
nil
,
false
])
}
scope
:due_date_inherited
,
->
{
where
(
due_date_is_fixed:
[
nil
,
false
])
}
scope
:counts_by_state
,
->
{
group
(
:state_id
).
count
}
MAX_HIERARCHY_DEPTH
=
5
def
etag_caching_enabled?
...
...
@@ -191,6 +194,15 @@ module EE
def
deepest_relationship_level
::
Gitlab
::
ObjectHierarchy
.
new
(
self
.
where
(
parent_id:
nil
)).
max_descendants_depth
end
def
groups_user_can_read_epics
(
epics
,
user
)
groups
=
::
Group
.
where
(
id:
epics
.
select
(
:group_id
))
groups
=
::
Gitlab
::
GroupPlansPreloader
.
new
.
preload
(
groups
)
DeclarativePolicy
.
user_scope
do
groups
.
select
{
|
g
|
Ability
.
allowed?
(
user
,
:read_epic
,
g
)
}
end
end
end
def
resource_parent
...
...
@@ -276,6 +288,10 @@ module EE
hierarchy
.
descendants
end
def
base_and_descendants
hierarchy
.
base_and_descendants
end
def
has_ancestor?
(
epic
)
ancestors
.
exists?
(
epic
.
id
)
end
...
...
ee/app/models/ee/issue.rb
View file @
197d88f6
...
...
@@ -19,6 +19,11 @@ module EE
scope
:order_created_at_desc
,
->
{
reorder
(
created_at: :desc
)
}
scope
:service_desk
,
->
{
where
(
author:
::
User
.
support_bot
)
}
scope
:in_epics
,
->
(
epics
)
do
issue_ids
=
EpicIssue
.
where
(
epic_id:
epics
).
select
(
:issue_id
)
id_in
(
issue_ids
)
end
has_one
:epic_issue
has_one
:epic
,
through: :epic_issue
has_many
:designs
,
class_name:
"DesignManagement::Design"
,
inverse_of: :issue
...
...
ee/app/services/epics/descendant_count_service.rb
0 → 100644
View file @
197d88f6
# frozen_string_literal: true
module
Epics
class
DescendantCountService
include
Gitlab
::
Utils
::
StrongMemoize
attr_reader
:epic
,
:current_user
def
initialize
(
epic
,
current_user
)
@epic
=
epic
@current_user
=
current_user
end
def
opened_epics
epics_count
.
fetch
(
Epic
.
state_ids
[
:opened
],
0
)
end
def
closed_epics
epics_count
.
fetch
(
Epic
.
state_ids
[
:closed
],
0
)
end
def
opened_issues
issues_count
.
fetch
(
'opened'
,
0
)
end
def
closed_issues
issues_count
.
fetch
(
'closed'
,
0
)
end
private
def
epics_count
strong_memoize
(
:epics_count
)
do
accessible_epics
.
id_not_in
(
epic
.
id
).
counts_by_state
end
end
def
issues_count
strong_memoize
(
:issue_counts
)
do
IssuesFinder
.
new
(
current_user
).
execute
.
in_epics
(
accessible_epics
).
counts_by_state
end
end
def
accessible_epics
strong_memoize
(
:epics
)
do
epics
=
epic
.
base_and_descendants
groups
=
Epic
.
groups_user_can_read_epics
(
epics
,
current_user
)
epics
.
in_selected_groups
(
groups
)
end
end
end
end
ee/changelogs/unreleased/epics-count.yml
0 → 100644
View file @
197d88f6
---
title
:
Expose number of sub-epics and epic issues in GraphQL API
merge_request
:
19450
author
:
type
:
added
ee/spec/graphql/types/epic_descendant_count_type_spec.rb
0 → 100644
View file @
197d88f6
# frozen_string_literal: true
require
'spec_helper'
describe
GitlabSchema
.
types
[
'EpicDescendantCount'
]
do
it
{
expect
(
described_class
.
graphql_name
).
to
eq
(
'EpicDescendantCount'
)
}
it
'has specific fields'
do
%i[opened_epics closed_epics opened_issues closed_issues]
.
each
do
|
field_name
|
expect
(
described_class
).
to
have_graphql_field
(
field_name
)
end
end
end
ee/spec/graphql/types/epic_type_spec.rb
View file @
197d88f6
...
...
@@ -11,6 +11,7 @@ describe GitlabSchema.types['Epic'] do
closed_at created_at updated_at children has_children has_issues
web_path web_url relation_path reference issues user_permissions
notes discussions relative_position subscribed participants
descendant_counts
]
end
...
...
ee/spec/models/epic_spec.rb
View file @
197d88f6
...
...
@@ -3,7 +3,8 @@
require
'spec_helper'
describe
Epic
do
set
(
:group
)
{
create
(
:group
)
}
let_it_be
(
:user
)
{
create
(
:user
)
}
let_it_be
(
:group
)
{
create
(
:group
)
}
let
(
:project
)
{
create
(
:project
,
group:
group
)
}
describe
'associations'
do
...
...
@@ -100,6 +101,46 @@ describe Epic do
end
end
describe
'.groups_user_can_read_epics'
do
let_it_be
(
:private_group
)
{
create
(
:group
,
:private
)
}
let_it_be
(
:epic
)
{
create
(
:epic
,
group:
private_group
)
}
subject
do
epics
=
described_class
.
where
(
id:
epic
.
id
)
described_class
.
groups_user_can_read_epics
(
epics
,
user
)
end
it
'does not return inaccessible groups'
do
expect
(
subject
).
to
be_empty
end
context
'with authorized user'
do
before
do
private_group
.
add_developer
(
user
)
end
context
'with epics enabled'
do
before
do
stub_licensed_features
(
epics:
true
)
end
it
'returns epic groups user can access'
do
expect
(
subject
).
to
eq
[
private_group
]
end
end
context
'with epics are disabled'
do
before
do
stub_licensed_features
(
epics:
false
)
end
it
'returns an empty list'
do
expect
(
subject
).
to
be_empty
end
end
end
end
describe
'#valid_parent?'
do
context
'basic checks'
do
let
(
:epic
)
{
build
(
:epic
,
group:
group
)
}
...
...
@@ -352,7 +393,6 @@ describe Epic do
end
describe
'#issues_readable_by'
do
let
(
:user
)
{
create
(
:user
)
}
let
(
:group
)
{
create
(
:group
,
:private
)
}
let
(
:project
)
{
create
(
:project
,
group:
group
)
}
let
(
:project2
)
{
create
(
:project
,
group:
group
)
}
...
...
@@ -400,7 +440,6 @@ describe Epic do
end
describe
'#reopen'
do
let
(
:user
)
{
create
(
:user
)
}
subject
(
:epic
)
{
create
(
:epic
,
state:
'closed'
,
closed_at:
Time
.
now
,
closed_by:
user
)
}
it
'sets closed_at to nil when an epic is reopend'
do
...
...
ee/spec/models/issue_spec.rb
View file @
197d88f6
...
...
@@ -66,6 +66,22 @@ describe Issue do
expect
(
described_class
.
service_desk
).
not_to
include
(
regular_issue
)
end
end
describe
'.in_epics'
do
let_it_be
(
:epic1
)
{
create
(
:epic
)
}
let_it_be
(
:epic2
)
{
create
(
:epic
)
}
let_it_be
(
:epic_issue1
)
{
create
(
:epic_issue
,
epic:
epic1
)
}
let_it_be
(
:epic_issue2
)
{
create
(
:epic_issue
,
epic:
epic2
)
}
before
do
stub_licensed_features
(
epics:
true
)
end
it
'returns only issues in selected epics'
do
expect
(
described_class
.
count
).
to
eq
2
expect
(
described_class
.
in_epics
([
epic1
])).
to
eq
[
epic_issue1
.
issue
]
end
end
end
describe
'validations'
do
...
...
ee/spec/services/epics/descendant_count_service_spec.rb
0 → 100644
View file @
197d88f6
# frozen_string_literal: true
require
'spec_helper'
describe
Epics
::
DescendantCountService
do
let_it_be
(
:group
)
{
create
(
:group
,
:public
)}
let_it_be
(
:subgroup
)
{
create
(
:group
,
:private
,
parent:
group
)}
let_it_be
(
:user
)
{
create
(
:user
)
}
let_it_be
(
:parent_epic
)
{
create
(
:epic
,
group:
group
)
}
let_it_be
(
:epic1
)
{
create
(
:epic
,
group:
subgroup
,
parent:
parent_epic
,
state: :opened
)
}
let_it_be
(
:epic2
)
{
create
(
:epic
,
group:
subgroup
,
parent:
parent_epic
,
state: :closed
)
}
let_it_be
(
:project
)
{
create
(
:project
,
:private
,
group:
group
)}
let_it_be
(
:issue1
)
{
create
(
:issue
,
project:
project
,
state: :opened
)
}
let_it_be
(
:issue2
)
{
create
(
:issue
,
project:
project
,
state: :closed
)
}
let_it_be
(
:issue3
)
{
create
(
:issue
,
project:
project
,
state: :opened
)
}
let_it_be
(
:issue4
)
{
create
(
:issue
,
project:
project
,
state: :closed
)
}
let_it_be
(
:epic_issue1
)
{
create
(
:epic_issue
,
epic:
parent_epic
,
issue:
issue1
)
}
let_it_be
(
:epic_issue2
)
{
create
(
:epic_issue
,
epic:
parent_epic
,
issue:
issue2
)
}
let_it_be
(
:epic_issue3
)
{
create
(
:epic_issue
,
epic:
epic1
,
issue:
issue3
)
}
let_it_be
(
:epic_issue4
)
{
create
(
:epic_issue
,
epic:
epic2
,
issue:
issue4
)
}
subject
{
described_class
.
new
(
parent_epic
,
user
)
}
shared_examples
'descendants state count'
do
|
method
,
expected_count
|
before
do
stub_licensed_features
(
epics:
true
)
end
it
'does not count inaccessible epics'
do
expect
(
subject
.
public_send
(
method
)).
to
eq
0
end
context
'when authorized'
do
before
do
subgroup
.
add_developer
(
user
)
project
.
add_developer
(
user
)
end
it
'returns correct number of epics'
do
expect
(
subject
.
public_send
(
method
)).
to
eq
expected_count
end
end
end
describe
'#opened_epics'
do
it_behaves_like
'descendants state count'
,
:opened_epics
,
1
end
describe
'#closed_epics'
do
it_behaves_like
'descendants state count'
,
:closed_epics
,
1
end
describe
'#opened_issues'
do
it_behaves_like
'descendants state count'
,
:opened_issues
,
2
end
describe
'#closed_issues'
do
it_behaves_like
'descendants state count'
,
:closed_issues
,
2
end
end
Write
Preview
Markdown
is supported
0%
Try again
or
attach a new file
Attach a file
Cancel
You are about to add
0
people
to the discussion. Proceed with caution.
Finish editing this message first!
Cancel
Please
register
or
sign in
to comment