Add latest changes from gitlab-org/gitlab@master

This commit is contained in:
GitLab Bot 2021-03-23 03:09:04 +00:00
parent 5df6990dac
commit 4f31109a95
38 changed files with 929 additions and 78 deletions

View file

@ -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'

View file

@ -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 = () => {

View 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

View file

@ -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

View 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

View 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

View file

@ -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

View file

@ -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

View file

@ -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

View file

@ -0,0 +1,5 @@
# frozen_string_literal: true
class TimelogPolicy < BasePolicy
delegate { @subject.issuable.resource_parent }
end

View file

@ -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

View file

@ -27,4 +27,4 @@
.note-search-result
.term
= search_md_sanitize(note.note)
= simple_search_highlight_and_truncate(note.note, @search_term)

View file

@ -0,0 +1,5 @@
---
title: Remove markdown from comment search result
merge_request: 55255
author:
type: other

View file

@ -0,0 +1,5 @@
---
title: Move graphql timelogs to CE
merge_request: 56633
author: Lee Tickett @leetickett
type: fixed

View 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

View file

@ -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).

View file

@ -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

View file

@ -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. |

View file

@ -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

View file

@ -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

View file

@ -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

View file

@ -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

View 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

View 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

View file

@ -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

View file

@ -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

View file

@ -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

View file

@ -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

View file

@ -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 }

View 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

View file

@ -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

View file

@ -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

View file

@ -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

View 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

View file

@ -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

View file

@ -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

View file

@ -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)