Welcome to mirror list, hosted at ThFree Co, Russian Federation.

gitlab.com/gitlab-org/gitlab-foss.git - Unnamed repository; edit this file 'description' to name the repository.
summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorGitLab Bot <gitlab-bot@gitlab.com>2020-10-14 06:08:32 +0300
committerGitLab Bot <gitlab-bot@gitlab.com>2020-10-14 06:08:32 +0300
commit79c7e236713d3ac4df48de63018a71ac69f5c2d9 (patch)
treee7a9c4d88af2add743e0b3ebe32decc2e52d5ac5
parentba12c4bcc80d9e7dba3bd2e24065cc899918acde (diff)
Add latest changes from gitlab-org/gitlab@master
-rw-r--r--Gemfile2
-rw-r--r--Gemfile.lock29
-rw-r--r--app/assets/javascripts/sidebar/components/time_tracking/comparison_pane.vue7
-rw-r--r--app/assets/stylesheets/page_bundles/environments.scss26
-rw-r--r--app/finders/concerns/time_frame_filter.rb7
-rw-r--r--app/finders/milestones_finder.rb3
-rw-r--r--app/graphql/resolvers/base_resolver.rb2
-rw-r--r--app/graphql/resolvers/concerns/time_frame_arguments.rb20
-rw-r--r--app/graphql/resolvers/milestones_resolver.rb27
-rw-r--r--app/graphql/types/base_argument.rb14
-rw-r--r--app/graphql/types/base_field.rb2
-rw-r--r--app/graphql/types/date_type.rb22
-rw-r--r--app/graphql/types/range_input_type.rb29
-rw-r--r--app/graphql/types/timeframe_input_type.rb10
-rw-r--r--app/models/concerns/timebox.rb26
-rw-r--r--app/models/milestone.rb4
-rw-r--r--app/views/projects/diffs/_diffs.html.haml2
-rw-r--r--changelogs/unreleased/264790-bs4-optimization-diff.yml5
-rw-r--r--changelogs/unreleased/ajk-graphql-milestone-filters.yml5
-rw-r--r--changelogs/unreleased/kubeclient_491.yml6
-rw-r--r--db/structure.sql2
-rw-r--r--doc/api/graphql/reference/gitlab_schema.graphql162
-rw-r--r--doc/api/graphql/reference/gitlab_schema.json221
-rw-r--r--doc/api/lint.md13
-rw-r--r--doc/ci/yaml/README.md81
-rw-r--r--doc/user/admin_area/img/admin_wrench.pngbin3314 -> 0 bytes
-rw-r--r--doc/user/admin_area/license.md112
-rw-r--r--qa/qa/specs/features/browser_ui/3_create/repository/protocol_v2_push_http_spec.rb6
-rw-r--r--qa/qa/specs/features/browser_ui/3_create/repository/push_mirroring_lfs_over_http_spec.rb6
-rw-r--r--qa/qa/specs/features/browser_ui/3_create/repository/push_over_http_spec.rb6
-rw-r--r--spec/features/boards/sidebar_spec.rb2
-rw-r--r--spec/frontend/sidebar/components/time_tracking/time_tracker_spec.js3
-rw-r--r--spec/graphql/resolvers/group_milestones_resolver_spec.rb23
-rw-r--r--spec/graphql/resolvers/project_milestones_resolver_spec.rb61
-rw-r--r--spec/graphql/types/range_input_type_spec.rb43
-rw-r--r--spec/graphql/types/timeframe_type_spec.rb38
-rw-r--r--spec/requests/api/graphql/project/milestones_spec.rb202
-rw-r--r--spec/support/helpers/graphql_helpers.rb2
-rw-r--r--spec/support/shared_examples/models/concerns/timebox_shared_examples.rb86
39 files changed, 1112 insertions, 205 deletions
diff --git a/Gemfile b/Gemfile
index a5ab8de90e0..116a58cf4b2 100644
--- a/Gemfile
+++ b/Gemfile
@@ -259,7 +259,7 @@ gem 'asana', '0.10.2'
gem 'ruby-fogbugz', '~> 0.2.1'
# Kubernetes integration
-gem 'kubeclient', '~> 4.6.0'
+gem 'kubeclient', '~> 4.9.1'
# Sanitize user input
gem 'sanitize', '~> 5.2.1'
diff --git a/Gemfile.lock b/Gemfile.lock
index 3c4c55c6142..437a39d5ac4 100644
--- a/Gemfile.lock
+++ b/Gemfile.lock
@@ -253,7 +253,7 @@ GEM
discordrb-webhooks-blackst0ne (3.3.0)
rest-client (~> 2.0)
docile (1.3.2)
- domain_name (0.5.20180417)
+ domain_name (0.5.20190701)
unf (>= 0.0.5, < 1.0.0)
doorkeeper (5.3.3)
railties (>= 5)
@@ -563,14 +563,15 @@ GEM
html2text (0.2.0)
nokogiri (~> 1.6)
htmlentities (4.3.4)
- http (4.2.0)
+ http (4.4.1)
addressable (~> 2.3)
http-cookie (~> 1.0)
- http-form_data (~> 2.0)
+ http-form_data (~> 2.2)
http-parser (~> 1.2.0)
+ http-accept (1.7.0)
http-cookie (1.0.3)
domain_name (~> 0.5)
- http-form_data (2.1.1)
+ http-form_data (2.3.0)
http-parser (1.2.1)
ffi-compiler (>= 1.0, < 2.0)
httparty (0.16.4)
@@ -611,6 +612,9 @@ GEM
hana (~> 1.3)
regexp_parser (~> 1.5)
uri_template (~> 0.7)
+ jsonpath (1.0.5)
+ multi_json
+ to_regexp (~> 0.2.1)
jwt (2.1.0)
kaminari (1.2.1)
activesupport (>= 4.1.0)
@@ -631,9 +635,10 @@ GEM
rexml
kramdown-parser-gfm (1.1.0)
kramdown (~> 2.0)
- kubeclient (4.6.0)
+ kubeclient (4.9.1)
http (>= 3.0, < 5.0)
- recursive-open-struct (~> 1.0, >= 1.0.4)
+ jsonpath (~> 1.0)
+ recursive-open-struct (~> 1.1, >= 1.1.1)
rest-client (~> 2.0)
launchy (2.4.3)
addressable (~> 2.3)
@@ -847,7 +852,7 @@ GEM
pry (~> 0.13.0)
pry-rails (0.3.9)
pry (>= 0.10.4)
- public_suffix (4.0.3)
+ public_suffix (4.0.6)
pyu-ruby-sasl (0.0.3.3)
raabro (1.1.6)
rack (2.1.4)
@@ -920,7 +925,7 @@ GEM
re2 (1.2.0)
recaptcha (4.13.1)
json
- recursive-open-struct (1.1.1)
+ recursive-open-struct (1.1.2)
redis (4.1.3)
redis-actionpack (5.2.0)
actionpack (>= 5, < 7)
@@ -951,7 +956,8 @@ GEM
responders (3.0.0)
actionpack (>= 5.0)
railties (>= 5.0)
- rest-client (2.0.2)
+ rest-client (2.1.0)
+ http-accept (>= 1.7.0, < 2.0)
http-cookie (>= 1.0.2, < 2.0)
mime-types (>= 1.16, < 4.0)
netrc (~> 0.8)
@@ -1145,6 +1151,7 @@ GEM
timecop (0.9.1)
timeliness (0.3.10)
timfel-krb5-auth (0.8.3)
+ to_regexp (0.2.1)
toml (0.2.0)
parslet (~> 1.8.0)
toml-rb (1.0.0)
@@ -1161,7 +1168,7 @@ GEM
uber (0.1.0)
unf (0.1.4)
unf_ext
- unf_ext (0.0.7.5)
+ unf_ext (0.0.7.7)
unicode-display_width (1.7.0)
unicode_plot (0.0.4)
enumerable-statistics (>= 2.0.1)
@@ -1371,7 +1378,7 @@ DEPENDENCIES
kaminari (~> 1.0)
knapsack (~> 1.17)
kramdown (~> 2.3.0)
- kubeclient (~> 4.6.0)
+ kubeclient (~> 4.9.1)
letter_opener_web (~> 1.3.4)
license_finder (~> 6.0)
licensee (~> 8.9)
diff --git a/app/assets/javascripts/sidebar/components/time_tracking/comparison_pane.vue b/app/assets/javascripts/sidebar/components/time_tracking/comparison_pane.vue
index 4cb8d9ebd62..d4cc98e3743 100644
--- a/app/assets/javascripts/sidebar/components/time_tracking/comparison_pane.vue
+++ b/app/assets/javascripts/sidebar/components/time_tracking/comparison_pane.vue
@@ -1,7 +1,6 @@
<script>
-import { GlProgressBar } from '@gitlab/ui';
+import { GlProgressBar, GlTooltipDirective } from '@gitlab/ui';
import { parseSeconds, stringifyTime } from '~/lib/utils/datetime_utility';
-import tooltip from '../../../vue_shared/directives/tooltip';
import { s__, sprintf } from '~/locale';
export default {
@@ -10,7 +9,7 @@ export default {
GlProgressBar,
},
directives: {
- tooltip,
+ GlTooltip: GlTooltipDirective,
},
props: {
timeSpent: {
@@ -73,7 +72,7 @@ export default {
<template>
<div class="time-tracking-comparison-pane">
<div
- v-tooltip
+ v-gl-tooltip
:title="timeRemainingTooltip"
:class="timeRemainingStatusClass"
class="compare-meter"
diff --git a/app/assets/stylesheets/page_bundles/environments.scss b/app/assets/stylesheets/page_bundles/environments.scss
index 8f65a626e5f..871f118ea9d 100644
--- a/app/assets/stylesheets/page_bundles/environments.scss
+++ b/app/assets/stylesheets/page_bundles/environments.scss
@@ -8,24 +8,24 @@
.external-url,
.dropdown-new {
- color: $gl-text-color-secondary;
+ color: var(--gray-500, $gray-500);
}
.build-link,
.ref-name {
- color: $gl-text-color;
+ color: var(--gray-900, $gray-900);
}
.folder-icon {
margin-right: 3px;
- color: $gl-text-color-secondary;
+ color: var(--gray-500, $gray-500);
display: inline-block;
vertical-align: text-top;
}
.folder-name {
cursor: pointer;
- color: $gl-text-color-secondary;
+ color: var(--gray-500, $gray-500);
display: inline-block;
}
@@ -74,17 +74,17 @@
.x-axis path,
.y-axis path {
- stroke: $gray-300;
+ stroke: var(--gray-300, $gray-300);
}
.label-x-axis-line,
.label-y-axis-line {
- stroke: $border-color;
+ stroke: var(--gray-100, $gray-100);
}
.y-axis {
line {
- stroke: $gray-300;
+ stroke: var(--gray-300, $gray-300);
stroke-width: 1;
}
}
@@ -94,13 +94,13 @@
}
.rect-text-metric {
- fill: $white;
+ fill: var(--white, $white);
stroke-width: 1;
- stroke: $gray-darkest;
+ stroke: var(--gray-600, $gray-600);
}
.rect-axis-text {
- fill: $white;
+ fill: var(--white, $white);
}
.text-metric {
@@ -108,18 +108,18 @@
}
.selected-metric-line {
- stroke: $gray-900;
+ stroke: var(--gray-900, $gray-900);
stroke-width: 1;
}
.deployment-line {
- stroke: $black;
+ stroke: var(--white, $white);
stroke-width: 1;
}
.divider-line {
stroke-width: 1;
- stroke: $gray-darkest;
+ stroke: var(--gray-600, $gray-600);
}
.environments-actions {
diff --git a/app/finders/concerns/time_frame_filter.rb b/app/finders/concerns/time_frame_filter.rb
index e0baba25b64..d1ebed730f6 100644
--- a/app/finders/concerns/time_frame_filter.rb
+++ b/app/finders/concerns/time_frame_filter.rb
@@ -11,4 +11,11 @@ module TimeFrameFilter
rescue ArgumentError
items
end
+
+ def containing_date(items)
+ return items unless params[:containing_date]
+
+ date = params[:containing_date].to_date
+ items.within_timeframe(date, date)
+ end
end
diff --git a/app/finders/milestones_finder.rb b/app/finders/milestones_finder.rb
index 16e59b31b36..5d2a54ac979 100644
--- a/app/finders/milestones_finder.rb
+++ b/app/finders/milestones_finder.rb
@@ -9,6 +9,8 @@
# order - Orders by field default due date asc.
# title - filter by title.
# state - filters by state.
+# start_date & end_date - filters by timeframe (see TimeFrameFilter)
+# containing_date - filters by point in time (see TimeFrameFilter)
class MilestonesFinder
include FinderMethods
@@ -28,6 +30,7 @@ class MilestonesFinder
items = by_search_title(items)
items = by_state(items)
items = by_timeframe(items)
+ items = containing_date(items)
order(items)
end
diff --git a/app/graphql/resolvers/base_resolver.rb b/app/graphql/resolvers/base_resolver.rb
index 5d29d0bd437..2b8854fb4d0 100644
--- a/app/graphql/resolvers/base_resolver.rb
+++ b/app/graphql/resolvers/base_resolver.rb
@@ -6,6 +6,8 @@ module Resolvers
include ::Gitlab::Utils::StrongMemoize
include ::Gitlab::Graphql::GlobalIDCompatibility
+ argument_class ::Types::BaseArgument
+
def self.single
@single ||= Class.new(self) do
def ready?(**args)
diff --git a/app/graphql/resolvers/concerns/time_frame_arguments.rb b/app/graphql/resolvers/concerns/time_frame_arguments.rb
index ef333dd05a5..56fa0943cb2 100644
--- a/app/graphql/resolvers/concerns/time_frame_arguments.rb
+++ b/app/graphql/resolvers/concerns/time_frame_arguments.rb
@@ -3,21 +3,33 @@
module TimeFrameArguments
extend ActiveSupport::Concern
+ OVERLAPPING_TIMEFRAME_DESC = 'List items overlapping a time frame defined by startDate..endDate (if one date is provided, both must be present)'
+
included do
argument :start_date, Types::TimeType,
required: false,
- description: 'List items within a time frame where items.start_date is between startDate and endDate parameters (endDate parameter must be present)'
+ description: OVERLAPPING_TIMEFRAME_DESC,
+ deprecated: { reason: 'Use timeframe.start', milestone: '14.0' }
argument :end_date, Types::TimeType,
required: false,
- description: 'List items within a time frame where items.end_date is between startDate and endDate parameters (startDate parameter must be present)'
+ description: OVERLAPPING_TIMEFRAME_DESC,
+ deprecated: { reason: 'Use timeframe.end', milestone: '14.0' }
+
+ argument :timeframe, Types::TimeframeInputType,
+ required: false,
+ description: 'List items overlapping the given timeframe'
end
+ # TODO: remove when the start_date and end_date arguments are removed
def validate_timeframe_params!(args)
- return unless args[:start_date].present? || args[:end_date].present?
+ return unless %i[start_date end_date timeframe].any? { |k| args[k].present? }
+ return if args[:timeframe] && %i[start_date end_date].all? { |k| args[k].nil? }
error_message =
- if args[:start_date].nil? || args[:end_date].nil?
+ if args[:timeframe].present?
+ "startDate and endDate are deprecated in favor of timeframe. Please use only timeframe."
+ elsif args[:start_date].nil? || args[:end_date].nil?
"Both startDate and endDate must be present."
elsif args[:start_date] > args[:end_date]
"startDate is after endDate"
diff --git a/app/graphql/resolvers/milestones_resolver.rb b/app/graphql/resolvers/milestones_resolver.rb
index 5f80506c01b..84712b674db 100644
--- a/app/graphql/resolvers/milestones_resolver.rb
+++ b/app/graphql/resolvers/milestones_resolver.rb
@@ -13,6 +13,18 @@ module Resolvers
required: false,
description: 'Filter milestones by state'
+ argument :title, GraphQL::STRING_TYPE,
+ required: false,
+ description: 'The title of the milestone'
+
+ argument :search_title, GraphQL::STRING_TYPE,
+ required: false,
+ description: 'A search string for the title'
+
+ argument :containing_date, Types::TimeType,
+ required: false,
+ description: 'A date that the milestone contains'
+
type Types::MilestoneType, null: true
def resolve(**args)
@@ -29,9 +41,18 @@ module Resolvers
{
ids: parse_gids(args[:ids]),
state: args[:state] || 'all',
- start_date: args[:start_date],
- end_date: args[:end_date]
- }.merge(parent_id_parameters(args))
+ title: args[:title],
+ search_title: args[:search_title],
+ containing_date: args[:containing_date]
+ }.merge!(timeframe_parameters(args)).merge!(parent_id_parameters(args))
+ end
+
+ def timeframe_parameters(args)
+ if args[:timeframe]
+ args[:timeframe].transform_keys { |k| :"#{k}_date" }
+ else
+ args.slice(:start_date, :end_date)
+ end
end
def parent
diff --git a/app/graphql/types/base_argument.rb b/app/graphql/types/base_argument.rb
new file mode 100644
index 00000000000..11774d0b59d
--- /dev/null
+++ b/app/graphql/types/base_argument.rb
@@ -0,0 +1,14 @@
+# frozen_string_literal: true
+
+module Types
+ class BaseArgument < GraphQL::Schema::Argument
+ include GitlabStyleDeprecations
+
+ def initialize(*args, **kwargs, &block)
+ kwargs = gitlab_deprecation(kwargs)
+ kwargs.delete(:deprecation_reason)
+
+ super(*args, **kwargs, &block)
+ end
+ end
+end
diff --git a/app/graphql/types/base_field.rb b/app/graphql/types/base_field.rb
index 1e72a4cddf5..5c8aabfe163 100644
--- a/app/graphql/types/base_field.rb
+++ b/app/graphql/types/base_field.rb
@@ -5,6 +5,8 @@ module Types
prepend Gitlab::Graphql::Authorize
include GitlabStyleDeprecations
+ argument_class ::Types::BaseArgument
+
DEFAULT_COMPLEXITY = 1
def initialize(*args, **kwargs, &block)
diff --git a/app/graphql/types/date_type.rb b/app/graphql/types/date_type.rb
new file mode 100644
index 00000000000..7129b75b8bb
--- /dev/null
+++ b/app/graphql/types/date_type.rb
@@ -0,0 +1,22 @@
+# frozen_string_literal: true
+
+module Types
+ class DateType < BaseScalar
+ graphql_name 'Date'
+ description 'Date represented in ISO 8601'
+
+ def self.coerce_input(value, ctx)
+ return if value.nil?
+
+ Date.iso8601(value)
+ rescue ArgumentError, TypeError => e
+ raise GraphQL::CoercionError, e.message
+ end
+
+ def self.coerce_result(value, ctx)
+ return if value.nil?
+
+ value.to_date.iso8601
+ end
+ end
+end
diff --git a/app/graphql/types/range_input_type.rb b/app/graphql/types/range_input_type.rb
new file mode 100644
index 00000000000..766e523a99e
--- /dev/null
+++ b/app/graphql/types/range_input_type.rb
@@ -0,0 +1,29 @@
+# frozen_string_literal: true
+
+module Types
+ # rubocop: disable Graphql/AuthorizeTypes
+ class RangeInputType < BaseInputObject
+ def self.[](type, closed = true)
+ @subtypes ||= {}
+
+ @subtypes[[type, closed]] ||= Class.new(self) do
+ argument :start, type,
+ required: closed,
+ description: 'The start of the range'
+
+ argument :end, type,
+ required: closed,
+ description: 'The end of the range'
+ end
+ end
+
+ def prepare
+ if self[:end] && self[:start] && self[:end] < self[:start]
+ raise ::Gitlab::Graphql::Errors::ArgumentError, 'start must be before end'
+ end
+
+ to_h
+ end
+ end
+ # rubocop: enable Graphql/AuthorizeTypes
+end
diff --git a/app/graphql/types/timeframe_input_type.rb b/app/graphql/types/timeframe_input_type.rb
new file mode 100644
index 00000000000..79c1bc5cf01
--- /dev/null
+++ b/app/graphql/types/timeframe_input_type.rb
@@ -0,0 +1,10 @@
+# frozen_string_literal: true
+
+module Types
+ # rubocop: disable Graphql/AuthorizeTypes
+ class TimeframeInputType < RangeInputType[::Types::DateType]
+ graphql_name 'Timeframe'
+ description 'A time-frame defined as a closed inclusive range of two dates'
+ end
+ # rubocop: enable Graphql/AuthorizeTypes
+end
diff --git a/app/models/concerns/timebox.rb b/app/models/concerns/timebox.rb
index 3e2cf9031d0..23fd73f2904 100644
--- a/app/models/concerns/timebox.rb
+++ b/app/models/concerns/timebox.rb
@@ -73,6 +73,32 @@ module Timebox
end
end
+ # A timebox is within the timeframe (start_date, end_date) if it overlaps
+ # with that timeframe:
+ #
+ # [ timeframe ]
+ # ----| ................ # Not overlapping
+ # |--| ................ # Not overlapping
+ # ------|............... # Overlapping
+ # -----------------------| # Overlapping
+ # ---------|............ # Overlapping
+ # |-----|............ # Overlapping
+ # |--------------| # Overlapping
+ # |--------------------| # Overlapping
+ # ...|-----|...... # Overlapping
+ # .........|-----| # Overlapping
+ # .........|--------- # Overlapping
+ # |-------------------- # Overlapping
+ # .........|--------| # Overlapping
+ # ...............|--| # Overlapping
+ # ............... |-| # Not Overlapping
+ # ............... |-- # Not Overlapping
+ #
+ # where: . = in timeframe
+ # ---| no start
+ # |--- no end
+ # |--| defined start and end
+ #
scope :within_timeframe, -> (start_date, end_date) do
where('start_date is not NULL or due_date is not NULL')
.where('start_date is NULL or start_date <= ?', end_date)
diff --git a/app/models/milestone.rb b/app/models/milestone.rb
index 55326b9a282..0a315ba8db2 100644
--- a/app/models/milestone.rb
+++ b/app/models/milestone.rb
@@ -46,6 +46,10 @@ class Milestone < ApplicationRecord
state :active
end
+ def self.min_chars_for_partial_matching
+ 2
+ end
+
def self.reference_prefix
'%'
end
diff --git a/app/views/projects/diffs/_diffs.html.haml b/app/views/projects/diffs/_diffs.html.haml
index 6ba363e6555..43aaa7cb405 100644
--- a/app/views/projects/diffs/_diffs.html.haml
+++ b/app/views/projects/diffs/_diffs.html.haml
@@ -7,7 +7,7 @@
.content-block.oneline-block.files-changed.diff-files-changed.js-diff-files-changed
.files-changed-inner
- .inline-parallel-buttons.d-none.d-sm-none.d-md-block
+ .inline-parallel-buttons.d-none.d-md-block
- if !diffs_expanded? && diff_files.any? { |diff_file| diff_file.collapsed? }
= link_to _('Expand all'), url_for(safe_params.merge(expanded: 1, format: nil)), class: 'btn btn-default'
- if show_whitespace_toggle
diff --git a/changelogs/unreleased/264790-bs4-optimization-diff.yml b/changelogs/unreleased/264790-bs4-optimization-diff.yml
new file mode 100644
index 00000000000..13a795d2568
--- /dev/null
+++ b/changelogs/unreleased/264790-bs4-optimization-diff.yml
@@ -0,0 +1,5 @@
+---
+title: Remove duplicated BS display properties from Diff's HAML
+merge_request: 44848
+author: Takuya Noguchi
+type: other
diff --git a/changelogs/unreleased/ajk-graphql-milestone-filters.yml b/changelogs/unreleased/ajk-graphql-milestone-filters.yml
new file mode 100644
index 00000000000..872cc180fd5
--- /dev/null
+++ b/changelogs/unreleased/ajk-graphql-milestone-filters.yml
@@ -0,0 +1,5 @@
+---
+title: Add filters on Milestone title in the GraphQL API
+merge_request: 44208
+author:
+type: changed
diff --git a/changelogs/unreleased/kubeclient_491.yml b/changelogs/unreleased/kubeclient_491.yml
new file mode 100644
index 00000000000..56f3c1403ed
--- /dev/null
+++ b/changelogs/unreleased/kubeclient_491.yml
@@ -0,0 +1,6 @@
+---
+title: Bump kubeclient to 4.9.1 which includes ability to integrate Kubernetes clusters
+ where their API url is on a sub-path
+merge_request: 44856
+author:
+type: other
diff --git a/db/structure.sql b/db/structure.sql
index 38a89fe7dc4..602622e6418 100644
--- a/db/structure.sql
+++ b/db/structure.sql
@@ -20563,6 +20563,8 @@ CREATE INDEX index_issues_on_updated_at ON issues USING btree (updated_at);
CREATE INDEX index_issues_on_updated_by_id ON issues USING btree (updated_by_id) WHERE (updated_by_id IS NOT NULL);
+CREATE INDEX index_issues_project_id_issue_type_incident ON issues USING btree (project_id) WHERE (issue_type = 1);
+
CREATE UNIQUE INDEX index_jira_connect_installations_on_client_key ON jira_connect_installations USING btree (client_key);
CREATE INDEX index_jira_connect_subscriptions_on_namespace_id ON jira_connect_subscriptions USING btree (namespace_id);
diff --git a/doc/api/graphql/reference/gitlab_schema.graphql b/doc/api/graphql/reference/gitlab_schema.graphql
index bd231c76b14..fd26d567574 100644
--- a/doc/api/graphql/reference/gitlab_schema.graphql
+++ b/doc/api/graphql/reference/gitlab_schema.graphql
@@ -1222,8 +1222,8 @@ type BoardEpic implements CurrentUserTodos & Noteable {
before: String
"""
- List items within a time frame where items.end_date is between startDate and
- endDate parameters (startDate parameter must be present)
+ List items overlapping a time frame defined by startDate..endDate (if one
+ date is provided, both must be present). Deprecated in 14.0: Use timeframe.end
"""
endDate: Time
@@ -1273,8 +1273,9 @@ type BoardEpic implements CurrentUserTodos & Noteable {
sort: EpicSort
"""
- List items within a time frame where items.start_date is between startDate
- and endDate parameters (endDate parameter must be present)
+ List items overlapping a time frame defined by startDate..endDate (if one
+ date is provided, both must be present). Deprecated in 14.0: Use
+ timeframe.start
"""
startDate: Time
@@ -1282,6 +1283,11 @@ type BoardEpic implements CurrentUserTodos & Noteable {
Filter epics by state
"""
state: EpicState
+
+ """
+ List items overlapping the given timeframe
+ """
+ timeframe: Timeframe
): EpicConnection
"""
@@ -4279,6 +4285,11 @@ enum DastSiteProfileValidationStatusEnum {
}
"""
+Date represented in ISO 8601
+"""
+scalar Date
+
+"""
Autogenerated input type of DeleteAnnotation
"""
input DeleteAnnotationInput {
@@ -5893,8 +5904,8 @@ type Epic implements CurrentUserTodos & Noteable {
before: String
"""
- List items within a time frame where items.end_date is between startDate and
- endDate parameters (startDate parameter must be present)
+ List items overlapping a time frame defined by startDate..endDate (if one
+ date is provided, both must be present). Deprecated in 14.0: Use timeframe.end
"""
endDate: Time
@@ -5944,8 +5955,9 @@ type Epic implements CurrentUserTodos & Noteable {
sort: EpicSort
"""
- List items within a time frame where items.start_date is between startDate
- and endDate parameters (endDate parameter must be present)
+ List items overlapping a time frame defined by startDate..endDate (if one
+ date is provided, both must be present). Deprecated in 14.0: Use
+ timeframe.start
"""
startDate: Time
@@ -5953,6 +5965,11 @@ type Epic implements CurrentUserTodos & Noteable {
Filter epics by state
"""
state: EpicState
+
+ """
+ List items overlapping the given timeframe
+ """
+ timeframe: Timeframe
): EpicConnection
"""
@@ -7381,8 +7398,8 @@ type Group {
authorUsername: String
"""
- List items within a time frame where items.end_date is between startDate and
- endDate parameters (startDate parameter must be present)
+ List items overlapping a time frame defined by startDate..endDate (if one
+ date is provided, both must be present). Deprecated in 14.0: Use timeframe.end
"""
endDate: Time
@@ -7422,8 +7439,9 @@ type Group {
sort: EpicSort
"""
- List items within a time frame where items.start_date is between startDate
- and endDate parameters (endDate parameter must be present)
+ List items overlapping a time frame defined by startDate..endDate (if one
+ date is provided, both must be present). Deprecated in 14.0: Use
+ timeframe.start
"""
startDate: Time
@@ -7431,6 +7449,11 @@ type Group {
Filter epics by state
"""
state: EpicState
+
+ """
+ List items overlapping the given timeframe
+ """
+ timeframe: Timeframe
): Epic
"""
@@ -7453,8 +7476,8 @@ type Group {
before: String
"""
- List items within a time frame where items.end_date is between startDate and
- endDate parameters (startDate parameter must be present)
+ List items overlapping a time frame defined by startDate..endDate (if one
+ date is provided, both must be present). Deprecated in 14.0: Use timeframe.end
"""
endDate: Time
@@ -7504,8 +7527,9 @@ type Group {
sort: EpicSort
"""
- List items within a time frame where items.start_date is between startDate
- and endDate parameters (endDate parameter must be present)
+ List items overlapping a time frame defined by startDate..endDate (if one
+ date is provided, both must be present). Deprecated in 14.0: Use
+ timeframe.start
"""
startDate: Time
@@ -7513,6 +7537,11 @@ type Group {
Filter epics by state
"""
state: EpicState
+
+ """
+ List items overlapping the given timeframe
+ """
+ timeframe: Timeframe
): EpicConnection
"""
@@ -7715,8 +7744,8 @@ type Group {
before: String
"""
- List items within a time frame where items.end_date is between startDate and
- endDate parameters (startDate parameter must be present)
+ List items overlapping a time frame defined by startDate..endDate (if one
+ date is provided, both must be present). Deprecated in 14.0: Use timeframe.end
"""
endDate: Time
@@ -7746,8 +7775,9 @@ type Group {
last: Int
"""
- List items within a time frame where items.start_date is between startDate
- and endDate parameters (endDate parameter must be present)
+ List items overlapping a time frame defined by startDate..endDate (if one
+ date is provided, both must be present). Deprecated in 14.0: Use
+ timeframe.start
"""
startDate: Time
@@ -7757,6 +7787,11 @@ type Group {
state: IterationState
"""
+ List items overlapping the given timeframe
+ """
+ timeframe: Timeframe
+
+ """
Fuzzy search by title
"""
title: String
@@ -7912,8 +7947,13 @@ type Group {
before: String
"""
- List items within a time frame where items.end_date is between startDate and
- endDate parameters (startDate parameter must be present)
+ A date that the milestone contains
+ """
+ containingDate: Time
+
+ """
+ List items overlapping a time frame defined by startDate..endDate (if one
+ date is provided, both must be present). Deprecated in 14.0: Use timeframe.end
"""
endDate: Time
@@ -7938,8 +7978,14 @@ type Group {
last: Int
"""
- List items within a time frame where items.start_date is between startDate
- and endDate parameters (endDate parameter must be present)
+ A search string for the title
+ """
+ searchTitle: String
+
+ """
+ List items overlapping a time frame defined by startDate..endDate (if one
+ date is provided, both must be present). Deprecated in 14.0: Use
+ timeframe.start
"""
startDate: Time
@@ -7947,6 +7993,16 @@ type Group {
Filter milestones by state
"""
state: MilestoneStateEnum
+
+ """
+ List items overlapping the given timeframe
+ """
+ timeframe: Timeframe
+
+ """
+ The title of the milestone
+ """
+ title: String
): MilestoneConnection
"""
@@ -13755,8 +13811,8 @@ type Project {
before: String
"""
- List items within a time frame where items.end_date is between startDate and
- endDate parameters (startDate parameter must be present)
+ List items overlapping a time frame defined by startDate..endDate (if one
+ date is provided, both must be present). Deprecated in 14.0: Use timeframe.end
"""
endDate: Time
@@ -13786,8 +13842,9 @@ type Project {
last: Int
"""
- List items within a time frame where items.start_date is between startDate
- and endDate parameters (endDate parameter must be present)
+ List items overlapping a time frame defined by startDate..endDate (if one
+ date is provided, both must be present). Deprecated in 14.0: Use
+ timeframe.start
"""
startDate: Time
@@ -13797,6 +13854,11 @@ type Project {
state: IterationState
"""
+ List items overlapping the given timeframe
+ """
+ timeframe: Timeframe
+
+ """
Fuzzy search by title
"""
title: String
@@ -14004,8 +14066,13 @@ type Project {
before: String
"""
- List items within a time frame where items.end_date is between startDate and
- endDate parameters (startDate parameter must be present)
+ A date that the milestone contains
+ """
+ containingDate: Time
+
+ """
+ List items overlapping a time frame defined by startDate..endDate (if one
+ date is provided, both must be present). Deprecated in 14.0: Use timeframe.end
"""
endDate: Time
@@ -14030,8 +14097,14 @@ type Project {
last: Int
"""
- List items within a time frame where items.start_date is between startDate
- and endDate parameters (endDate parameter must be present)
+ A search string for the title
+ """
+ searchTitle: String
+
+ """
+ List items overlapping a time frame defined by startDate..endDate (if one
+ date is provided, both must be present). Deprecated in 14.0: Use
+ timeframe.start
"""
startDate: Time
@@ -14039,6 +14112,16 @@ type Project {
Filter milestones by state
"""
state: MilestoneStateEnum
+
+ """
+ List items overlapping the given timeframe
+ """
+ timeframe: Timeframe
+
+ """
+ The title of the milestone
+ """
+ title: String
): MilestoneConnection
"""
@@ -18336,6 +18419,21 @@ interface TimeboxBurnupTimeSeriesInterface {
burnupTimeSeries: [BurnupChartDailyTotals!]
}
+"""
+A time-frame defined as a closed inclusive range of two dates
+"""
+input Timeframe {
+ """
+ The end of the range
+ """
+ end: Date!
+
+ """
+ The start of the range
+ """
+ start: Date!
+}
+
type Timelog {
"""
Timestamp of when the time tracked was spent at. Deprecated in 12.10: Use `spentAt`
diff --git a/doc/api/graphql/reference/gitlab_schema.json b/doc/api/graphql/reference/gitlab_schema.json
index f70814267ef..7e6266ca26c 100644
--- a/doc/api/graphql/reference/gitlab_schema.json
+++ b/doc/api/graphql/reference/gitlab_schema.json
@@ -3181,7 +3181,7 @@
"args": [
{
"name": "startDate",
- "description": "List items within a time frame where items.start_date is between startDate and endDate parameters (endDate parameter must be present)",
+ "description": "List items overlapping a time frame defined by startDate..endDate (if one date is provided, both must be present). Deprecated in 14.0: Use timeframe.start",
"type": {
"kind": "SCALAR",
"name": "Time",
@@ -3191,7 +3191,7 @@
},
{
"name": "endDate",
- "description": "List items within a time frame where items.end_date is between startDate and endDate parameters (startDate parameter must be present)",
+ "description": "List items overlapping a time frame defined by startDate..endDate (if one date is provided, both must be present). Deprecated in 14.0: Use timeframe.end",
"type": {
"kind": "SCALAR",
"name": "Time",
@@ -3200,6 +3200,16 @@
"defaultValue": null
},
{
+ "name": "timeframe",
+ "description": "List items overlapping the given timeframe",
+ "type": {
+ "kind": "INPUT_OBJECT",
+ "name": "Timeframe",
+ "ofType": null
+ },
+ "defaultValue": null
+ },
+ {
"name": "iid",
"description": "IID of the epic, e.g., \"1\"",
"type": {
@@ -11567,6 +11577,16 @@
"possibleTypes": null
},
{
+ "kind": "SCALAR",
+ "name": "Date",
+ "description": "Date represented in ISO 8601",
+ "fields": null,
+ "inputFields": null,
+ "interfaces": null,
+ "enumValues": null,
+ "possibleTypes": null
+ },
+ {
"kind": "INPUT_OBJECT",
"name": "DeleteAnnotationInput",
"description": "Autogenerated input type of DeleteAnnotation",
@@ -16290,7 +16310,7 @@
"args": [
{
"name": "startDate",
- "description": "List items within a time frame where items.start_date is between startDate and endDate parameters (endDate parameter must be present)",
+ "description": "List items overlapping a time frame defined by startDate..endDate (if one date is provided, both must be present). Deprecated in 14.0: Use timeframe.start",
"type": {
"kind": "SCALAR",
"name": "Time",
@@ -16300,7 +16320,7 @@
},
{
"name": "endDate",
- "description": "List items within a time frame where items.end_date is between startDate and endDate parameters (startDate parameter must be present)",
+ "description": "List items overlapping a time frame defined by startDate..endDate (if one date is provided, both must be present). Deprecated in 14.0: Use timeframe.end",
"type": {
"kind": "SCALAR",
"name": "Time",
@@ -16309,6 +16329,16 @@
"defaultValue": null
},
{
+ "name": "timeframe",
+ "description": "List items overlapping the given timeframe",
+ "type": {
+ "kind": "INPUT_OBJECT",
+ "name": "Timeframe",
+ "ofType": null
+ },
+ "defaultValue": null
+ },
+ {
"name": "iid",
"description": "IID of the epic, e.g., \"1\"",
"type": {
@@ -20364,7 +20394,7 @@
"args": [
{
"name": "startDate",
- "description": "List items within a time frame where items.start_date is between startDate and endDate parameters (endDate parameter must be present)",
+ "description": "List items overlapping a time frame defined by startDate..endDate (if one date is provided, both must be present). Deprecated in 14.0: Use timeframe.start",
"type": {
"kind": "SCALAR",
"name": "Time",
@@ -20374,7 +20404,7 @@
},
{
"name": "endDate",
- "description": "List items within a time frame where items.end_date is between startDate and endDate parameters (startDate parameter must be present)",
+ "description": "List items overlapping a time frame defined by startDate..endDate (if one date is provided, both must be present). Deprecated in 14.0: Use timeframe.end",
"type": {
"kind": "SCALAR",
"name": "Time",
@@ -20383,6 +20413,16 @@
"defaultValue": null
},
{
+ "name": "timeframe",
+ "description": "List items overlapping the given timeframe",
+ "type": {
+ "kind": "INPUT_OBJECT",
+ "name": "Timeframe",
+ "ofType": null
+ },
+ "defaultValue": null
+ },
+ {
"name": "iid",
"description": "IID of the epic, e.g., \"1\"",
"type": {
@@ -20503,7 +20543,7 @@
"args": [
{
"name": "startDate",
- "description": "List items within a time frame where items.start_date is between startDate and endDate parameters (endDate parameter must be present)",
+ "description": "List items overlapping a time frame defined by startDate..endDate (if one date is provided, both must be present). Deprecated in 14.0: Use timeframe.start",
"type": {
"kind": "SCALAR",
"name": "Time",
@@ -20513,7 +20553,7 @@
},
{
"name": "endDate",
- "description": "List items within a time frame where items.end_date is between startDate and endDate parameters (startDate parameter must be present)",
+ "description": "List items overlapping a time frame defined by startDate..endDate (if one date is provided, both must be present). Deprecated in 14.0: Use timeframe.end",
"type": {
"kind": "SCALAR",
"name": "Time",
@@ -20522,6 +20562,16 @@
"defaultValue": null
},
{
+ "name": "timeframe",
+ "description": "List items overlapping the given timeframe",
+ "type": {
+ "kind": "INPUT_OBJECT",
+ "name": "Timeframe",
+ "ofType": null
+ },
+ "defaultValue": null
+ },
+ {
"name": "iid",
"description": "IID of the epic, e.g., \"1\"",
"type": {
@@ -21134,7 +21184,7 @@
"args": [
{
"name": "startDate",
- "description": "List items within a time frame where items.start_date is between startDate and endDate parameters (endDate parameter must be present)",
+ "description": "List items overlapping a time frame defined by startDate..endDate (if one date is provided, both must be present). Deprecated in 14.0: Use timeframe.start",
"type": {
"kind": "SCALAR",
"name": "Time",
@@ -21144,7 +21194,7 @@
},
{
"name": "endDate",
- "description": "List items within a time frame where items.end_date is between startDate and endDate parameters (startDate parameter must be present)",
+ "description": "List items overlapping a time frame defined by startDate..endDate (if one date is provided, both must be present). Deprecated in 14.0: Use timeframe.end",
"type": {
"kind": "SCALAR",
"name": "Time",
@@ -21153,6 +21203,16 @@
"defaultValue": null
},
{
+ "name": "timeframe",
+ "description": "List items overlapping the given timeframe",
+ "type": {
+ "kind": "INPUT_OBJECT",
+ "name": "Timeframe",
+ "ofType": null
+ },
+ "defaultValue": null
+ },
+ {
"name": "state",
"description": "Filter iterations by state",
"type": {
@@ -21580,7 +21640,7 @@
"args": [
{
"name": "startDate",
- "description": "List items within a time frame where items.start_date is between startDate and endDate parameters (endDate parameter must be present)",
+ "description": "List items overlapping a time frame defined by startDate..endDate (if one date is provided, both must be present). Deprecated in 14.0: Use timeframe.start",
"type": {
"kind": "SCALAR",
"name": "Time",
@@ -21590,7 +21650,7 @@
},
{
"name": "endDate",
- "description": "List items within a time frame where items.end_date is between startDate and endDate parameters (startDate parameter must be present)",
+ "description": "List items overlapping a time frame defined by startDate..endDate (if one date is provided, both must be present). Deprecated in 14.0: Use timeframe.end",
"type": {
"kind": "SCALAR",
"name": "Time",
@@ -21599,6 +21659,16 @@
"defaultValue": null
},
{
+ "name": "timeframe",
+ "description": "List items overlapping the given timeframe",
+ "type": {
+ "kind": "INPUT_OBJECT",
+ "name": "Timeframe",
+ "ofType": null
+ },
+ "defaultValue": null
+ },
+ {
"name": "ids",
"description": "Array of global milestone IDs, e.g., \"gid://gitlab/Milestone/1\"",
"type": {
@@ -21627,6 +21697,36 @@
"defaultValue": null
},
{
+ "name": "title",
+ "description": "The title of the milestone",
+ "type": {
+ "kind": "SCALAR",
+ "name": "String",
+ "ofType": null
+ },
+ "defaultValue": null
+ },
+ {
+ "name": "searchTitle",
+ "description": "A search string for the title",
+ "type": {
+ "kind": "SCALAR",
+ "name": "String",
+ "ofType": null
+ },
+ "defaultValue": null
+ },
+ {
+ "name": "containingDate",
+ "description": "A date that the milestone contains",
+ "type": {
+ "kind": "SCALAR",
+ "name": "Time",
+ "ofType": null
+ },
+ "defaultValue": null
+ },
+ {
"name": "includeDescendants",
"description": "Also return milestones in all subgroups and subprojects",
"type": {
@@ -40165,7 +40265,7 @@
"args": [
{
"name": "startDate",
- "description": "List items within a time frame where items.start_date is between startDate and endDate parameters (endDate parameter must be present)",
+ "description": "List items overlapping a time frame defined by startDate..endDate (if one date is provided, both must be present). Deprecated in 14.0: Use timeframe.start",
"type": {
"kind": "SCALAR",
"name": "Time",
@@ -40175,7 +40275,7 @@
},
{
"name": "endDate",
- "description": "List items within a time frame where items.end_date is between startDate and endDate parameters (startDate parameter must be present)",
+ "description": "List items overlapping a time frame defined by startDate..endDate (if one date is provided, both must be present). Deprecated in 14.0: Use timeframe.end",
"type": {
"kind": "SCALAR",
"name": "Time",
@@ -40184,6 +40284,16 @@
"defaultValue": null
},
{
+ "name": "timeframe",
+ "description": "List items overlapping the given timeframe",
+ "type": {
+ "kind": "INPUT_OBJECT",
+ "name": "Timeframe",
+ "ofType": null
+ },
+ "defaultValue": null
+ },
+ {
"name": "state",
"description": "Filter iterations by state",
"type": {
@@ -40737,7 +40847,7 @@
"args": [
{
"name": "startDate",
- "description": "List items within a time frame where items.start_date is between startDate and endDate parameters (endDate parameter must be present)",
+ "description": "List items overlapping a time frame defined by startDate..endDate (if one date is provided, both must be present). Deprecated in 14.0: Use timeframe.start",
"type": {
"kind": "SCALAR",
"name": "Time",
@@ -40747,7 +40857,7 @@
},
{
"name": "endDate",
- "description": "List items within a time frame where items.end_date is between startDate and endDate parameters (startDate parameter must be present)",
+ "description": "List items overlapping a time frame defined by startDate..endDate (if one date is provided, both must be present). Deprecated in 14.0: Use timeframe.end",
"type": {
"kind": "SCALAR",
"name": "Time",
@@ -40756,6 +40866,16 @@
"defaultValue": null
},
{
+ "name": "timeframe",
+ "description": "List items overlapping the given timeframe",
+ "type": {
+ "kind": "INPUT_OBJECT",
+ "name": "Timeframe",
+ "ofType": null
+ },
+ "defaultValue": null
+ },
+ {
"name": "ids",
"description": "Array of global milestone IDs, e.g., \"gid://gitlab/Milestone/1\"",
"type": {
@@ -40784,6 +40904,36 @@
"defaultValue": null
},
{
+ "name": "title",
+ "description": "The title of the milestone",
+ "type": {
+ "kind": "SCALAR",
+ "name": "String",
+ "ofType": null
+ },
+ "defaultValue": null
+ },
+ {
+ "name": "searchTitle",
+ "description": "A search string for the title",
+ "type": {
+ "kind": "SCALAR",
+ "name": "String",
+ "ofType": null
+ },
+ "defaultValue": null
+ },
+ {
+ "name": "containingDate",
+ "description": "A date that the milestone contains",
+ "type": {
+ "kind": "SCALAR",
+ "name": "Time",
+ "ofType": null
+ },
+ "defaultValue": null
+ },
+ {
"name": "includeAncestors",
"description": "Also return milestones in the project's parent group and its ancestors",
"type": {
@@ -53363,6 +53513,45 @@
]
},
{
+ "kind": "INPUT_OBJECT",
+ "name": "Timeframe",
+ "description": "A time-frame defined as a closed inclusive range of two dates",
+ "fields": null,
+ "inputFields": [
+ {
+ "name": "start",
+ "description": "The start of the range",
+ "type": {
+ "kind": "NON_NULL",
+ "name": null,
+ "ofType": {
+ "kind": "SCALAR",
+ "name": "Date",
+ "ofType": null
+ }
+ },
+ "defaultValue": null
+ },
+ {
+ "name": "end",
+ "description": "The end of the range",
+ "type": {
+ "kind": "NON_NULL",
+ "name": null,
+ "ofType": {
+ "kind": "SCALAR",
+ "name": "Date",
+ "ofType": null
+ }
+ },
+ "defaultValue": null
+ }
+ ],
+ "interfaces": null,
+ "enumValues": null,
+ "possibleTypes": null
+ },
+ {
"kind": "OBJECT",
"name": "Timelog",
"description": null,
diff --git a/doc/api/lint.md b/doc/api/lint.md
index 652a5289f13..addc8d0f9a3 100644
--- a/doc/api/lint.md
+++ b/doc/api/lint.md
@@ -6,7 +6,7 @@ info: To determine the technical writer assigned to the Stage/Group associated w
# CI Lint API
-## Validate the CI YAML config
+## Validate the CI YAML configuration
> [Introduced](https://gitlab.com/gitlab-org/gitlab-foss/-/merge_requests/5953) in GitLab 8.12.
@@ -26,7 +26,7 @@ POST /ci/lint
curl --header "Content-Type: application/json" "https://gitlab.example.com/api/v4/ci/lint" --data '{"content": "{ \"image\": \"ruby:2.6\", \"services\": [\"postgres\"], \"before_script\": [\"bundle install\", \"bundle exec rake db:create\"], \"variables\": {\"DB_NAME\": \"postgres\"}, \"types\": [\"test\", \"deploy\", \"notify\"], \"rspec\": { \"script\": \"rake spec\", \"tags\": [\"ruby\", \"postgres\"], \"only\": [\"branches\"]}}"}'
```
-Be sure to paste the exact contents of your GitLab CI/CD YAML config because YAML
+Be sure to paste the exact contents of your GitLab CI/CD YAML configuration because YAML
is very sensitive about indentation and spacing.
Example responses:
@@ -61,7 +61,10 @@ Example responses:
### YAML expansion
-The expansion only works for CI configurations that don't have local [includes](../ci/yaml/README.md#include).
+> [Introduced](https://gitlab.com/gitlab-org/gitlab/-/issues/29568) in GitLab 13.5.
+
+The CI lint returns an expanded version of the configuration. The expansion does not
+work for CI configuration added with [`include: local`](../ci/yaml/README.md#includelocal).
Example contents of a `.gitlab-ci.yml` passed to the CI Lint API with
`include_merged_yaml` set as true:
@@ -119,7 +122,7 @@ curl "https://gitlab.example.com/api/v4/projects/:id/ci/lint"
Example responses:
-- Valid config:
+- Valid configuration:
```json
{
@@ -130,7 +133,7 @@ Example responses:
}
```
-- Invalid config:
+- Invalid configuration:
```json
{
diff --git a/doc/ci/yaml/README.md b/doc/ci/yaml/README.md
index 0656c76236d..920cf92fe88 100644
--- a/doc/ci/yaml/README.md
+++ b/doc/ci/yaml/README.md
@@ -2622,10 +2622,12 @@ The `stop_review_app` job is **required** to have the following keywords defined
- `environment:action`
Additionally, both jobs should have matching [`rules`](../yaml/README.md#onlyexcept-basic)
-or [`only/except`](../yaml/README.md#onlyexcept-basic) configuration. In the example
-above, if the configuration is not identical, the `stop_review_app` job might not be
-included in all pipelines that include the `review_app` job, and it is not
-possible to trigger the `action: stop` to stop the environment automatically.
+or [`only/except`](../yaml/README.md#onlyexcept-basic) configuration.
+
+In the example above, if the configuration is not identical:
+
+- The `stop_review_app` job might not be included in all pipelines that include the `review_app` job.
+- It is not possible to trigger the `action: stop` to stop the environment automatically.
#### `environment:auto_stop_in`
@@ -2774,17 +2776,17 @@ rspec:
- binaries/
```
-Note that since cache is shared between jobs, if you're using different
-paths for different jobs, you should also set a different **cache:key**
-otherwise cache content can be overwritten.
+The cache is shared between jobs, so if you're using different
+paths for different jobs, you should also set a different `cache:key`.
+Otherwise cache content can be overwritten.
#### `cache:key`
> Introduced in GitLab Runner v1.0.0.
-Since the cache is shared between jobs, if you're using different
-paths for different jobs, you should also set a different `cache:key`
-otherwise cache content can be overwritten.
+The cache is shared between jobs, so if you're using different
+paths for different jobs, you should also set a different `cache:key`.
+Otherwise cache content can be overwritten.
The `key` parameter defines the affinity of caching between jobs,
to have a single cache for all jobs, cache per-job, cache per-branch
@@ -2973,13 +2975,13 @@ rspec:
- bundle exec rspec ...
```
-This helps to speed up job execution and reduce load on the cache server,
-especially when you have a large number of cache-using jobs executing in
+This helps to speed up job execution and reduce load on the cache server.
+It is especially helpful when you have a large number of cache-using jobs executing in
parallel.
-Additionally, if you have a job that unconditionally recreates the cache without
-reference to its previous contents, you can use `policy: push` in that job to
-skip the download step.
+If you have a job that unconditionally recreates the cache without
+referring to its previous contents, you can skip the download step.
+To do so, add `policy: push` to the job.
### `artifacts`
@@ -2992,7 +2994,7 @@ skip the download step.
`artifacts` is used to specify a list of files and directories that are
attached to the job when it [succeeds, fails, or always](#artifactswhen).
-The artifacts are sent to GitLab after the job finishes and are
+The artifacts are sent to GitLab after the job finishes. They are
available for download in the GitLab UI if the size is not
larger than the [maximum artifact size](../../user/gitlab_com/index.md#gitlab-cicd).
@@ -3341,19 +3343,22 @@ These are the available report types:
> Introduced in GitLab 8.6 and GitLab Runner v1.1.1.
-By default, all [`artifacts`](#artifacts) from all previous [stages](#stages)
-are passed, but you can use the `dependencies` parameter to define a limited
-list of jobs (or no jobs) to fetch artifacts from.
+By default, all [`artifacts`](#artifacts) from previous [stages](#stages)
+are passed to each job. However, you can use the `dependencies` parameter to
+define a limited list of jobs to fetch artifacts from. You can also set a job to download no artifacts at all.
To use this feature, define `dependencies` in context of the job and pass
a list of all previous jobs the artifacts should be downloaded from.
-You can only define jobs from stages that are executed before the current one.
-An error is shown if you define jobs from the current stage or next ones.
-Defining an empty array skips downloading any artifacts for that job.
-The status of the previous job is not considered when using `dependencies`, so
-if it failed or it's a manual job that was not run, no error occurs.
-In the following example, we define two jobs with artifacts, `build:osx` and
+You can define jobs from stages that were executed before the current one.
+An error occurs if you define jobs from the current or an upcoming stage.
+
+To prevent a job from downloading artifacts, define an empty array.
+
+When you use `dependencies`, the status of the previous job is not considered.
+If a job fails or it's a manual job that was not run, no error occurs.
+
+The following example defines two jobs with artifacts: `build:osx` and
`build:linux`. When the `test:osx` is executed, the artifacts from `build:osx`
are downloaded and extracted in the context of the build. The same happens
for `test:linux` and artifacts from `build:linux`.
@@ -3435,14 +3440,14 @@ job1:
Use `retry` to configure how many times a job is retried in
case of a failure.
-When a job fails and has `retry` configured, the job is processed again,
-up to the amount of times specified by the `retry` keyword.
+When a job fails, the job is processed again,
+until the limit specified by the `retry` keyword is reached.
-If `retry` is set to 2, and a job succeeds in a second run (first retry), it is not tried
-again. `retry` value has to be a positive integer, equal to or larger than 0, but
-less than or equal to 2 (two retries maximum, three runs in total).
+If `retry` is set to `2`, and a job succeeds in a second run (first retry), it is not retried.
+The `retry` value must be a positive integer, from `0` to `2`
+(two retries maximum, three runs in total).
-A simple example to retry in all failure cases:
+This example retries all failure cases:
```yaml
test:
@@ -3640,9 +3645,9 @@ You can use this keyword to create two different types of downstream pipelines:
- [Multi-project pipelines](../multi_project_pipelines.md#creating-multi-project-pipelines-from-gitlab-ciyml)
- [Child pipelines](../parent_child_pipelines.md)
-[Since GitLab 13.2](https://gitlab.com/gitlab-org/gitlab/-/issues/197140/), you can
-see which job triggered a downstream pipeline by hovering your mouse cursor over
-the downstream pipeline job in the [pipeline graph](../pipelines/index.md#visualize-pipelines).
+[In GitLab 13.2 and later](https://gitlab.com/gitlab-org/gitlab/-/issues/197140/), you can
+view which job triggered a downstream pipeline. In the [pipeline graph](../pipelines/index.md#visualize-pipelines),
+hover over the downstream pipeline job.
In [GitLab 13.5](https://gitlab.com/gitlab-org/gitlab/-/issues/201938) and later, you
can use [`when:manual`](#whenmanual) in the same job as `trigger`. In GitLab 13.4 and
@@ -3782,7 +3787,7 @@ trigger_job:
This setting can help keep your pipeline execution linear. In the example above, jobs from
subsequent stages wait for the triggered pipeline to successfully complete before
-starting, at the cost of reduced parallelization.
+starting, which reduces parallelization.
#### Trigger a pipeline by API call
@@ -3859,7 +3864,7 @@ to semaphores in other programming languages.
When the `resource_group` key is defined for a job in `.gitlab-ci.yml`,
job executions are mutually exclusive across different pipelines for the same project.
If multiple jobs belonging to the same resource group are enqueued simultaneously,
-only one of the jobs is picked by the runner, and the other jobs wait until the
+only one of the jobs is picked by the runner. The other jobs wait until the
`resource_group` is free.
Here is a simple example:
@@ -3870,9 +3875,7 @@ deploy-to-production:
resource_group: production
```
-In this case, if a `deploy-to-production` job is running in a pipeline, and a new
-`deploy-to-production` job is created in a different pipeline, it doesn't run until
-the currently running/pending `deploy-to-production` job finishes. As a result,
+In this case, two `deploy-to-production` jobs in two separate pipelines can never run at the same time. As a result,
you can ensure that concurrent deployments never happen to the production environment.
There can be multiple `resource_group`s defined per environment. A good use case for this
diff --git a/doc/user/admin_area/img/admin_wrench.png b/doc/user/admin_area/img/admin_wrench.png
deleted file mode 100644
index 17eee143e87..00000000000
--- a/doc/user/admin_area/img/admin_wrench.png
+++ /dev/null
Binary files differ
diff --git a/doc/user/admin_area/license.md b/doc/user/admin_area/license.md
index ecbc615f56a..c37a61d6748 100644
--- a/doc/user/admin_area/license.md
+++ b/doc/user/admin_area/license.md
@@ -8,130 +8,126 @@ type: howto
# Activate GitLab EE with a license **(STARTER ONLY)**
To activate all GitLab Enterprise Edition (EE) functionality, you need to upload
-a license. Once you've received your license from GitLab Inc., you can upload it
-by **signing into your GitLab instance as an admin** or add it at
+a license. After you've received your license from GitLab Inc., you can upload it
+by **signing into your GitLab instance as an admin** or adding it at
installation time.
-The license has the form of a base64 encoded ASCII text with a `.gitlab-license`
-extension and can be obtained when you [purchase one](https://about.gitlab.com/pricing/) or when you sign
-up for a [free trial](https://about.gitlab.com/free-trial/).
+The license is a base64-encoded ASCII text file with a `.gitlab-license`
+extension. You can obtain the file by [purchasing a license](https://about.gitlab.com/pricing/)
+or by signing up for a [free trial](https://about.gitlab.com/free-trial/).
-NOTE: **Note:**
As of GitLab Enterprise Edition 9.4.0, a newly-installed instance without an
-uploaded license will only have the Core features active. A trial license will
-activate all Ultimate features, but after
+uploaded license only has the Core features active. A trial license
+activates all Ultimate features, but after
[the trial expires](#what-happens-when-your-license-expires), some functionality
-will be locked.
+is locked.
## Uploading your license
The very first time you visit your GitLab EE installation signed in as an admin,
you should see a note urging you to upload a license with a link that takes you
-straight to **Admin Area > License**.
+to **Admin Area > License**.
Otherwise, you can:
-1. Navigate manually to the **Admin Area** by clicking the wrench icon in the menu bar.
+1. Navigate manually to the **Admin Area** by clicking the wrench (**{admin}**) icon in the menu bar.
- ![Admin Area icon](img/admin_wrench.png)
-
-1. And then going to the **License** tab and click on **Upload New License**.
+1. Navigate to the **License** tab, and click **Upload New License**.
![License Admin Area](img/license_admin_area.png)
-1. If you've received a `.gitlab-license` file, you should have already downloaded
- it in your local machine. You can then upload it directly by choosing the
- license file and clicking the **Upload license** button. In the image below,
- you can see that the selected license file is named `GitLab.gitlab-license`.
+ - *If you've received a `.gitlab-license` file,* you should have already downloaded
+ it in your local machine. You can then upload it directly by choosing the
+ license file and clicking the **Upload license** button. In the image below,
+ the selected license file is named `GitLab.gitlab-license`.
- ![Upload license](img/license_upload.png)
+ ![Upload license](img/license_upload.png)
- If you've received your license as plain text, you need to select the
- "Enter license key" option, copy the license, paste it into the "License key"
- field and click **Upload license**.
+ - *If you've received your license as plain text,* select the
+ **Enter license key** option, copy the license, paste it into the **License key**
+ field, and click **Upload license**.
## Add your license at install time
-A license can be automatically imported at install time, by placing a file named
-`Gitlab.gitlab-license` in `/etc/gitlab/` for Omnibus, or `config/` for source installations.
+A license can be automatically imported at install time by placing a file named
+`Gitlab.gitlab-license` in `/etc/gitlab/` for Omnibus GitLab, or `config/` for source installations.
-It is also possible to specify a custom location and filename for the license.
+You can also specify a custom location and filename for the license:
-Source installations should set the `GITLAB_LICENSE_FILE` environment
-variable with the path to a valid GitLab Enterprise Edition license.
+- Source installations should set the `GITLAB_LICENSE_FILE` environment
+ variable with the path to a valid GitLab Enterprise Edition license.
-```shell
-export GITLAB_LICENSE_FILE="/path/to/license/file"
-```
+ ```shell
+ export GITLAB_LICENSE_FILE="/path/to/license/file"
+ ```
-Omnibus installations should add this entry to `gitlab.rb`:
+- Omnibus GitLab installations should add this entry to `gitlab.rb`:
-```ruby
-gitlab_rails['initial_license_file'] = "/path/to/license/file"
-```
+ ```ruby
+ gitlab_rails['initial_license_file'] = "/path/to/license/file"
+ ```
CAUTION: **Caution:**
-These methods will only add a license at the time of installation. Use the
-Admin Area in the web user interface to renew or upgrade licenses.
+These methods only add a license at the time of installation. Use the
+**{admin}** **Admin Area** in the web user interface to renew or upgrade licenses.
---
-Once the license is uploaded, all GitLab Enterprise Edition functionality
-will be active until the end of the license period. When that period ends, the
+After the license is uploaded, all GitLab Enterprise Edition functionality
+is active until the end of the license period. When that period ends, the
instance will [fall back](#what-happens-when-your-license-expires) to Core-only
functionality.
-You can review the license details at any time in the License section of the
-Admin Area.
+You can review the license details at any time in the **License** section of the
+**Admin Area**.
![License details](img/license_details.png)
## Notification before the license expires
-One month before the license expires, a message informing when the expiration
-is due to, will start appearing to GitLab admins. Make sure that you update your
-license, otherwise you will miss all the paid features if it expires.
+One month before the license expires, a message informing about the expiration
+date is displayed to GitLab admins. Make sure that you update your
+license, otherwise you miss all the paid features if your license expires.
## What happens when your license expires
-In case your license expires, GitLab will lock down some features like Git pushes,
-issue creation, etc., and a message to inform of the expired license will be
-presented to all admins.
+In case your license expires, GitLab locks down some features like Git pushes,
+and issue creation, and displays a message to all admins to inform of the expired license.
-In order to get back all the previous functionality, a new license must be uploaded.
-To fall back to having only the Core features active, you'll need to delete the
+To get back all the previous functionality, you must upload a new license.
+To fall back to having only the Core features active, you must delete the
expired license(s).
### Remove a license
To remove a license from a self-managed instance:
-1. Go to the [Admin Area](index.md) (click the wrench in the top navigation bar).
+1. In the top navigation bar, click the **{admin}** wrench icon to navigate to the [Admin Area](index.md).
1. Click **License** in the left sidebar.
1. Click **Remove License**.
## License history
-It's possible to upload and view more than one license,
-but only the latest license will be used as the active license.
+You can upload and view more than one license,
+but only the latest license is used as the active license.
## Troubleshooting
### There is no License tab in the Admin Area
-If you originally installed Community Edition rather than Enterprise Edition you will need to
+If you originally installed Community Edition rather than Enterprise Edition you must
[upgrade to Enterprise Edition](../../update/README.md#community-to-enterprise-edition)
before uploading your license.
-GitLab.com users cannot upload and use a self-managed license. If you
-wish to use paid features on GitLab.com, a separate subscription may be
-[purchased](../../subscriptions/gitlab_com/index.md).
+GitLab.com users can't upload and use a self-managed license. If you
+want to use paid features on GitLab.com, you can
+[purchase a separate subscription](../../subscriptions/gitlab_com/index.md).
### Users exceed license limit upon renewal
-If you've added new users to your GitLab instance prior to renewal you may need to
-purchase additional seats to cover those users. If this is the case and a license
-without enough users is uploaded a message is displayed prompting you to purchase
+If you've added new users to your GitLab instance prior to renewal, you may need to
+purchase additional seats to cover those users. If this is the case, and a license
+without enough users is uploaded, GitLab displays a message prompting you to purchase
additional users. More information on how to determine the required number of users
and how to add additional seats can be found in the
[licensing FAQ](https://about.gitlab.com/pricing/licensing-faq/).
diff --git a/qa/qa/specs/features/browser_ui/3_create/repository/protocol_v2_push_http_spec.rb b/qa/qa/specs/features/browser_ui/3_create/repository/protocol_v2_push_http_spec.rb
index 8ac7285d70c..5781bf8a7f0 100644
--- a/qa/qa/specs/features/browser_ui/3_create/repository/protocol_v2_push_http_spec.rb
+++ b/qa/qa/specs/features/browser_ui/3_create/repository/protocol_v2_push_http_spec.rb
@@ -37,8 +37,10 @@ module QA
project.wait_for_push_new_branch
# Check that the push worked
- expect(page).to have_content(file_name)
- expect(page).to have_content(file_content)
+ Page::Project::Show.perform do |project_page|
+ expect(project_page).to have_file(file_name)
+ expect(project_page).to have_readme_content(file_content)
+ end
# And check that the correct Git protocol was used
expect(git_protocol_reported).to eq(git_protocol)
diff --git a/qa/qa/specs/features/browser_ui/3_create/repository/push_mirroring_lfs_over_http_spec.rb b/qa/qa/specs/features/browser_ui/3_create/repository/push_mirroring_lfs_over_http_spec.rb
index 83945a09587..2d86cfdbaf8 100644
--- a/qa/qa/specs/features/browser_ui/3_create/repository/push_mirroring_lfs_over_http_spec.rb
+++ b/qa/qa/specs/features/browser_ui/3_create/repository/push_mirroring_lfs_over_http_spec.rb
@@ -37,8 +37,10 @@ module QA
# Check that the target project has the commit from the source
target_project.visit!
- expect(page).to have_content('README.md')
- expect(page).to have_content('The rendered file could not be displayed because it is stored in LFS')
+ Page::Project::Show.perform do |project_page|
+ expect(project_page).to have_file('README.md')
+ expect(project_page).to have_readme_content('The rendered file could not be displayed because it is stored in LFS')
+ end
end
end
end
diff --git a/qa/qa/specs/features/browser_ui/3_create/repository/push_over_http_spec.rb b/qa/qa/specs/features/browser_ui/3_create/repository/push_over_http_spec.rb
index 8b6973e6cea..cf14017b7f1 100644
--- a/qa/qa/specs/features/browser_ui/3_create/repository/push_over_http_spec.rb
+++ b/qa/qa/specs/features/browser_ui/3_create/repository/push_over_http_spec.rb
@@ -36,8 +36,10 @@ module QA
project.visit!
- expect(page).to have_content('README.md')
- expect(page).to have_content("This is a test project named #{project.name}")
+ Page::Project::Show.perform do |project_page|
+ expect(project_page).to have_file('README.md')
+ expect(project_page).to have_readme_content("This is a test project named #{project.name}")
+ end
end
end
end
diff --git a/spec/features/boards/sidebar_spec.rb b/spec/features/boards/sidebar_spec.rb
index 4b4cb444903..332c90df6d7 100644
--- a/spec/features/boards/sidebar_spec.rb
+++ b/spec/features/boards/sidebar_spec.rb
@@ -229,7 +229,7 @@ RSpec.describe 'Issue Boards', :js do
end
context 'time tracking' do
- let(:compare_meter_tooltip) { find('.time-tracking .time-tracking-content .compare-meter')['data-original-title'] }
+ let(:compare_meter_tooltip) { find('.time-tracking .time-tracking-content .compare-meter')['title'] }
before do
issue2.timelogs.create(time_spent: 14400, user: user)
diff --git a/spec/frontend/sidebar/components/time_tracking/time_tracker_spec.js b/spec/frontend/sidebar/components/time_tracking/time_tracker_spec.js
index 1f028f74423..5307be0bf58 100644
--- a/spec/frontend/sidebar/components/time_tracking/time_tracker_spec.js
+++ b/spec/frontend/sidebar/components/time_tracking/time_tracker_spec.js
@@ -155,8 +155,7 @@ describe('Issuable Time Tracker', () => {
it('should show the correct tooltip text', done => {
Vue.nextTick(() => {
expect(vm.showComparisonState).toBe(true);
- const $title = vm.$el.querySelector('.time-tracking-content .compare-meter').dataset
- .originalTitle;
+ const $title = vm.$el.querySelector('.time-tracking-content .compare-meter').title;
expect($title).toBe('Time remaining: 26h 23m');
done();
diff --git a/spec/graphql/resolvers/group_milestones_resolver_spec.rb b/spec/graphql/resolvers/group_milestones_resolver_spec.rb
index 05d0ec38192..d8ff8e9c1f2 100644
--- a/spec/graphql/resolvers/group_milestones_resolver_spec.rb
+++ b/spec/graphql/resolvers/group_milestones_resolver_spec.rb
@@ -15,6 +15,12 @@ RSpec.describe Resolvers::GroupMilestonesResolver do
let_it_be(:now) { Time.now }
let_it_be(:group) { create(:group, :private) }
+ def args(**arguments)
+ satisfy("contain only #{arguments.inspect}") do |passed|
+ expect(passed.compact).to match(arguments)
+ end
+ end
+
before_all do
group.add_developer(current_user)
end
@@ -30,7 +36,7 @@ RSpec.describe Resolvers::GroupMilestonesResolver do
context 'without parameters' do
it 'calls MilestonesFinder to retrieve all milestones' do
expect(MilestonesFinder).to receive(:new)
- .with(ids: nil, group_ids: group.id, state: 'all', start_date: nil, end_date: nil)
+ .with(args(group_ids: group.id, state: 'all'))
.and_call_original
resolve_group_milestones
@@ -43,11 +49,22 @@ RSpec.describe Resolvers::GroupMilestonesResolver do
end_date = start_date + 1.hour
expect(MilestonesFinder).to receive(:new)
- .with(ids: nil, group_ids: group.id, state: 'closed', start_date: start_date, end_date: end_date)
+ .with(args(group_ids: group.id, state: 'closed', start_date: start_date, end_date: end_date))
.and_call_original
resolve_group_milestones(start_date: start_date, end_date: end_date, state: 'closed')
end
+
+ it 'understands the timeframe argument' do
+ start_date = now
+ end_date = start_date + 1.hour
+
+ expect(MilestonesFinder).to receive(:new)
+ .with(args(group_ids: group.id, state: 'closed', start_date: start_date, end_date: end_date))
+ .and_call_original
+
+ resolve_group_milestones(timeframe: { start: start_date, end: end_date }, state: 'closed')
+ end
end
context 'by ids' do
@@ -55,7 +72,7 @@ RSpec.describe Resolvers::GroupMilestonesResolver do
milestone = create(:milestone, group: group)
expect(MilestonesFinder).to receive(:new)
- .with(ids: [milestone.id.to_s], group_ids: group.id, state: 'all', start_date: nil, end_date: nil)
+ .with(args(ids: [milestone.id.to_s], group_ids: group.id, state: 'all'))
.and_call_original
resolve_group_milestones(ids: [milestone.to_global_id])
diff --git a/spec/graphql/resolvers/project_milestones_resolver_spec.rb b/spec/graphql/resolvers/project_milestones_resolver_spec.rb
index e0b250cfe7c..b641a54393e 100644
--- a/spec/graphql/resolvers/project_milestones_resolver_spec.rb
+++ b/spec/graphql/resolvers/project_milestones_resolver_spec.rb
@@ -13,13 +13,19 @@ RSpec.describe Resolvers::ProjectMilestonesResolver do
project.add_developer(current_user)
end
+ def args(**arguments)
+ satisfy("contain only #{arguments.inspect}") do |passed|
+ expect(passed.compact).to match(arguments)
+ end
+ end
+
def resolve_project_milestones(args = {}, context = { current_user: current_user })
resolve(described_class, obj: project, args: args, ctx: context)
end
it 'calls MilestonesFinder to retrieve all milestones' do
expect(MilestonesFinder).to receive(:new)
- .with(ids: nil, project_ids: project.id, state: 'all', start_date: nil, end_date: nil)
+ .with(args(project_ids: project.id, state: 'all'))
.and_call_original
resolve_project_milestones
@@ -36,7 +42,7 @@ RSpec.describe Resolvers::ProjectMilestonesResolver do
it 'calls MilestonesFinder with correct parameters' do
expect(MilestonesFinder).to receive(:new)
- .with(ids: nil, project_ids: project.id, group_ids: contain_exactly(group, parent_group), state: 'all', start_date: nil, end_date: nil)
+ .with(args(project_ids: project.id, group_ids: contain_exactly(group, parent_group), state: 'all'))
.and_call_original
resolve_project_milestones(include_ancestors: true)
@@ -48,7 +54,7 @@ RSpec.describe Resolvers::ProjectMilestonesResolver do
milestone = create(:milestone, project: project)
expect(MilestonesFinder).to receive(:new)
- .with(ids: [milestone.id.to_s], project_ids: project.id, state: 'all', start_date: nil, end_date: nil)
+ .with(args(ids: [milestone.id.to_s], project_ids: project.id, state: 'all'))
.and_call_original
resolve_project_milestones(ids: [milestone.to_global_id])
@@ -58,7 +64,7 @@ RSpec.describe Resolvers::ProjectMilestonesResolver do
context 'by state' do
it 'calls MilestonesFinder with correct parameters' do
expect(MilestonesFinder).to receive(:new)
- .with(ids: nil, project_ids: project.id, state: 'closed', start_date: nil, end_date: nil)
+ .with(args(project_ids: project.id, state: 'closed'))
.and_call_original
resolve_project_milestones(state: 'closed')
@@ -72,7 +78,7 @@ RSpec.describe Resolvers::ProjectMilestonesResolver do
end_date = Time.now + 5.days
expect(MilestonesFinder).to receive(:new)
- .with(ids: nil, project_ids: project.id, state: 'all', start_date: start_date, end_date: end_date)
+ .with(args(project_ids: project.id, state: 'all', start_date: start_date, end_date: end_date))
.and_call_original
resolve_project_milestones(start_date: start_date, end_date: end_date)
@@ -102,6 +108,51 @@ RSpec.describe Resolvers::ProjectMilestonesResolver do
end.to raise_error(Gitlab::Graphql::Errors::ArgumentError, /Both startDate and endDate/)
end
end
+
+ context 'when passing a timeframe' do
+ it 'calls MilestonesFinder with correct parameters' do
+ start_date = Time.now
+ end_date = Time.now + 5.days
+
+ expect(MilestonesFinder).to receive(:new)
+ .with(args(project_ids: project.id, state: 'all', start_date: start_date, end_date: end_date))
+ .and_call_original
+
+ resolve_project_milestones(timeframe: { start: start_date, end: end_date })
+ end
+ end
+ end
+
+ context 'when title is present' do
+ it 'calls MilestonesFinder with correct parameters' do
+ expect(MilestonesFinder).to receive(:new)
+ .with(args(title: '13.5', state: 'all', project_ids: project.id))
+ .and_call_original
+
+ resolve_project_milestones(title: '13.5')
+ end
+ end
+
+ context 'when search_title is present' do
+ it 'calls MilestonesFinder with correct parameters' do
+ expect(MilestonesFinder).to receive(:new)
+ .with(args(search_title: '13', state: 'all', project_ids: project.id))
+ .and_call_original
+
+ resolve_project_milestones(search_title: '13')
+ end
+ end
+
+ context 'when containing date is present' do
+ it 'calls MilestonesFinder with correct parameters' do
+ t = Time.now
+
+ expect(MilestonesFinder).to receive(:new)
+ .with(args(containing_date: t, state: 'all', project_ids: project.id))
+ .and_call_original
+
+ resolve_project_milestones(containing_date: t)
+ end
end
context 'when user cannot read milestones' do
diff --git a/spec/graphql/types/range_input_type_spec.rb b/spec/graphql/types/range_input_type_spec.rb
new file mode 100644
index 00000000000..aa6fd72cf13
--- /dev/null
+++ b/spec/graphql/types/range_input_type_spec.rb
@@ -0,0 +1,43 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+RSpec.describe ::Types::RangeInputType do
+ let(:of_integer) { ::GraphQL::INT_TYPE }
+
+ context 'parameterized on Integer' do
+ let(:type) { described_class[of_integer] }
+
+ it 'accepts start and end' do
+ input = { start: 1, end: 10 }
+ output = { start: 1, end: 10 }
+
+ expect(type.coerce_isolated_input(input)).to eq(output)
+ end
+
+ it 'rejects inverted ranges' do
+ input = { start: 10, end: 1 }
+
+ expect { type.coerce_isolated_input(input) }.to raise_error(Gitlab::Graphql::Errors::ArgumentError)
+ end
+ end
+
+ it 'follows expected subtyping relationships for instances' do
+ context = GraphQL::Query::Context.new(
+ query: OpenStruct.new(schema: nil),
+ values: {},
+ object: nil
+ )
+ instance = described_class[of_integer].new(context: context, defaults_used: [], ruby_kwargs: {})
+
+ expect(instance).to be_a_kind_of(described_class)
+ expect(instance).to be_a_kind_of(described_class[of_integer])
+ expect(instance).not_to be_a_kind_of(described_class[GraphQL::ID_TYPE])
+ end
+
+ it 'follows expected subtyping relationships for classes' do
+ expect(described_class[of_integer]).to be < described_class
+ expect(described_class[of_integer]).not_to be < described_class[GraphQL::ID_TYPE]
+ expect(described_class[of_integer]).not_to be < described_class[of_integer, false]
+ end
+end
diff --git a/spec/graphql/types/timeframe_type_spec.rb b/spec/graphql/types/timeframe_type_spec.rb
new file mode 100644
index 00000000000..dfde3242897
--- /dev/null
+++ b/spec/graphql/types/timeframe_type_spec.rb
@@ -0,0 +1,38 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+RSpec.describe GitlabSchema.types['Timeframe'] do
+ let(:input) { { start: "2018-06-04", end: "2020-10-06" } }
+ let(:output) { { start: Date.parse(input[:start]), end: Date.parse(input[:end]) } }
+
+ it 'coerces ISO-dates into Time objects' do
+ expect(described_class.coerce_isolated_input(input)).to eq(output)
+ end
+
+ it 'rejects invalid input' do
+ input[:start] = 'foo'
+
+ expect { described_class.coerce_isolated_input(input) }
+ .to raise_error(GraphQL::CoercionError)
+ end
+
+ it 'accepts times as input' do
+ with_time = input.merge(start: '2018-06-04T13:48:14Z')
+
+ expect(described_class.coerce_isolated_input(with_time)).to eq(output)
+ end
+
+ it 'requires both ends of the range' do
+ types = described_class.arguments.slice('start', 'end').values.map(&:type)
+
+ expect(types).to all(be_non_null)
+ end
+
+ it 'rejects invalid range' do
+ input.merge!(start: input[:end], end: input[:start])
+
+ expect { described_class.coerce_isolated_input(input) }
+ .to raise_error(::Gitlab::Graphql::Errors::ArgumentError, 'start must be before end')
+ end
+end
diff --git a/spec/requests/api/graphql/project/milestones_spec.rb b/spec/requests/api/graphql/project/milestones_spec.rb
new file mode 100644
index 00000000000..2fede4c7285
--- /dev/null
+++ b/spec/requests/api/graphql/project/milestones_spec.rb
@@ -0,0 +1,202 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+RSpec.describe 'getting milestone listings nested in a project' do
+ include GraphqlHelpers
+
+ let_it_be(:today) { Time.now.utc.to_date }
+ let_it_be(:project) { create(:project, :repository, :public) }
+ let_it_be(:current_user) { create(:user) }
+
+ let_it_be(:no_dates) { create(:milestone, project: project, title: 'no dates') }
+ let_it_be(:no_end) { create(:milestone, project: project, title: 'no end', start_date: today - 10.days) }
+ let_it_be(:no_start) { create(:milestone, project: project, title: 'no start', due_date: today - 5.days) }
+ let_it_be(:fully_past) { create(:milestone, project: project, title: 'past', start_date: today - 10.days, due_date: today - 5.days) }
+ let_it_be(:covers_today) { create(:milestone, project: project, title: 'present', start_date: today - 5.days, due_date: today + 5.days) }
+ let_it_be(:fully_future) { create(:milestone, project: project, title: 'future', start_date: today + 5.days, due_date: today + 10.days) }
+ let_it_be(:closed) { create(:milestone, :closed, project: project) }
+
+ let(:results) { graphql_data_at(:project, :milestones, :nodes) }
+
+ let(:search_params) { nil }
+
+ def query_milestones(fields)
+ graphql_query_for(
+ :project,
+ { full_path: project.full_path },
+ query_graphql_field(:milestones, search_params, [
+ query_graphql_field(:nodes, nil, %i[id title])
+ ])
+ )
+ end
+
+ def result_list(expected)
+ expected.map do |milestone|
+ a_hash_including('id' => global_id_of(milestone))
+ end
+ end
+
+ let(:query) do
+ query_milestones(all_graphql_fields_for('Milestone', max_depth: 1))
+ end
+
+ let(:all_milestones) do
+ [no_dates, no_end, no_start, fully_past, fully_future, covers_today, closed]
+ end
+
+ it_behaves_like 'a working graphql query' do
+ before do
+ post_graphql(query, current_user: current_user)
+ end
+ end
+
+ shared_examples 'searching with parameters' do
+ it 'finds the right milestones' do
+ post_graphql(query, current_user: current_user)
+
+ expect(results).to match_array(result_list(expected))
+ end
+ end
+
+ context 'there are no search params' do
+ let(:search_params) { nil }
+ let(:expected) { all_milestones }
+
+ it_behaves_like 'searching with parameters'
+ end
+
+ context 'the search params do not match anything' do
+ let(:search_params) { { title: 'wibble' } }
+ let(:expected) { [] }
+
+ it_behaves_like 'searching with parameters'
+ end
+
+ context 'searching by state:closed' do
+ let(:search_params) { { state: :closed } }
+ let(:expected) { [closed] }
+
+ it_behaves_like 'searching with parameters'
+ end
+
+ context 'searching by state:active' do
+ let(:search_params) { { state: :active } }
+ let(:expected) { all_milestones - [closed] }
+
+ it_behaves_like 'searching with parameters'
+ end
+
+ context 'searching by title' do
+ let(:search_params) { { title: 'no start' } }
+ let(:expected) { [no_start] }
+
+ it_behaves_like 'searching with parameters'
+ end
+
+ context 'searching by search_title' do
+ let(:search_params) { { search_title: 'no' } }
+ let(:expected) { [no_dates, no_start, no_end] }
+
+ it_behaves_like 'searching with parameters'
+ end
+
+ context 'searching by containing_date' do
+ let(:search_params) { { containing_date: (today - 7.days).iso8601 } }
+ let(:expected) { [no_start, no_end, fully_past] }
+
+ it_behaves_like 'searching with parameters'
+ end
+
+ context 'searching by containing_date = today' do
+ let(:search_params) { { containing_date: today.iso8601 } }
+ let(:expected) { [no_end, covers_today] }
+
+ it_behaves_like 'searching with parameters'
+ end
+
+ context 'searching by custom range' do
+ let(:expected) { [no_end, fully_future] }
+ let(:search_params) do
+ {
+ start_date: (today + 6.days).iso8601,
+ end_date: (today + 7.days).iso8601
+ }
+ end
+
+ it_behaves_like 'searching with parameters'
+ end
+
+ context 'using timeframe argument' do
+ let(:expected) { [no_end, fully_future] }
+ let(:search_params) do
+ {
+ timeframe: {
+ start: (today + 6.days).iso8601,
+ end: (today + 7.days).iso8601
+ }
+ }
+ end
+
+ it_behaves_like 'searching with parameters'
+ end
+
+ describe 'timeframe validations' do
+ let(:vars) do
+ {
+ path: project.full_path,
+ start: (today + 6.days).iso8601,
+ end: (today + 7.days).iso8601
+ }
+ end
+
+ it_behaves_like 'a working graphql query' do
+ before do
+ query = <<~GQL
+ query($path: ID!, $start: Date!, $end: Date!) {
+ project(fullPath: $path) {
+ milestones(timeframe: { start: $start, end: $end }) {
+ nodes { id }
+ }
+ }
+ }
+ GQL
+
+ post_graphql(query, current_user: current_user, variables: vars)
+ end
+ end
+
+ it 'is invalid to provide timeframe and start_date/end_date' do
+ query = <<~GQL
+ query($path: ID!, $tstart: Date!, $tend: Date!, $start: Time!, $end: Time!) {
+ project(fullPath: $path) {
+ milestones(timeframe: { start: $tstart, end: $tend }, startDate: $start, endDate: $end) {
+ nodes { id }
+ }
+ }
+ }
+ GQL
+
+ post_graphql(query, current_user: current_user,
+ variables: vars.merge(vars.transform_keys { |k| :"t#{k}" }))
+
+ expect(graphql_errors).to contain_exactly(a_hash_including('message' => include('deprecated in favor of timeframe')))
+ end
+
+ it 'is invalid to invert the timeframe arguments' do
+ query = <<~GQL
+ query($path: ID!, $start: Date!, $end: Date!) {
+ project(fullPath: $path) {
+ milestones(timeframe: { start: $end, end: $start }) {
+ nodes { id }
+ }
+ }
+ }
+ GQL
+
+ post_graphql(query, current_user: current_user, variables: vars)
+
+ expect(graphql_errors).to contain_exactly(a_hash_including('message' => include('start must be before end')))
+ end
+ end
+end
diff --git a/spec/support/helpers/graphql_helpers.rb b/spec/support/helpers/graphql_helpers.rb
index 35eba81cfd6..db769041f1e 100644
--- a/spec/support/helpers/graphql_helpers.rb
+++ b/spec/support/helpers/graphql_helpers.rb
@@ -219,7 +219,7 @@ module GraphqlHelpers
def as_graphql_literal(value)
case value
when Array then "[#{value.map { |v| as_graphql_literal(v) }.join(',')}]"
- when Hash then "{#{value.map { |k, v| "#{k}:#{as_graphql_literal(v)}" }.join(',')}}"
+ when Hash then "{#{attributes_to_graphql(value)}}"
when Integer, Float then value.to_s
when String then "\"#{value.gsub(/"/, '\\"')}\""
when Symbol then value
diff --git a/spec/support/shared_examples/models/concerns/timebox_shared_examples.rb b/spec/support/shared_examples/models/concerns/timebox_shared_examples.rb
index d199bae4170..f91e4bd8cf7 100644
--- a/spec/support/shared_examples/models/concerns/timebox_shared_examples.rb
+++ b/spec/support/shared_examples/models/concerns/timebox_shared_examples.rb
@@ -9,6 +9,11 @@ RSpec.shared_examples 'a timebox' do |timebox_type|
let(:user) { create(:user) }
let(:timebox_table_name) { timebox_type.to_s.pluralize.to_sym }
+ # Values implementions can override
+ let(:mid_point) { Time.now.utc.to_date }
+ let(:open_on_left) { nil }
+ let(:open_on_right) { nil }
+
describe 'modules' do
context 'with a project' do
it_behaves_like 'AtomicInternalId' do
@@ -240,4 +245,85 @@ RSpec.shared_examples 'a timebox' do |timebox_type|
expect(timebox.to_ability_name).to eq(timebox_type.to_s)
end
end
+
+ describe '.within_timeframe' do
+ let(:factory) { timebox_type }
+ let(:min_date) { mid_point - 10.days }
+ let(:max_date) { mid_point + 10.days }
+
+ def box(from, to)
+ create(factory, *timebox_args,
+ start_date: from || open_on_left,
+ due_date: to || open_on_right)
+ end
+
+ it 'can find overlapping timeboxes' do
+ fully_open = box(nil, nil)
+ # ----| ................ # Not overlapping
+ non_overlapping_open_on_left = box(nil, min_date - 1.day)
+ # |--| ................ # Not overlapping
+ non_overlapping_closed_on_left = box(min_date - 2.days, min_date - 1.day)
+ # ------|............... # Overlapping
+ overlapping_open_on_left_just = box(nil, min_date)
+ # -----------------------| # Overlapping
+ overlapping_open_on_left_fully = box(nil, max_date + 1.day)
+ # ---------|............ # Overlapping
+ overlapping_open_on_left_partial = box(nil, min_date + 1.day)
+ # |-----|............ # Overlapping
+ overlapping_closed_partial = box(min_date - 1.day, min_date + 1.day)
+ # |--------------| # Overlapping
+ exact_match = box(min_date, max_date)
+ # |--------------------| # Overlapping
+ larger = box(min_date - 1.day, max_date + 1.day)
+ # ...|-----|...... # Overlapping
+ smaller = box(min_date + 1.day, max_date - 1.day)
+ # .........|-----| # Overlapping
+ at_end = box(max_date - 1.day, max_date)
+ # .........|--------- # Overlapping
+ at_end_open = box(max_date - 1.day, nil)
+ # |-------------------- # Overlapping
+ cover_from_left = box(min_date - 1.day, nil)
+ # .........|--------| # Overlapping
+ cover_from_middle_closed = box(max_date - 1.day, max_date + 1.day)
+ # ...............|--| # Overlapping
+ overlapping_at_end_just = box(max_date, max_date + 1.day)
+ # ............... |-| # Not Overlapping
+ not_overlapping_at_right_closed = box(max_date + 1.day, max_date + 2.days)
+ # ............... |-- # Not Overlapping
+ not_overlapping_at_right_open = box(max_date + 1.day, nil)
+
+ matches = described_class.within_timeframe(min_date, max_date)
+
+ expect(matches).to include(
+ overlapping_open_on_left_just,
+ overlapping_open_on_left_fully,
+ overlapping_open_on_left_partial,
+ overlapping_closed_partial,
+ exact_match,
+ larger,
+ smaller,
+ at_end,
+ at_end_open,
+ cover_from_left,
+ cover_from_middle_closed,
+ overlapping_at_end_just
+ )
+
+ expect(matches).not_to include(
+ non_overlapping_open_on_left,
+ non_overlapping_closed_on_left,
+ not_overlapping_at_right_closed,
+ not_overlapping_at_right_open
+ )
+
+ # Whether we match the 'fully-open' range depends on whether
+ # it is in fact open (i.e. whether the class allows infinite
+ # ranges)
+ if open_on_left.nil? && open_on_right.nil?
+ expect(matches).not_to include(fully_open)
+ else
+ expect(matches).to include(fully_open)
+ end
+ end
+ end
end