Add latest changes from gitlab-org/gitlab@master
This commit is contained in:
parent
5df6990dac
commit
4f31109a95
38 changed files with 929 additions and 78 deletions
|
@ -1663,6 +1663,7 @@ Gitlab/NamespacedClass:
|
|||
- 'app/policies/service_policy.rb'
|
||||
- 'app/policies/suggestion_policy.rb'
|
||||
- 'app/policies/timebox_policy.rb'
|
||||
- 'app/policies/timelog_policy.rb'
|
||||
- 'app/policies/todo_policy.rb'
|
||||
- 'app/policies/user_policy.rb'
|
||||
- 'app/policies/wiki_page_policy.rb'
|
||||
|
@ -2265,7 +2266,6 @@ Gitlab/NamespacedClass:
|
|||
- 'ee/app/policies/iteration_policy.rb'
|
||||
- 'ee/app/policies/push_rule_policy.rb'
|
||||
- 'ee/app/policies/saml_provider_policy.rb'
|
||||
- 'ee/app/policies/timelog_policy.rb'
|
||||
- 'ee/app/policies/vulnerability_policy.rb'
|
||||
- 'ee/app/presenters/approval_rule_presenter.rb'
|
||||
- 'ee/app/presenters/audit_event_presenter.rb'
|
||||
|
|
|
@ -8,6 +8,7 @@ const PERSISTENT_USER_CALLOUTS = [
|
|||
'.js-token-expiry-callout',
|
||||
'.js-registration-enabled-callout',
|
||||
'.js-new-user-signups-cap-reached',
|
||||
'.js-eoa-bronze-plan-banner',
|
||||
];
|
||||
|
||||
const initCallouts = () => {
|
||||
|
|
112
app/graphql/resolvers/timelog_resolver.rb
Normal file
112
app/graphql/resolvers/timelog_resolver.rb
Normal file
|
@ -0,0 +1,112 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
module Resolvers
|
||||
class TimelogResolver < BaseResolver
|
||||
include LooksAhead
|
||||
|
||||
type ::Types::TimelogType.connection_type, null: false
|
||||
|
||||
argument :start_date, Types::TimeType,
|
||||
required: false,
|
||||
description: 'List time logs within a date range where the logged date is equal to or after startDate.'
|
||||
|
||||
argument :end_date, Types::TimeType,
|
||||
required: false,
|
||||
description: 'List time logs within a date range where the logged date is equal to or before endDate.'
|
||||
|
||||
argument :start_time, Types::TimeType,
|
||||
required: false,
|
||||
description: 'List time-logs within a time range where the logged time is equal to or after startTime.'
|
||||
|
||||
argument :end_time, Types::TimeType,
|
||||
required: false,
|
||||
description: 'List time-logs within a time range where the logged time is equal to or before endTime.'
|
||||
|
||||
def resolve_with_lookahead(**args)
|
||||
return Timelog.none unless timelogs_available_for_user?
|
||||
|
||||
validate_params_presence!(args)
|
||||
transformed_args = transform_args(args)
|
||||
validate_time_difference!(transformed_args)
|
||||
|
||||
find_timelogs(transformed_args)
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def preloads
|
||||
{
|
||||
note: [:note]
|
||||
}
|
||||
end
|
||||
|
||||
def find_timelogs(args)
|
||||
apply_lookahead(group.timelogs(args[:start_time], args[:end_time]))
|
||||
end
|
||||
|
||||
def timelogs_available_for_user?
|
||||
group&.user_can_access_group_timelogs?(context[:current_user])
|
||||
end
|
||||
|
||||
def validate_params_presence!(args)
|
||||
message = case time_params_count(args)
|
||||
when 0
|
||||
'Start and End arguments must be present'
|
||||
when 1
|
||||
'Both Start and End arguments must be present'
|
||||
when 2
|
||||
validate_duplicated_args(args)
|
||||
when 3 || 4
|
||||
'Only Time or Date arguments must be present'
|
||||
end
|
||||
|
||||
raise_argument_error(message) if message
|
||||
end
|
||||
|
||||
def validate_time_difference!(args)
|
||||
message = if args[:end_time] < args[:start_time]
|
||||
'Start argument must be before End argument'
|
||||
elsif args[:end_time] - args[:start_time] > 60.days
|
||||
'The time range period cannot contain more than 60 days'
|
||||
end
|
||||
|
||||
raise_argument_error(message) if message
|
||||
end
|
||||
|
||||
def transform_args(args)
|
||||
return args if args.keys == [:start_time, :end_time]
|
||||
|
||||
time_args = args.except(:start_date, :end_date)
|
||||
|
||||
if time_args.empty?
|
||||
time_args[:start_time] = args[:start_date].beginning_of_day
|
||||
time_args[:end_time] = args[:end_date].end_of_day
|
||||
elsif time_args.key?(:start_time)
|
||||
time_args[:end_time] = args[:end_date].end_of_day
|
||||
elsif time_args.key?(:end_time)
|
||||
time_args[:start_time] = args[:start_date].beginning_of_day
|
||||
end
|
||||
|
||||
time_args
|
||||
end
|
||||
|
||||
def time_params_count(args)
|
||||
[:start_time, :end_time, :start_date, :end_date].count { |param| args.key?(param) }
|
||||
end
|
||||
|
||||
def validate_duplicated_args(args)
|
||||
if args.key?(:start_time) && args.key?(:start_date) ||
|
||||
args.key?(:end_time) && args.key?(:end_date)
|
||||
'Both Start and End arguments must be present'
|
||||
end
|
||||
end
|
||||
|
||||
def raise_argument_error(message)
|
||||
raise Gitlab::Graphql::Errors::ArgumentError, message
|
||||
end
|
||||
|
||||
def group
|
||||
@group ||= object.respond_to?(:sync) ? object.sync : object
|
||||
end
|
||||
end
|
||||
end
|
|
@ -114,6 +114,12 @@ module Types
|
|||
description: 'Labels available on this group.',
|
||||
resolver: Resolvers::GroupLabelsResolver
|
||||
|
||||
field :timelogs, ::Types::TimelogType.connection_type, null: false,
|
||||
description: 'Time logged on issues in the group and its subgroups.',
|
||||
extras: [:lookahead],
|
||||
complexity: 5,
|
||||
resolver: ::Resolvers::TimelogResolver
|
||||
|
||||
def avatar_url
|
||||
object.avatar_url(only_path: false)
|
||||
end
|
||||
|
|
42
app/graphql/types/timelog_type.rb
Normal file
42
app/graphql/types/timelog_type.rb
Normal file
|
@ -0,0 +1,42 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
module Types
|
||||
class TimelogType < BaseObject
|
||||
graphql_name 'Timelog'
|
||||
|
||||
authorize :read_group_timelogs
|
||||
|
||||
field :spent_at,
|
||||
Types::TimeType,
|
||||
null: true,
|
||||
description: 'Timestamp of when the time tracked was spent at.'
|
||||
|
||||
field :time_spent,
|
||||
GraphQL::INT_TYPE,
|
||||
null: false,
|
||||
description: 'The time spent displayed in seconds.'
|
||||
|
||||
field :user,
|
||||
Types::UserType,
|
||||
null: false,
|
||||
description: 'The user that logged the time.'
|
||||
|
||||
field :issue,
|
||||
Types::IssueType,
|
||||
null: true,
|
||||
description: 'The issue that logged time was added to.'
|
||||
|
||||
field :note,
|
||||
Types::Notes::NoteType,
|
||||
null: true,
|
||||
description: 'The note where the quick action to add the logged time was executed.'
|
||||
|
||||
def user
|
||||
Gitlab::Graphql::Loaders::BatchModelLoader.new(User, object.user_id).find
|
||||
end
|
||||
|
||||
def issue
|
||||
Gitlab::Graphql::Loaders::BatchModelLoader.new(Issue, object.issue_id).find
|
||||
end
|
||||
end
|
||||
end
|
19
app/models/concerns/has_timelogs_report.rb
Normal file
19
app/models/concerns/has_timelogs_report.rb
Normal file
|
@ -0,0 +1,19 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
module HasTimelogsReport
|
||||
extend ActiveSupport::Concern
|
||||
|
||||
def timelogs(start_time, end_time)
|
||||
@timelogs ||= timelogs_for(start_time, end_time)
|
||||
end
|
||||
|
||||
def user_can_access_group_timelogs?(current_user)
|
||||
Ability.allowed?(current_user, :read_group_timelogs, self)
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def timelogs_for(start_time, end_time)
|
||||
Timelog.between_times(start_time, end_time).for_issues_in_group(self)
|
||||
end
|
||||
end
|
|
@ -16,6 +16,7 @@ class Group < Namespace
|
|||
include Gitlab::Utils::StrongMemoize
|
||||
include GroupAPICompatibility
|
||||
include EachBatch
|
||||
include HasTimelogsReport
|
||||
|
||||
ACCESS_REQUEST_APPROVERS_TO_BE_NOTIFIED_LIMIT = 10
|
||||
|
||||
|
|
|
@ -130,6 +130,7 @@ class GroupPolicy < BasePolicy
|
|||
enable :read_prometheus
|
||||
enable :read_package
|
||||
enable :read_package_settings
|
||||
enable :read_group_timelogs
|
||||
end
|
||||
|
||||
rule { maintainer }.policy do
|
||||
|
|
|
@ -259,6 +259,7 @@ class ProjectPolicy < BasePolicy
|
|||
enable :read_confidential_issues
|
||||
enable :read_package
|
||||
enable :read_product_analytics
|
||||
enable :read_group_timelogs
|
||||
end
|
||||
|
||||
# We define `:public_user_access` separately because there are cases in gitlab-ee
|
||||
|
|
5
app/policies/timelog_policy.rb
Normal file
5
app/policies/timelog_policy.rb
Normal file
|
@ -0,0 +1,5 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
class TimelogPolicy < BasePolicy
|
||||
delegate { @subject.issuable.resource_parent }
|
||||
end
|
|
@ -22,7 +22,7 @@
|
|||
- if impersonation_enabled? && @user != current_user && @user.can?(:log_in)
|
||||
= link_to 'Impersonate', impersonate_admin_user_path(@user), method: :post, class: "btn btn-info gl-button btn-grouped", data: { qa_selector: 'impersonate_user_link' }
|
||||
= link_to edit_admin_user_path(@user), class: "btn btn-default gl-button btn-grouped" do
|
||||
= sprite_icon('pencil-square', css_class: 'gl-icon')
|
||||
= sprite_icon('pencil-square', css_class: 'gl-icon gl-button-icon')
|
||||
= _('Edit')
|
||||
%hr
|
||||
%ul.nav-links.nav.nav-tabs
|
||||
|
|
|
@ -27,4 +27,4 @@
|
|||
|
||||
.note-search-result
|
||||
.term
|
||||
= search_md_sanitize(note.note)
|
||||
= simple_search_highlight_and_truncate(note.note, @search_term)
|
||||
|
|
|
@ -0,0 +1,5 @@
|
|||
---
|
||||
title: Remove markdown from comment search result
|
||||
merge_request: 55255
|
||||
author:
|
||||
type: other
|
5
changelogs/unreleased/move-graphql-timelogs-to-ce.yml
Normal file
5
changelogs/unreleased/move-graphql-timelogs-to-ce.yml
Normal file
|
@ -0,0 +1,5 @@
|
|||
---
|
||||
title: Move graphql timelogs to CE
|
||||
merge_request: 56633
|
||||
author: Lee Tickett @leetickett
|
||||
type: fixed
|
5
changelogs/unreleased/update-admin-edit-button-icon.yml
Normal file
5
changelogs/unreleased/update-admin-edit-button-icon.yml
Normal file
|
@ -0,0 +1,5 @@
|
|||
---
|
||||
title: Update admin edit button icon class
|
||||
merge_request: 57151
|
||||
author:
|
||||
type: fixed
|
Binary file not shown.
Before Width: | Height: | Size: 42 KiB |
|
@ -7,20 +7,45 @@ type: reference
|
|||
|
||||
# Gitaly
|
||||
|
||||
[Gitaly](https://gitlab.com/gitlab-org/gitaly) is the service that provides high-level RPC access to
|
||||
Git repositories. Without it, no GitLab components can read or write Git data.
|
||||
[Gitaly](https://gitlab.com/gitlab-org/gitaly) provides high-level RPC access to Git repositories.
|
||||
It is used by GitLab to read and write Git data.
|
||||
|
||||
In the Gitaly documentation:
|
||||
Gitaly implements a client-server architecture:
|
||||
|
||||
- **Gitaly server** refers to any node that runs Gitaly itself.
|
||||
- **Gitaly client** refers to any node that runs a process that makes requests of the
|
||||
Gitaly server. Processes include, but are not limited to:
|
||||
- A Gitaly server is any node that runs Gitaly itself.
|
||||
- A Gitaly client is any node that runs a process that makes requests of the Gitaly server. These
|
||||
include, but are not limited to:
|
||||
- [GitLab Rails application](https://gitlab.com/gitlab-org/gitlab).
|
||||
- [GitLab Shell](https://gitlab.com/gitlab-org/gitlab-shell).
|
||||
- [GitLab Workhorse](https://gitlab.com/gitlab-org/gitlab-workhorse).
|
||||
|
||||
GitLab end users do not have direct access to Gitaly. Gitaly manages only Git
|
||||
repository access for GitLab. Other types of GitLab data aren't accessed using Gitaly.
|
||||
The following illustrates the Gitaly client-server architecture:
|
||||
|
||||
```mermaid
|
||||
flowchart TD
|
||||
subgraph Gitaly clients
|
||||
A[GitLab Rails]
|
||||
B[GitLab Workhorse]
|
||||
C[GitLab Shell]
|
||||
D[...]
|
||||
end
|
||||
|
||||
subgraph Gitaly
|
||||
E[Git integration]
|
||||
end
|
||||
|
||||
F[Local filesystem]
|
||||
|
||||
A -- gRPC --> Gitaly
|
||||
B -- gRPC--> Gitaly
|
||||
C -- gRPC --> Gitaly
|
||||
D -- gRPC --> Gitaly
|
||||
|
||||
E --> F
|
||||
```
|
||||
|
||||
End users do not have direct access to Gitaly. Gitaly manages only Git repository access for GitLab.
|
||||
Other types of GitLab data aren't accessed using Gitaly.
|
||||
|
||||
<!-- vale gitlab.FutureTense = NO -->
|
||||
|
||||
|
@ -31,45 +56,47 @@ considered and customer technical support will be considered out of scope.
|
|||
|
||||
<!-- vale gitlab.FutureTense = YES -->
|
||||
|
||||
## Architecture
|
||||
|
||||
The following is a high-level architecture overview of how Gitaly is used.
|
||||
|
||||
![Gitaly architecture diagram](img/architecture_v12_4.png)
|
||||
|
||||
## Configure Gitaly
|
||||
|
||||
Gitaly comes pre-configured with Omnibus GitLab. For more information on customizing your
|
||||
Gitaly installation, see [Configure Gitaly](configure_gitaly.md).
|
||||
Gitaly comes pre-configured with Omnibus GitLab, which is a configuration
|
||||
[suitable for up to 1000 users](../reference_architectures/1k_users.md). For:
|
||||
|
||||
## Direct Git access bypassing Gitaly
|
||||
- Omnibus GitLab installations for up to 2000 users, see [specific Gitaly configuration instructions](../reference_architectures/2k_users.md#configure-gitaly).
|
||||
- Source installations or custom Gitaly installations, see [Configure Gitaly](#configure-gitaly).
|
||||
|
||||
GitLab doesn't advise directly accessing Gitaly repositories stored on disk with
|
||||
a Git client, because Gitaly is being continuously improved and changed. These
|
||||
improvements may invalidate assumptions, resulting in performance degradation, instability, and even data loss.
|
||||
GitLab installations for more than 2000 users should use Gitaly Cluster.
|
||||
|
||||
Gitaly has optimizations, such as the
|
||||
[`info/refs` advertisement cache](https://gitlab.com/gitlab-org/gitaly/blob/master/doc/design_diskcache.md),
|
||||
that rely on Gitaly controlling and monitoring access to repositories by using the
|
||||
official gRPC interface. Likewise, Praefect has optimizations, such as fault
|
||||
tolerance and distributed reads, that depend on the gRPC interface and
|
||||
database to determine repository state.
|
||||
## Gitaly Cluster
|
||||
|
||||
For these reasons, **accessing repositories directly is done at your own risk
|
||||
and is not supported**.
|
||||
Gitaly can run in a clustered configuration to scale Gitaly and increase fault tolerance. For more
|
||||
information, see [Gitaly Cluster](praefect.md).
|
||||
|
||||
## Do not bypass Gitaly
|
||||
|
||||
GitLab doesn't advise directly accessing Gitaly repositories stored on disk with a Git client,
|
||||
because Gitaly is being continuously improved and changed. These improvements may invalidate
|
||||
your assumptions, resulting in performance degradation, instability, and even data loss. For example:
|
||||
|
||||
- Gitaly has optimizations such as the [`info/refs` advertisement cache](https://gitlab.com/gitlab-org/gitaly/blob/master/doc/design_diskcache.md),
|
||||
that rely on Gitaly controlling and monitoring access to repositories by using the official gRPC
|
||||
interface.
|
||||
- [Gitaly Cluster](praefect.md) has optimizations, such as fault tolerance and
|
||||
[distributed reads](praefect.md#distributed-reads), that depend on the gRPC interface and database
|
||||
to determine repository state.
|
||||
|
||||
WARNING:
|
||||
Accessing Git repositories directly is done at your own risk and is not supported.
|
||||
|
||||
## Direct access to Git in GitLab
|
||||
|
||||
Direct access to Git uses code in GitLab known as the "Rugged patches".
|
||||
|
||||
### History
|
||||
Before Gitaly existed, what are now Gitaly clients accessed Git repositories directly, either:
|
||||
|
||||
Before Gitaly existed, what are now Gitaly clients used to access Git repositories directly, either:
|
||||
|
||||
- On a local disk in the case of a single-machine Omnibus GitLab installation
|
||||
- On a local disk in the case of a single-machine Omnibus GitLab installation.
|
||||
- Using NFS in the case of a horizontally-scaled GitLab installation.
|
||||
|
||||
Besides running plain `git` commands, GitLab used to use a Ruby library called
|
||||
In addition to running plain `git` commands, GitLab used a Ruby library called
|
||||
[Rugged](https://github.com/libgit2/rugged). Rugged is a wrapper around
|
||||
[libgit2](https://libgit2.org/), a stand-alone implementation of Git in the form of a C library.
|
||||
|
||||
|
@ -80,9 +107,9 @@ not an external process, there was very little overhead between:
|
|||
- GitLab application code that tried to look up data in Git repositories.
|
||||
- The Git implementation itself.
|
||||
|
||||
Because the combination of Rugged and Unicorn was so efficient, the GitLab application code ended up with lots of
|
||||
duplicate Git object lookups. For example, looking up the `master` commit a dozen times in one
|
||||
request. We could write inefficient code without poor performance.
|
||||
Because the combination of Rugged and Unicorn was so efficient, the GitLab application code ended up
|
||||
with lots of duplicate Git object lookups. For example, looking up the default branch commit a dozen
|
||||
times in one request. We could write inefficient code without poor performance.
|
||||
|
||||
When we migrated these Git lookups to Gitaly calls, we suddenly had a much higher fixed cost per Git
|
||||
lookup. Even when Gitaly is able to re-use an already-running `git` process (for example, to look up
|
||||
|
@ -93,8 +120,8 @@ a commit), you still have:
|
|||
|
||||
Using GitLab.com to measure, we reduced the number of Gitaly calls per request until the loss of
|
||||
Rugged's efficiency was no longer felt. It also helped that we run Gitaly itself directly on the Git
|
||||
file severs, rather than by using NFS mounts. This gave us a speed boost that counteracted the negative
|
||||
effect of not using Rugged anymore.
|
||||
file servers, rather than by using NFS mounts. This gave us a speed boost that counteracted the
|
||||
negative effect of not using Rugged anymore.
|
||||
|
||||
Unfortunately, other deployments of GitLab could not remove NFS like we did on GitLab.com, and they
|
||||
got the worst of both worlds:
|
||||
|
@ -261,9 +288,9 @@ so, there's not that much visibility into what goes on inside
|
|||
If you have Prometheus set up to scrape your Gitaly process, you can see
|
||||
request rates and error codes for individual RPCs in `gitaly-ruby` by
|
||||
querying `grpc_client_handled_total`. Strictly speaking, this metric does
|
||||
not differentiate between `gitaly-ruby` and other RPCs, but in practice
|
||||
(as of GitLab 11.9), all gRPC calls made by Gitaly itself are internal
|
||||
calls from the main Gitaly process to one of its `gitaly-ruby` sidecars.
|
||||
not differentiate between `gitaly-ruby` and other RPCs. However from GitLab 11.9,
|
||||
all gRPC calls made by Gitaly itself are internal calls from the main Gitaly process to one of its
|
||||
`gitaly-ruby` sidecars.
|
||||
|
||||
Assuming your `grpc_client_handled_total` counter observes only Gitaly,
|
||||
the following query shows you RPCs are (most likely) internally
|
||||
|
@ -370,9 +397,10 @@ update the secrets file on the Gitaly server to match the Gitaly client, then
|
|||
|
||||
### Command line tools cannot connect to Gitaly
|
||||
|
||||
If you can't connect to a Gitaly server with command line (CLI) tools,
|
||||
and certain actions result in a `14: Connect Failed` error message,
|
||||
gRPC cannot reach your Gitaly server.
|
||||
gRPC cannot reach your Gitaly server if:
|
||||
|
||||
- You can't connect to a Gitaly server with command-line tools.
|
||||
- Certain actions result in a `14: Connect Failed` error message.
|
||||
|
||||
Verify you can reach Gitaly by using TCP:
|
||||
|
||||
|
@ -412,8 +440,3 @@ the Gitaly server is experiencing
|
|||
|
||||
Ensure the Gitaly clients and servers are synchronized, and use an NTP time
|
||||
server to keep them synchronized, if possible.
|
||||
|
||||
### Praefect
|
||||
|
||||
Praefect is a router and transaction manager for Gitaly, and a required
|
||||
component for running a Gitaly Cluster. For more information see [Gitaly Cluster](praefect.md).
|
||||
|
|
|
@ -189,7 +189,7 @@ The following table outlines the major differences between Gitaly Cluster and Ge
|
|||
|
||||
For more information, see:
|
||||
|
||||
- [Gitaly architecture](index.md#architecture).
|
||||
- [Gitaly](index.md).
|
||||
- Geo [use cases](../geo/index.md#use-cases) and [architecture](../geo/index.md#architecture).
|
||||
|
||||
## Architecture
|
||||
|
|
|
@ -3065,7 +3065,6 @@ Autogenerated return type of GitlabSubscriptionActivate.
|
|||
| `fullName` | [`String!`](#string) | Full name of the namespace. |
|
||||
| `fullPath` | [`ID!`](#id) | Full path of the namespace. |
|
||||
| `groupMembers` | [`GroupMemberConnection`](#groupmemberconnection) | A membership of a user within this group. |
|
||||
| `groupTimelogsEnabled` | [`Boolean`](#boolean) | Indicates if Group timelogs are enabled for namespace |
|
||||
| `id` | [`ID!`](#id) | ID of the namespace. |
|
||||
| `isTemporaryStorageIncreaseEnabled` | [`Boolean!`](#boolean) | Status of the temporary storage increase. |
|
||||
| `issues` | [`IssueConnection`](#issueconnection) | Issues for projects in this group. |
|
||||
|
@ -3093,7 +3092,7 @@ Autogenerated return type of GitlabSubscriptionActivate.
|
|||
| `storageSizeLimit` | [`Float`](#float) | Total storage limit of the root namespace in bytes. |
|
||||
| `subgroupCreationLevel` | [`String`](#string) | The permission level required to create subgroups within the group. |
|
||||
| `temporaryStorageIncreaseEndsOn` | [`Time`](#time) | Date until the temporary storage increase is active. |
|
||||
| `timelogs` | [`TimelogConnection!`](#timelogconnection) | Time logged in issues by group members. |
|
||||
| `timelogs` | [`TimelogConnection!`](#timelogconnection) | Time logged on issues in the group and its subgroups. |
|
||||
| `totalRepositorySize` | [`Float`](#float) | Total repository size of all projects in the root namespace in bytes. |
|
||||
| `totalRepositorySizeExcess` | [`Float`](#float) | Total excess repository size of all projects in the root namespace in bytes. |
|
||||
| `twoFactorGracePeriod` | [`Int`](#int) | Time before two-factor authentication is enforced. |
|
||||
|
|
|
@ -9788,6 +9788,30 @@ Status: `implemented`
|
|||
|
||||
Tiers: `free`, `premium`, `ultimate`
|
||||
|
||||
### `redis_hll_counters.epics_usage.epics_usage_total_unique_counts_monthly`
|
||||
|
||||
Total monthly users count for epics_usage
|
||||
|
||||
[YAML definition](https://gitlab.com/gitlab-org/gitlab/-/blob/master/ee/config/metrics/counts_28d/20210318183733_epics_usage_total_unique_counts_monthly.yml)
|
||||
|
||||
Group: `group::product planning`
|
||||
|
||||
Status: `implemented`
|
||||
|
||||
Tiers: `premium`, `ultimate`
|
||||
|
||||
### `redis_hll_counters.epics_usage.epics_usage_total_unique_counts_weekly`
|
||||
|
||||
Total weekly users count for epics_usage
|
||||
|
||||
[YAML definition](https://gitlab.com/gitlab-org/gitlab/-/blob/master/ee/config/metrics/counts_7d/20210318183220_epics_usage_total_unique_counts_weekly.yml)
|
||||
|
||||
Group: `group::product planning`
|
||||
|
||||
Status: `implemented`
|
||||
|
||||
Tiers: `premium`, `ultimate`
|
||||
|
||||
### `redis_hll_counters.epics_usage.g_project_management_epic_created_monthly`
|
||||
|
||||
Count of MAU creating epics
|
||||
|
|
|
@ -24,6 +24,12 @@ module Gitlab
|
|||
Range.new(self.begin, self.end, self.exclude_end?)
|
||||
end
|
||||
|
||||
def ==(other)
|
||||
return false unless other.is_a?(self.class)
|
||||
|
||||
self.mode == other.mode && super
|
||||
end
|
||||
|
||||
attr_reader :mode
|
||||
end
|
||||
end
|
||||
|
|
|
@ -18,6 +18,27 @@ module Gitlab
|
|||
def reset
|
||||
@chunks = []
|
||||
end
|
||||
|
||||
def marker_ranges
|
||||
start = 0
|
||||
|
||||
@chunks.each_with_object([]) do |element, ranges|
|
||||
mode = mode_for_element(element)
|
||||
|
||||
ranges << Gitlab::MarkerRange.new(start, start + element.length - 1, mode: mode) if mode
|
||||
|
||||
start += element.length
|
||||
end
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def mode_for_element(element)
|
||||
return Gitlab::MarkerRange::DELETION if element.removed?
|
||||
return Gitlab::MarkerRange::ADDITION if element.added?
|
||||
|
||||
nil
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
|
@ -31,7 +31,7 @@ module Gitlab
|
|||
@chunks.add(segment)
|
||||
|
||||
when Segments::Newline
|
||||
yielder << build_line(@chunks.content, nil, parent_file: diff_file)
|
||||
yielder << build_line(@chunks.content, nil, parent_file: diff_file).tap { |line| line.set_marker_ranges(@chunks.marker_ranges) }
|
||||
|
||||
@chunks.reset
|
||||
counter.increase_pos_num
|
||||
|
|
161
spec/graphql/resolvers/timelog_resolver_spec.rb
Normal file
161
spec/graphql/resolvers/timelog_resolver_spec.rb
Normal file
|
@ -0,0 +1,161 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
require 'spec_helper'
|
||||
|
||||
RSpec.describe Resolvers::TimelogResolver do
|
||||
include GraphqlHelpers
|
||||
|
||||
specify do
|
||||
expect(described_class).to have_non_null_graphql_type(::Types::TimelogType.connection_type)
|
||||
end
|
||||
|
||||
context "within a group" do
|
||||
let_it_be(:current_user) { create(:user) }
|
||||
let(:group) { create(:group) }
|
||||
let(:project) { create(:project, :public, group: group) }
|
||||
|
||||
before do
|
||||
group.add_developer(current_user)
|
||||
project.add_developer(current_user)
|
||||
end
|
||||
|
||||
describe '#resolve' do
|
||||
let(:issue) { create(:issue, project: project) }
|
||||
let(:issue2) { create(:issue, project: project) }
|
||||
let(:args) { { start_time: 6.days.ago, end_time: 2.days.ago.noon } }
|
||||
let!(:timelog1) { create(:timelog, issue: issue, spent_at: 2.days.ago.beginning_of_day) }
|
||||
let!(:timelog2) { create(:timelog, issue: issue2, spent_at: 2.days.ago.end_of_day) }
|
||||
let!(:timelog3) { create(:timelog, issue: issue2, spent_at: 10.days.ago) }
|
||||
|
||||
it 'finds all timelogs within given dates' do
|
||||
timelogs = resolve_timelogs(args)
|
||||
|
||||
expect(timelogs).to contain_exactly(timelog1)
|
||||
end
|
||||
|
||||
it 'return nothing when user has insufficient permissions' do
|
||||
group.add_guest(current_user)
|
||||
|
||||
expect(resolve_timelogs(args)).to be_empty
|
||||
end
|
||||
|
||||
context 'when start_time and end_date are present' do
|
||||
let(:args) { { start_time: 6.days.ago, end_date: 2.days.ago } }
|
||||
|
||||
it 'finds timelogs until the end of day of end_date' do
|
||||
timelogs = resolve_timelogs(args)
|
||||
|
||||
expect(timelogs).to contain_exactly(timelog1, timelog2)
|
||||
end
|
||||
end
|
||||
|
||||
context 'finds timelogs until the time specified on end_time' do
|
||||
let(:args) { { start_date: 6.days.ago, end_time: 2.days.ago.noon } }
|
||||
|
||||
it 'finds all timelogs within start_date and end_time' do
|
||||
timelogs = resolve_timelogs(args)
|
||||
|
||||
expect(timelogs).to contain_exactly(timelog1)
|
||||
end
|
||||
end
|
||||
|
||||
context 'when arguments are invalid' do
|
||||
let_it_be(:error_class) { Gitlab::Graphql::Errors::ArgumentError }
|
||||
|
||||
context 'when no time or date arguments are present' do
|
||||
let(:args) { {} }
|
||||
|
||||
it 'returns correct error' do
|
||||
expect {resolve_timelogs(args)}
|
||||
.to raise_error(error_class, /Start and End arguments must be present/)
|
||||
end
|
||||
end
|
||||
|
||||
context 'when only start_time is present' do
|
||||
let(:args) { { start_time: 6.days.ago } }
|
||||
|
||||
it 'returns correct error' do
|
||||
expect {resolve_timelogs(args)}
|
||||
.to raise_error(error_class, /Both Start and End arguments must be present/)
|
||||
end
|
||||
end
|
||||
|
||||
context 'when only end_time is present' do
|
||||
let(:args) { { end_time: 2.days.ago } }
|
||||
|
||||
it 'returns correct error' do
|
||||
expect {resolve_timelogs(args)}
|
||||
.to raise_error(error_class, /Both Start and End arguments must be present/)
|
||||
end
|
||||
end
|
||||
|
||||
context 'when only start_date is present' do
|
||||
let(:args) { { start_date: 6.days.ago } }
|
||||
|
||||
it 'returns correct error' do
|
||||
expect {resolve_timelogs(args)}
|
||||
.to raise_error(error_class, /Both Start and End arguments must be present/)
|
||||
end
|
||||
end
|
||||
|
||||
context 'when only end_date is present' do
|
||||
let(:args) { { end_date: 2.days.ago } }
|
||||
|
||||
it 'returns correct error' do
|
||||
expect {resolve_timelogs(args)}
|
||||
.to raise_error(error_class, /Both Start and End arguments must be present/)
|
||||
end
|
||||
end
|
||||
|
||||
context 'when start_time and start_date are present' do
|
||||
let(:args) { { start_time: 6.days.ago, start_date: 6.days.ago } }
|
||||
|
||||
it 'returns correct error' do
|
||||
expect {resolve_timelogs(args)}
|
||||
.to raise_error(error_class, /Both Start and End arguments must be present/)
|
||||
end
|
||||
end
|
||||
|
||||
context 'when end_time and end_date are present' do
|
||||
let(:args) { { end_time: 2.days.ago, end_date: 2.days.ago } }
|
||||
|
||||
it 'returns correct error' do
|
||||
expect {resolve_timelogs(args)}
|
||||
.to raise_error(error_class, /Both Start and End arguments must be present/)
|
||||
end
|
||||
end
|
||||
|
||||
context 'when three arguments are present' do
|
||||
let(:args) { { start_date: 6.days.ago, end_date: 2.days.ago, end_time: 2.days.ago } }
|
||||
|
||||
it 'returns correct error' do
|
||||
expect {resolve_timelogs(args)}
|
||||
.to raise_error(error_class, /Only Time or Date arguments must be present/)
|
||||
end
|
||||
end
|
||||
|
||||
context 'when start argument is after end argument' do
|
||||
let(:args) { { start_time: 2.days.ago, end_time: 6.days.ago } }
|
||||
|
||||
it 'returns correct error' do
|
||||
expect {resolve_timelogs(args)}
|
||||
.to raise_error(error_class, /Start argument must be before End argument/)
|
||||
end
|
||||
end
|
||||
|
||||
context 'when time range is more than 60 days' do
|
||||
let(:args) { { start_time: 3.months.ago, end_time: 2.days.ago } }
|
||||
|
||||
it 'returns correct error' do
|
||||
expect {resolve_timelogs(args)}
|
||||
.to raise_error(error_class, /The time range period cannot contain more than 60 days/)
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
def resolve_timelogs(args = {}, context = { current_user: current_user })
|
||||
resolve(described_class, obj: group, args: args, ctx: context)
|
||||
end
|
||||
end
|
35
spec/graphql/types/timelog_type_spec.rb
Normal file
35
spec/graphql/types/timelog_type_spec.rb
Normal file
|
@ -0,0 +1,35 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
require 'spec_helper'
|
||||
|
||||
RSpec.describe GitlabSchema.types['Timelog'] do
|
||||
let(:fields) { %i[spent_at time_spent user issue note] }
|
||||
|
||||
it { expect(described_class.graphql_name).to eq('Timelog') }
|
||||
it { expect(described_class).to have_graphql_fields(fields) }
|
||||
it { expect(described_class).to require_graphql_authorizations(:read_group_timelogs) }
|
||||
|
||||
describe 'user field' do
|
||||
subject { described_class.fields['user'] }
|
||||
|
||||
it 'returns user' do
|
||||
is_expected.to have_non_null_graphql_type(Types::UserType)
|
||||
end
|
||||
end
|
||||
|
||||
describe 'issue field' do
|
||||
subject { described_class.fields['issue'] }
|
||||
|
||||
it 'returns issue' do
|
||||
is_expected.to have_graphql_type(Types::IssueType)
|
||||
end
|
||||
end
|
||||
|
||||
describe 'note field' do
|
||||
subject { described_class.fields['note'] }
|
||||
|
||||
it 'returns note' do
|
||||
is_expected.to have_graphql_type(Types::Notes::NoteType)
|
||||
end
|
||||
end
|
||||
end
|
|
@ -49,15 +49,15 @@ RSpec.describe Gitlab::Diff::CharDiff do
|
|||
old_diffs, new_diffs = subject
|
||||
|
||||
expect(old_diffs).to eq([])
|
||||
expect(new_diffs).to eq([0..12])
|
||||
expect(new_diffs).to eq([Gitlab::MarkerRange.new(0, 12, mode: :addition)])
|
||||
end
|
||||
end
|
||||
|
||||
it 'returns ranges of changes' do
|
||||
old_diffs, new_diffs = subject
|
||||
|
||||
expect(old_diffs).to eq([11..11])
|
||||
expect(new_diffs).to eq([3..3])
|
||||
expect(old_diffs).to eq([Gitlab::MarkerRange.new(11, 11, mode: :deletion)])
|
||||
expect(new_diffs).to eq([Gitlab::MarkerRange.new(3, 3, mode: :addition)])
|
||||
end
|
||||
end
|
||||
|
||||
|
|
|
@ -9,7 +9,7 @@ RSpec.describe Gitlab::MarkerRange do
|
|||
let(:last) { 10 }
|
||||
let(:mode) { nil }
|
||||
|
||||
it { is_expected.to eq(first..last) }
|
||||
it { expect(marker_range.to_range).to eq(first..last) }
|
||||
|
||||
it 'behaves like a Range' do
|
||||
is_expected.to be_kind_of(Range)
|
||||
|
@ -51,14 +51,14 @@ RSpec.describe Gitlab::MarkerRange do
|
|||
end
|
||||
|
||||
it 'keeps correct range' do
|
||||
is_expected.to eq(range)
|
||||
is_expected.to eq(described_class.new(1, 3))
|
||||
end
|
||||
|
||||
context 'when range excludes end' do
|
||||
let(:range) { 1...3 }
|
||||
|
||||
it 'keeps correct range' do
|
||||
is_expected.to eq(range)
|
||||
is_expected.to eq(described_class.new(1, 3, exclude_end: true))
|
||||
end
|
||||
end
|
||||
|
||||
|
@ -68,4 +68,31 @@ RSpec.describe Gitlab::MarkerRange do
|
|||
it { is_expected.to be(marker_range) }
|
||||
end
|
||||
end
|
||||
|
||||
describe '#==' do
|
||||
subject { default_marker_range == another_marker_range }
|
||||
|
||||
let(:default_marker_range) { described_class.new(0, 1, mode: :addition) }
|
||||
let(:another_marker_range) { default_marker_range }
|
||||
|
||||
it { is_expected.to be_truthy }
|
||||
|
||||
context 'when marker ranges have different modes' do
|
||||
let(:another_marker_range) { described_class.new(0, 1, mode: :deletion) }
|
||||
|
||||
it { is_expected.to be_falsey }
|
||||
end
|
||||
|
||||
context 'when marker ranges have different ranges' do
|
||||
let(:another_marker_range) { described_class.new(0, 2, mode: :addition) }
|
||||
|
||||
it { is_expected.to be_falsey }
|
||||
end
|
||||
|
||||
context 'when marker ranges is a simple range' do
|
||||
let(:another_marker_range) { (0..1) }
|
||||
|
||||
it { is_expected.to be_falsey }
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
|
@ -41,4 +41,27 @@ RSpec.describe Gitlab::WordDiff::ChunkCollection do
|
|||
expect(collection.content).to eq('')
|
||||
end
|
||||
end
|
||||
|
||||
describe '#marker_ranges' do
|
||||
let(:chunks) do
|
||||
[
|
||||
Gitlab::WordDiff::Segments::Chunk.new(' Hello '),
|
||||
Gitlab::WordDiff::Segments::Chunk.new('-World'),
|
||||
Gitlab::WordDiff::Segments::Chunk.new('+GitLab'),
|
||||
Gitlab::WordDiff::Segments::Chunk.new('+!!!')
|
||||
]
|
||||
end
|
||||
|
||||
it 'returns marker ranges for every chunk with changes' do
|
||||
chunks.each { |chunk| collection.add(chunk) }
|
||||
|
||||
expect(collection.marker_ranges).to eq(
|
||||
[
|
||||
Gitlab::MarkerRange.new(6, 10, mode: :deletion),
|
||||
Gitlab::MarkerRange.new(11, 16, mode: :addition),
|
||||
Gitlab::MarkerRange.new(17, 19, mode: :addition)
|
||||
]
|
||||
)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
|
@ -36,15 +36,26 @@ RSpec.describe Gitlab::WordDiff::Parser do
|
|||
aggregate_failures do
|
||||
expect(diff_lines.count).to eq(7)
|
||||
|
||||
expect(diff_lines.map(&:to_hash)).to match_array(
|
||||
expect(diff_lines.map { |line| diff_line_attributes(line) }).to eq(
|
||||
[
|
||||
a_hash_including(index: 0, old_pos: 1, new_pos: 1, text: '', type: nil),
|
||||
a_hash_including(index: 1, old_pos: 2, new_pos: 2, text: 'Unchanged line', type: nil),
|
||||
a_hash_including(index: 2, old_pos: 3, new_pos: 3, text: '', type: nil),
|
||||
a_hash_including(index: 3, old_pos: 4, new_pos: 4, text: 'Old changeNew addition unchanged content', type: nil),
|
||||
a_hash_including(index: 4, old_pos: 50, new_pos: 50, text: '@@ -50,14 +50,13 @@', type: 'match'),
|
||||
a_hash_including(index: 5, old_pos: 50, new_pos: 50, text: 'First change same same same_removed_added_end of the line', type: nil),
|
||||
a_hash_including(index: 6, old_pos: 51, new_pos: 51, text: '', type: nil)
|
||||
{ index: 0, old_pos: 1, new_pos: 1, text: '', type: nil, marker_ranges: [] },
|
||||
{ index: 1, old_pos: 2, new_pos: 2, text: 'Unchanged line', type: nil, marker_ranges: [] },
|
||||
{ index: 2, old_pos: 3, new_pos: 3, text: '', type: nil, marker_ranges: [] },
|
||||
{ index: 3, old_pos: 4, new_pos: 4, text: 'Old changeNew addition unchanged content', type: nil,
|
||||
marker_ranges: [
|
||||
Gitlab::MarkerRange.new(0, 9, mode: :deletion),
|
||||
Gitlab::MarkerRange.new(10, 21, mode: :addition)
|
||||
] },
|
||||
|
||||
{ index: 4, old_pos: 50, new_pos: 50, text: '@@ -50,14 +50,13 @@', type: 'match', marker_ranges: [] },
|
||||
{ index: 5, old_pos: 50, new_pos: 50, text: 'First change same same same_removed_added_end of the line', type: nil,
|
||||
marker_ranges: [
|
||||
Gitlab::MarkerRange.new(0, 11, mode: :addition),
|
||||
Gitlab::MarkerRange.new(28, 35, mode: :deletion),
|
||||
Gitlab::MarkerRange.new(36, 41, mode: :addition)
|
||||
] },
|
||||
|
||||
{ index: 6, old_pos: 51, new_pos: 51, text: '', type: nil, marker_ranges: [] }
|
||||
]
|
||||
)
|
||||
end
|
||||
|
@ -64,4 +75,17 @@ RSpec.describe Gitlab::WordDiff::Parser do
|
|||
it { is_expected.to eq([]) }
|
||||
end
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def diff_line_attributes(diff_line)
|
||||
{
|
||||
index: diff_line.index,
|
||||
old_pos: diff_line.old_pos,
|
||||
new_pos: diff_line.new_pos,
|
||||
text: diff_line.text,
|
||||
type: diff_line.type,
|
||||
marker_ranges: diff_line.marker_ranges
|
||||
}
|
||||
end
|
||||
end
|
||||
|
|
|
@ -95,14 +95,13 @@ RSpec.describe 'Marginalia spec' do
|
|||
# have to do some extra steps to make this happen:
|
||||
# https://github.com/rails/rails/issues/37270#issuecomment-553927324
|
||||
around do |example|
|
||||
original_queue_adapter = ActiveJob::Base.queue_adapter
|
||||
descendants = ActiveJob::Base.descendants + [ActiveJob::Base]
|
||||
|
||||
ActiveJob::Base.queue_adapter = :sidekiq
|
||||
descendants.each(&:disable_test_adapter)
|
||||
ActiveJob::Base.queue_adapter = :sidekiq
|
||||
|
||||
example.run
|
||||
descendants.each { |a| a.enable_test_adapter(ActiveJob::QueueAdapters::TestAdapter.new) }
|
||||
ActiveJob::Base.queue_adapter = original_queue_adapter
|
||||
|
||||
descendants.each { |a| a.queue_adapter = :test }
|
||||
end
|
||||
|
||||
let(:delivery_job) { MarginaliaTestMailer.first_user.deliver_later }
|
||||
|
|
53
spec/models/concerns/has_timelogs_report_spec.rb
Normal file
53
spec/models/concerns/has_timelogs_report_spec.rb
Normal file
|
@ -0,0 +1,53 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
require 'spec_helper'
|
||||
|
||||
RSpec.describe HasTimelogsReport do
|
||||
let(:user) { create(:user) }
|
||||
let(:group) { create(:group) }
|
||||
let(:issue) { create(:issue, project: create(:project, :public, group: group)) }
|
||||
|
||||
describe '#timelogs' do
|
||||
let!(:timelog1) { create_timelog(15.days.ago) }
|
||||
let!(:timelog2) { create_timelog(10.days.ago) }
|
||||
let!(:timelog3) { create_timelog(5.days.ago) }
|
||||
let(:start_time) { 20.days.ago }
|
||||
let(:end_time) { 8.days.ago }
|
||||
|
||||
before do
|
||||
group.add_developer(user)
|
||||
end
|
||||
|
||||
it 'returns collection of timelogs between given times' do
|
||||
expect(group.timelogs(start_time, end_time).to_a).to match_array([timelog1, timelog2])
|
||||
end
|
||||
|
||||
it 'returns empty collection if times are not present' do
|
||||
expect(group.timelogs(nil, nil)).to be_empty
|
||||
end
|
||||
|
||||
it 'returns empty collection if time range is invalid' do
|
||||
expect(group.timelogs(end_time, start_time)).to be_empty
|
||||
end
|
||||
end
|
||||
|
||||
describe '#user_can_access_group_timelogs?' do
|
||||
before do
|
||||
group.add_developer(user)
|
||||
end
|
||||
|
||||
it 'returns true if user can access group timelogs' do
|
||||
expect(group.user_can_access_group_timelogs?(user)).to be_truthy
|
||||
end
|
||||
|
||||
it 'returns false if user has insufficient permissions' do
|
||||
group.add_guest(user)
|
||||
|
||||
expect(group.user_can_access_group_timelogs?(user)).to be_falsey
|
||||
end
|
||||
end
|
||||
|
||||
def create_timelog(time)
|
||||
create(:timelog, issue: issue, user: user, spent_at: time)
|
||||
end
|
||||
end
|
|
@ -922,4 +922,54 @@ RSpec.describe GroupPolicy do
|
|||
it { expect(described_class.new(current_user, subgroup)).to be_allowed(:read_label) }
|
||||
end
|
||||
end
|
||||
|
||||
context 'timelogs' do
|
||||
context 'with admin' do
|
||||
let(:current_user) { admin }
|
||||
|
||||
context 'when admin mode is enabled', :enable_admin_mode do
|
||||
it { is_expected.to be_allowed(:read_group_timelogs) }
|
||||
end
|
||||
|
||||
context 'when admin mode is disabled' do
|
||||
it { is_expected.to be_disallowed(:read_group_timelogs) }
|
||||
end
|
||||
end
|
||||
|
||||
context 'with owner' do
|
||||
let(:current_user) { owner }
|
||||
|
||||
it { is_expected.to be_allowed(:read_group_timelogs) }
|
||||
end
|
||||
|
||||
context 'with maintainer' do
|
||||
let(:current_user) { maintainer }
|
||||
|
||||
it { is_expected.to be_allowed(:read_group_timelogs) }
|
||||
end
|
||||
|
||||
context 'with reporter' do
|
||||
let(:current_user) { reporter }
|
||||
|
||||
it { is_expected.to be_allowed(:read_group_timelogs) }
|
||||
end
|
||||
|
||||
context 'with guest' do
|
||||
let(:current_user) { guest }
|
||||
|
||||
it { is_expected.to be_disallowed(:read_group_timelogs) }
|
||||
end
|
||||
|
||||
context 'with non member' do
|
||||
let(:current_user) { create(:user) }
|
||||
|
||||
it { is_expected.to be_disallowed(:read_group_timelogs) }
|
||||
end
|
||||
|
||||
context 'with anonymous' do
|
||||
let(:current_user) { nil }
|
||||
|
||||
it { is_expected.to be_disallowed(:read_group_timelogs) }
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
|
@ -1353,4 +1353,54 @@ RSpec.describe ProjectPolicy do
|
|||
end
|
||||
end
|
||||
end
|
||||
|
||||
context 'timelogs' do
|
||||
context 'with admin' do
|
||||
let(:current_user) { admin }
|
||||
|
||||
context 'when admin mode enabled', :enable_admin_mode do
|
||||
it { is_expected.to be_allowed(:read_group_timelogs) }
|
||||
end
|
||||
|
||||
context 'when admin mode disabled' do
|
||||
it { is_expected.to be_disallowed(:read_group_timelogs) }
|
||||
end
|
||||
end
|
||||
|
||||
context 'with owner' do
|
||||
let(:current_user) { owner }
|
||||
|
||||
it { is_expected.to be_allowed(:read_group_timelogs) }
|
||||
end
|
||||
|
||||
context 'with maintainer' do
|
||||
let(:current_user) { maintainer }
|
||||
|
||||
it { is_expected.to be_allowed(:read_group_timelogs) }
|
||||
end
|
||||
|
||||
context 'with reporter' do
|
||||
let(:current_user) { reporter }
|
||||
|
||||
it { is_expected.to be_allowed(:read_group_timelogs) }
|
||||
end
|
||||
|
||||
context 'with guest' do
|
||||
let(:current_user) { guest }
|
||||
|
||||
it { is_expected.to be_disallowed(:read_group_timelogs) }
|
||||
end
|
||||
|
||||
context 'with non member' do
|
||||
let(:current_user) { non_member }
|
||||
|
||||
it { is_expected.to be_disallowed(:read_group_timelogs) }
|
||||
end
|
||||
|
||||
context 'with anonymous' do
|
||||
let(:current_user) { anonymous }
|
||||
|
||||
it { is_expected.to be_disallowed(:read_group_timelogs) }
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
|
@ -16,7 +16,15 @@ RSpec.describe 'getting custom emoji within namespace' do
|
|||
|
||||
describe "Query CustomEmoji on Group" do
|
||||
def custom_emoji_query(group)
|
||||
graphql_query_for('group', 'fullPath' => group.full_path)
|
||||
fields = all_graphql_fields_for('Group')
|
||||
# TODO: Set required timelogs args elsewhere https://gitlab.com/gitlab-org/gitlab/-/issues/325499
|
||||
fields.selection['timelogs(startDate: "2021-03-01" endDate: "2021-03-30")'] = fields.selection.delete('timelogs')
|
||||
|
||||
graphql_query_for(
|
||||
'group',
|
||||
{ fullPath: group.full_path },
|
||||
fields
|
||||
)
|
||||
end
|
||||
|
||||
it 'returns emojis when authorised' do
|
||||
|
|
122
spec/requests/api/graphql/group/timelogs_spec.rb
Normal file
122
spec/requests/api/graphql/group/timelogs_spec.rb
Normal file
|
@ -0,0 +1,122 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
require 'spec_helper'
|
||||
|
||||
RSpec.describe 'Timelogs through GroupQuery' do
|
||||
include GraphqlHelpers
|
||||
|
||||
describe 'Get list of timelogs from a group issues' do
|
||||
let_it_be(:user) { create(:user) }
|
||||
let_it_be(:group) { create(:group) }
|
||||
let_it_be(:project) { create(:project, :public, group: group) }
|
||||
let_it_be(:milestone) { create(:milestone, group: group) }
|
||||
let_it_be(:issue) { create(:issue, project: project, milestone: milestone) }
|
||||
let_it_be(:timelog1) { create(:timelog, issue: issue, user: user, spent_at: '2019-08-13 14:00:00') }
|
||||
let_it_be(:timelog2) { create(:timelog, issue: issue, user: user, spent_at: '2019-08-10 08:00:00') }
|
||||
let_it_be(:params) { { startTime: '2019-08-10 12:00:00', endTime: '2019-08-21 12:00:00' } }
|
||||
let(:timelogs_data) { graphql_data['group']['timelogs']['nodes'] }
|
||||
|
||||
before do
|
||||
group.add_developer(user)
|
||||
end
|
||||
|
||||
context 'when the request is correct' do
|
||||
before do
|
||||
post_graphql(query, current_user: user)
|
||||
end
|
||||
|
||||
it_behaves_like 'a working graphql query'
|
||||
|
||||
it 'returns timelogs successfully' do
|
||||
expect(response).to have_gitlab_http_status(:ok)
|
||||
expect(graphql_errors).to be_nil
|
||||
expect(timelog_array.size).to eq 1
|
||||
end
|
||||
|
||||
it 'contains correct data', :aggregate_failures do
|
||||
username = timelog_array.map {|data| data['user']['username'] }
|
||||
spent_at = timelog_array.map { |data| data['spentAt'].to_time }
|
||||
time_spent = timelog_array.map { |data| data['timeSpent'] }
|
||||
issue_title = timelog_array.map {|data| data['issue']['title'] }
|
||||
milestone_title = timelog_array.map {|data| data['issue']['milestone']['title'] }
|
||||
|
||||
expect(username).to eq([user.username])
|
||||
expect(spent_at.first).to be_like_time(timelog1.spent_at)
|
||||
expect(time_spent).to eq([timelog1.time_spent])
|
||||
expect(issue_title).to eq([issue.title])
|
||||
expect(milestone_title).to eq([milestone.title])
|
||||
end
|
||||
|
||||
context 'when arguments with no time are present' do
|
||||
let!(:timelog3) { create(:timelog, issue: issue, user: user, spent_at: '2019-08-10 15:00:00') }
|
||||
let!(:timelog4) { create(:timelog, issue: issue, user: user, spent_at: '2019-08-21 15:00:00') }
|
||||
let(:params) { { startDate: '2019-08-10', endDate: '2019-08-21' }}
|
||||
|
||||
it 'sets times as start of day and end of day' do
|
||||
expect(response).to have_gitlab_http_status(:ok)
|
||||
expect(timelog_array.size).to eq 2
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
context 'when requests has errors' do
|
||||
context 'when there are no timelogs present' do
|
||||
before do
|
||||
Timelog.delete_all
|
||||
end
|
||||
|
||||
it 'returns empty result' do
|
||||
post_graphql(query, current_user: user)
|
||||
|
||||
expect(response).to have_gitlab_http_status(:success)
|
||||
expect(graphql_errors).to be_nil
|
||||
expect(timelogs_data).to be_empty
|
||||
end
|
||||
end
|
||||
|
||||
context 'when user has no permission to read group timelogs' do
|
||||
it 'returns empty result' do
|
||||
guest = create(:user)
|
||||
group.add_guest(guest)
|
||||
post_graphql(query, current_user: guest)
|
||||
|
||||
expect(response).to have_gitlab_http_status(:success)
|
||||
expect(graphql_errors).to be_nil
|
||||
expect(timelogs_data).to be_empty
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
def timelog_array(extract_attribute = nil)
|
||||
timelogs_data.map do |item|
|
||||
extract_attribute ? item[extract_attribute] : item
|
||||
end
|
||||
end
|
||||
|
||||
def query(timelog_params = params)
|
||||
timelog_nodes = <<~NODE
|
||||
nodes {
|
||||
spentAt
|
||||
timeSpent
|
||||
user {
|
||||
username
|
||||
}
|
||||
issue {
|
||||
title
|
||||
milestone {
|
||||
title
|
||||
}
|
||||
}
|
||||
}
|
||||
NODE
|
||||
|
||||
graphql_query_for("group", { "fullPath" => group.full_path },
|
||||
[query_graphql_field(
|
||||
"timelogs",
|
||||
timelog_params,
|
||||
timelog_nodes
|
||||
)]
|
||||
)
|
||||
end
|
||||
end
|
|
@ -17,7 +17,15 @@ RSpec.describe 'getting group information' do
|
|||
# similar to the API "GET /groups/:id"
|
||||
describe "Query group(fullPath)" do
|
||||
def group_query(group)
|
||||
graphql_query_for('group', 'fullPath' => group.full_path)
|
||||
fields = all_graphql_fields_for('Group')
|
||||
# TODO: Set required timelogs args elsewhere https://gitlab.com/gitlab-org/gitlab/-/issues/325499
|
||||
fields.selection['timelogs(startDate: "2021-03-01" endDate: "2021-03-30")'] = fields.selection.delete('timelogs')
|
||||
|
||||
graphql_query_for(
|
||||
'group',
|
||||
{ fullPath: group.full_path },
|
||||
fields
|
||||
)
|
||||
end
|
||||
|
||||
it_behaves_like 'a working graphql query' do
|
||||
|
|
|
@ -28,6 +28,21 @@ RSpec.describe 'search/_results' do
|
|||
expect(rendered).to have_content('Showing 1 - 2 of 3 issues for foo')
|
||||
end
|
||||
|
||||
context 'when searching notes which contain quotes in markdown' do
|
||||
let_it_be(:project) { create(:project) }
|
||||
let_it_be(:issue) { create(:issue, project: project, title: '*') }
|
||||
let_it_be(:note) { create(:discussion_note_on_issue, noteable: issue, project: issue.project, note: '```"helloworld"```') }
|
||||
let(:scope) { 'notes' }
|
||||
let(:search_objects) { Note.page(1).per(2) }
|
||||
let(:term) { 'helloworld' }
|
||||
|
||||
it 'renders plain quotes' do
|
||||
render
|
||||
|
||||
expect(rendered).to include('"<mark>helloworld</mark>"')
|
||||
end
|
||||
end
|
||||
|
||||
context 'when search results do not have a count' do
|
||||
before do
|
||||
@search_objects = @search_objects.without_count
|
||||
|
|
|
@ -18,7 +18,7 @@ RSpec.describe ServiceDeskEmailReceiverWorker, :mailer do
|
|||
worker.perform(email)
|
||||
end
|
||||
|
||||
context 'when service desk receiver raises an exception', quarantine: 'https://gitlab.com/gitlab-org/gitlab/-/issues/325579' do
|
||||
context 'when service desk receiver raises an exception' do
|
||||
before do
|
||||
allow_next_instance_of(Gitlab::Email::ServiceDeskReceiver) do |receiver|
|
||||
allow(receiver).to receive(:find_handler).and_return(nil)
|
||||
|
|
Loading…
Reference in a new issue