summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--app/assets/javascripts/monitoring/components/dashboard.vue4
-rw-r--r--app/assets/javascripts/monitoring/components/embed.vue15
-rw-r--r--app/assets/javascripts/monitoring/components/panel_type.vue7
-rw-r--r--app/assets/javascripts/pages/snippets/show/index.js12
-rw-r--r--app/controllers/projects/merge_requests/diffs_controller.rb1
-rw-r--r--app/mailers/emails/profile.rb2
-rw-r--r--app/models/ci/legacy_stage.rb1
-rw-r--r--app/models/merge_request.rb4
-rw-r--r--app/models/personal_access_token.rb2
-rw-r--r--app/presenters/ci/legacy_stage_presenter.rb31
-rw-r--r--app/views/admin/application_settings/_account_and_limit.html.haml3
-rw-r--r--app/views/projects/stage/_stage.html.haml6
-rw-r--r--app/views/shared/_personal_access_tokens_form.html.haml3
-rw-r--r--app/views/snippets/show.html.haml17
-rw-r--r--changelogs/unreleased/preserve-merge-train-rows-after-merge.yml5
-rw-r--r--config/sidekiq_queues.yml2
-rw-r--r--db/migrate/20190920122420_add_max_personal_access_token_lifetime_to_application_settings.rb9
-rw-r--r--db/migrate/20191106144901_add_state_to_merge_trains.rb18
-rw-r--r--db/migrate/20191112105448_add_index_on_personal_access_tokens_user_id_and_expires_at.rb18
-rw-r--r--db/migrate/20191118155702_add_index_on_status_to_merge_trains.rb20
-rw-r--r--db/schema.rb5
-rw-r--r--doc/api/settings.md1
-rw-r--r--doc/ci/environments.md35
-rw-r--r--doc/ci/yaml/README.md32
-rw-r--r--doc/user/admin_area/settings/account_and_limit_settings.md32
-rw-r--r--lib/gitlab/diff/file_collection/merge_request_diff.rb39
-rw-r--r--lib/gitlab/diff/file_collection/merge_request_diff_base.rb38
-rw-r--r--lib/gitlab/gon_helper.rb1
-rw-r--r--locale/gitlab.pot26
-rw-r--r--spec/controllers/projects/merge_requests/diffs_controller_spec.rb12
-rw-r--r--spec/features/snippets/internal_snippet_spec.rb4
-rw-r--r--spec/features/snippets/notes_on_personal_snippets_spec.rb1
-rw-r--r--spec/features/snippets/private_snippets_spec.rb1
-rw-r--r--spec/features/snippets/public_snippets_spec.rb4
-rw-r--r--spec/features/snippets/show_spec.rb4
-rw-r--r--spec/features/snippets/spam_snippets_spec.rb1
-rw-r--r--spec/features/snippets/user_creates_snippet_spec.rb1
-rw-r--r--spec/features/snippets/user_deletes_snippet_spec.rb2
-rw-r--r--spec/features/snippets/user_edits_snippet_spec.rb1
-rw-r--r--spec/features/snippets_spec.rb29
-rw-r--r--spec/frontend/monitoring/embed/embed_spec.js10
-rw-r--r--spec/frontend/monitoring/panel_type_spec.js4
-rw-r--r--spec/lib/gitlab/diff/file_collection/merge_request_diff_batch_spec.rb4
-rw-r--r--spec/lib/gitlab/diff/file_collection/merge_request_diff_spec.rb7
-rw-r--r--spec/presenters/ci/legacy_stage_presenter_spec.rb31
-rw-r--r--spec/support/shared_examples/diff_file_collections.rb42
46 files changed, 418 insertions, 129 deletions
diff --git a/app/assets/javascripts/monitoring/components/dashboard.vue b/app/assets/javascripts/monitoring/components/dashboard.vue
index 9e376a52702..fb6f5dc73b8 100644
--- a/app/assets/javascripts/monitoring/components/dashboard.vue
+++ b/app/assets/javascripts/monitoring/components/dashboard.vue
@@ -18,8 +18,6 @@ import Icon from '~/vue_shared/components/icon.vue';
import { getParameterValues, mergeUrlParams, redirectTo } from '~/lib/utils/url_utility';
import invalidUrl from '~/lib/utils/invalid_url';
import DateTimePicker from './date_time_picker/date_time_picker.vue';
-import MonitorTimeSeriesChart from './charts/time_series.vue';
-import MonitorSingleStatChart from './charts/single_stat.vue';
import GraphGroup from './graph_group.vue';
import EmptyState from './empty_state.vue';
import TrackEventDirective from '~/vue_shared/directives/track_event';
@@ -28,8 +26,6 @@ import { getTimeDiff, isValidDate, getAddMetricTrackingOptions } from '../utils'
export default {
components: {
VueDraggable,
- MonitorTimeSeriesChart,
- MonitorSingleStatChart,
PanelType,
GraphGroup,
EmptyState,
diff --git a/app/assets/javascripts/monitoring/components/embed.vue b/app/assets/javascripts/monitoring/components/embed.vue
index 581b2093d44..a5c933a0071 100644
--- a/app/assets/javascripts/monitoring/components/embed.vue
+++ b/app/assets/javascripts/monitoring/components/embed.vue
@@ -1,8 +1,8 @@
<script>
import { mapActions, mapState } from 'vuex';
import { getParameterValues, removeParams } from '~/lib/utils/url_utility';
+import PanelType from 'ee_else_ce/monitoring/components/panel_type.vue';
import GraphGroup from './graph_group.vue';
-import MonitorTimeSeriesChart from './charts/time_series.vue';
import { sidebarAnimationDuration } from '../constants';
import { getTimeDiff } from '../utils';
@@ -11,7 +11,7 @@ let sidebarMutationObserver;
export default {
components: {
GraphGroup,
- MonitorTimeSeriesChart,
+ PanelType,
},
props: {
dashboardUrl: {
@@ -92,16 +92,13 @@ export default {
<template>
<div class="metrics-embed" :class="{ 'd-inline-flex col-lg-6 p-0': isSingleChart }">
<div v-if="charts.length" class="row w-100 m-n2 pb-4">
- <monitor-time-series-chart
- v-for="graphData in charts"
- :key="graphData.title"
+ <panel-type
+ v-for="(graphData, graphIndex) in charts"
+ :key="`panel-type-${graphIndex}`"
class="w-100"
+ clipboard-text=""
:graph-data="graphData"
- :container-width="elWidth"
:group-id="dashboardUrl"
- :project-path="null"
- :show-border="true"
- :single-embed="isSingleChart"
/>
</div>
</div>
diff --git a/app/assets/javascripts/monitoring/components/panel_type.vue b/app/assets/javascripts/monitoring/components/panel_type.vue
index fab1dd0f981..080fe6f2b4b 100644
--- a/app/assets/javascripts/monitoring/components/panel_type.vue
+++ b/app/assets/javascripts/monitoring/components/panel_type.vue
@@ -47,6 +47,11 @@ export default {
required: false,
default: '',
},
+ groupId: {
+ type: String,
+ required: false,
+ default: 'panel-type-chart',
+ },
},
computed: {
...mapState('monitoringDashboard', ['deploymentData', 'projectPath']),
@@ -117,7 +122,7 @@ export default {
:deployment-data="deploymentData"
:project-path="projectPath"
:thresholds="getGraphAlertValues(graphData.metrics)"
- group-id="panel-type-chart"
+ :group-id="groupId"
>
<div class="d-flex align-items-center">
<alert-widget
diff --git a/app/assets/javascripts/pages/snippets/show/index.js b/app/assets/javascripts/pages/snippets/show/index.js
index 5efcc901fde..26936110402 100644
--- a/app/assets/javascripts/pages/snippets/show/index.js
+++ b/app/assets/javascripts/pages/snippets/show/index.js
@@ -5,11 +5,9 @@ import initNotes from '~/init_notes';
import snippetEmbed from '~/snippet/snippet_embed';
document.addEventListener('DOMContentLoaded', () => {
- if (!gon.features.snippetsVue) {
- new LineHighlighter(); // eslint-disable-line no-new
- new BlobViewer(); // eslint-disable-line no-new
- initNotes();
- new ZenMode(); // eslint-disable-line no-new
- snippetEmbed();
- }
+ new LineHighlighter(); // eslint-disable-line no-new
+ new BlobViewer(); // eslint-disable-line no-new
+ initNotes();
+ new ZenMode(); // eslint-disable-line no-new
+ snippetEmbed();
});
diff --git a/app/controllers/projects/merge_requests/diffs_controller.rb b/app/controllers/projects/merge_requests/diffs_controller.rb
index 42f9c0522a3..5d458a229f2 100644
--- a/app/controllers/projects/merge_requests/diffs_controller.rb
+++ b/app/controllers/projects/merge_requests/diffs_controller.rb
@@ -28,6 +28,7 @@ class Projects::MergeRequests::DiffsController < Projects::MergeRequests::Applic
positions = @merge_request.note_positions_for_paths(diffs.diff_file_paths, current_user)
diffs.unfold_diff_files(positions.unfoldable)
+ diffs.write_cache
options = {
merge_request: @merge_request,
diff --git a/app/mailers/emails/profile.rb b/app/mailers/emails/profile.rb
index 2ea1aea1f51..c368e6c8ac7 100644
--- a/app/mailers/emails/profile.rb
+++ b/app/mailers/emails/profile.rb
@@ -34,3 +34,5 @@ module Emails
# rubocop: enable CodeReuse/ActiveRecord
end
end
+
+Emails::Profile.prepend_if_ee('EE::Emails::Profile')
diff --git a/app/models/ci/legacy_stage.rb b/app/models/ci/legacy_stage.rb
index 2fd369c9aff..0a67a652e22 100644
--- a/app/models/ci/legacy_stage.rb
+++ b/app/models/ci/legacy_stage.rb
@@ -5,6 +5,7 @@ module Ci
# We should migrate this object to actual database record in the future
class LegacyStage
include StaticModel
+ include Presentable
attr_reader :pipeline, :name
diff --git a/app/models/merge_request.rb b/app/models/merge_request.rb
index 780bbe75485..f4dfd9128fa 100644
--- a/app/models/merge_request.rb
+++ b/app/models/merge_request.rb
@@ -27,8 +27,6 @@ class MergeRequest < ApplicationRecord
SORTING_PREFERENCE_FIELD = :merge_requests_sort
- prepend_if_ee('::EE::MergeRequest') # rubocop: disable Cop/InjectEnterpriseEditionModule
-
belongs_to :target_project, class_name: "Project"
belongs_to :source_project, class_name: "Project"
belongs_to :merge_user, class_name: "User"
@@ -1531,3 +1529,5 @@ class MergeRequest < ApplicationRecord
Gitlab::EtagCaching::Store.new.touch(key)
end
end
+
+MergeRequest.prepend_if_ee('::EE::MergeRequest')
diff --git a/app/models/personal_access_token.rb b/app/models/personal_access_token.rb
index 7ae431eaad7..770fc2b5c8f 100644
--- a/app/models/personal_access_token.rb
+++ b/app/models/personal_access_token.rb
@@ -70,3 +70,5 @@ class PersonalAccessToken < ApplicationRecord
"gitlab:personal_access_token:#{user_id}"
end
end
+
+PersonalAccessToken.prepend_if_ee('EE::PersonalAccessToken')
diff --git a/app/presenters/ci/legacy_stage_presenter.rb b/app/presenters/ci/legacy_stage_presenter.rb
new file mode 100644
index 00000000000..2a60b1280da
--- /dev/null
+++ b/app/presenters/ci/legacy_stage_presenter.rb
@@ -0,0 +1,31 @@
+# frozen_string_literal: true
+
+module Ci
+ class LegacyStagePresenter < Gitlab::View::Presenter::Delegated
+ presents :legacy_stage
+
+ def latest_ordered_statuses
+ preload_statuses(legacy_stage.statuses.latest_ordered)
+ end
+
+ def retried_ordered_statuses
+ preload_statuses(legacy_stage.statuses.retried_ordered)
+ end
+
+ private
+
+ def preload_statuses(statuses)
+ statuses.tap do |statuses|
+ # rubocop: disable CodeReuse/ActiveRecord
+ ActiveRecord::Associations::Preloader.new.preload(preloadable_statuses(statuses), :tags)
+ # rubocop: enable CodeReuse/ActiveRecord
+ end
+ end
+
+ def preloadable_statuses(statuses)
+ statuses.reject do |status|
+ status.instance_of?(::GenericCommitStatus) || status.instance_of?(::Ci::Bridge)
+ end
+ end
+ end
+end
diff --git a/app/views/admin/application_settings/_account_and_limit.html.haml b/app/views/admin/application_settings/_account_and_limit.html.haml
index 4358365504a..6b95c0f40c5 100644
--- a/app/views/admin/application_settings/_account_and_limit.html.haml
+++ b/app/views/admin/application_settings/_account_and_limit.html.haml
@@ -23,6 +23,9 @@
= f.label :session_expire_delay, _('Session duration (minutes)'), class: 'label-light'
= f.number_field :session_expire_delay, class: 'form-control'
%span.form-text.text-muted#session_expire_delay_help_block= _('GitLab restart is required to apply changes')
+
+ = render_if_exists 'admin/application_settings/personal_access_token_expiration_policy', form: f
+
.form-group
= f.label :user_oauth_applications, _('User OAuth applications'), class: 'label-bold'
.form-check
diff --git a/app/views/projects/stage/_stage.html.haml b/app/views/projects/stage/_stage.html.haml
index f93994bebe3..387c8fb3234 100644
--- a/app/views/projects/stage/_stage.html.haml
+++ b/app/views/projects/stage/_stage.html.haml
@@ -1,3 +1,5 @@
+- stage = stage.present(current_user: current_user)
+
%tr
%th{ colspan: 10 }
%strong
@@ -6,8 +8,8 @@
= ci_icon_for_status(stage.status)
&nbsp;
= stage.name.titleize
-= render stage.statuses.latest_ordered, stage: false, ref: false, pipeline_link: false, allow_retry: true
-= render stage.statuses.retried_ordered, stage: false, ref: false, pipeline_link: false, retried: true
+= render stage.latest_ordered_statuses, stage: false, ref: false, pipeline_link: false, allow_retry: true
+= render stage.retried_ordered_statuses, stage: false, ref: false, pipeline_link: false, retried: true
%tr
%td{ colspan: 10 }
&nbsp;
diff --git a/app/views/shared/_personal_access_tokens_form.html.haml b/app/views/shared/_personal_access_tokens_form.html.haml
index ca0b473addf..16f8a692635 100644
--- a/app/views/shared/_personal_access_tokens_form.html.haml
+++ b/app/views/shared/_personal_access_tokens_form.html.haml
@@ -18,6 +18,9 @@
.form-group.col-md-6
= f.label :expires_at, _('Expires at'), class: 'label-bold'
.input-icon-wrapper
+
+ = render_if_exists 'personal_access_tokens/callout_max_personal_access_token_lifetime'
+
= f.text_field :expires_at, class: "datepicker form-control", placeholder: 'YYYY-MM-DD'
.form-group
diff --git a/app/views/snippets/show.html.haml b/app/views/snippets/show.html.haml
index c77b05e3ea8..36b4e00e8d5 100644
--- a/app/views/snippets/show.html.haml
+++ b/app/views/snippets/show.html.haml
@@ -4,16 +4,13 @@
- breadcrumb_title @snippet.to_reference
- page_title "#{@snippet.title} (#{@snippet.to_reference})", _("Snippets")
-- if Feature.enabled?(:snippets_vue)
- #js-snippet-view{ 'data-qa-selector': 'snippet_view' }
-- else
- = render 'shared/snippets/header'
+= render 'shared/snippets/header'
- .personal-snippets
- %article.file-holder.snippet-file-content
- = render 'shared/snippets/blob'
+.personal-snippets
+ %article.file-holder.snippet-file-content
+ = render 'shared/snippets/blob'
- .row-content-block.top-block.content-component-block
- = render 'award_emoji/awards_block', awardable: @snippet, inline: true
+ .row-content-block.top-block.content-component-block
+ = render 'award_emoji/awards_block', awardable: @snippet, inline: true
- #notes.limited-width-notes= render "shared/notes/notes_with_form", :autocomplete => false
+ #notes.limited-width-notes= render "shared/notes/notes_with_form", :autocomplete => false
diff --git a/changelogs/unreleased/preserve-merge-train-rows-after-merge.yml b/changelogs/unreleased/preserve-merge-train-rows-after-merge.yml
new file mode 100644
index 00000000000..3441ebbbf1b
--- /dev/null
+++ b/changelogs/unreleased/preserve-merge-train-rows-after-merge.yml
@@ -0,0 +1,5 @@
+---
+title: Preserve merge train history
+merge_request: 19864
+author:
+type: changed
diff --git a/config/sidekiq_queues.yml b/config/sidekiq_queues.yml
index b4be61d8a3d..063a3cc2b5b 100644
--- a/config/sidekiq_queues.yml
+++ b/config/sidekiq_queues.yml
@@ -123,3 +123,5 @@
- [refresh_license_compliance_checks, 2]
- [design_management_new_version, 1]
- [epics, 2]
+ - [personal_access_tokens, 1]
+
diff --git a/db/migrate/20190920122420_add_max_personal_access_token_lifetime_to_application_settings.rb b/db/migrate/20190920122420_add_max_personal_access_token_lifetime_to_application_settings.rb
new file mode 100644
index 00000000000..5a6e810dede
--- /dev/null
+++ b/db/migrate/20190920122420_add_max_personal_access_token_lifetime_to_application_settings.rb
@@ -0,0 +1,9 @@
+# frozen_string_literal: true
+
+class AddMaxPersonalAccessTokenLifetimeToApplicationSettings < ActiveRecord::Migration[5.2]
+ DOWNTIME = false
+
+ def change
+ add_column :application_settings, :max_personal_access_token_lifetime, :integer
+ end
+end
diff --git a/db/migrate/20191106144901_add_state_to_merge_trains.rb b/db/migrate/20191106144901_add_state_to_merge_trains.rb
new file mode 100644
index 00000000000..e2256705f53
--- /dev/null
+++ b/db/migrate/20191106144901_add_state_to_merge_trains.rb
@@ -0,0 +1,18 @@
+# frozen_string_literal: true
+
+class AddStateToMergeTrains < ActiveRecord::Migration[5.2]
+ include Gitlab::Database::MigrationHelpers
+
+ MERGE_TRAIN_STATUS_CREATED = 0 # Equivalent to MergeTrain.statuses[:created]
+ DOWNTIME = false
+
+ disable_ddl_transaction!
+
+ def up
+ add_column_with_default :merge_trains, :status, :integer, limit: 2, default: MERGE_TRAIN_STATUS_CREATED
+ end
+
+ def down
+ remove_column :merge_trains, :status
+ end
+end
diff --git a/db/migrate/20191112105448_add_index_on_personal_access_tokens_user_id_and_expires_at.rb b/db/migrate/20191112105448_add_index_on_personal_access_tokens_user_id_and_expires_at.rb
new file mode 100644
index 00000000000..1c1dc31ff23
--- /dev/null
+++ b/db/migrate/20191112105448_add_index_on_personal_access_tokens_user_id_and_expires_at.rb
@@ -0,0 +1,18 @@
+# frozen_string_literal: true
+
+class AddIndexOnPersonalAccessTokensUserIdAndExpiresAt < ActiveRecord::Migration[5.2]
+ include Gitlab::Database::MigrationHelpers
+
+ DOWNTIME = false
+ INDEX_NAME = 'index_pat_on_user_id_and_expires_at'
+
+ disable_ddl_transaction!
+
+ def up
+ add_concurrent_index :personal_access_tokens, [:user_id, :expires_at], name: INDEX_NAME, using: :btree
+ end
+
+ def down
+ remove_concurrent_index_by_name :personal_access_tokens, INDEX_NAME
+ end
+end
diff --git a/db/migrate/20191118155702_add_index_on_status_to_merge_trains.rb b/db/migrate/20191118155702_add_index_on_status_to_merge_trains.rb
new file mode 100644
index 00000000000..9b5238045f8
--- /dev/null
+++ b/db/migrate/20191118155702_add_index_on_status_to_merge_trains.rb
@@ -0,0 +1,20 @@
+# frozen_string_literal: true
+
+class AddIndexOnStatusToMergeTrains < ActiveRecord::Migration[5.2]
+ include Gitlab::Database::MigrationHelpers
+
+ DOWNTIME = false
+ INDEX_NAME = 'index_for_status_per_branch_per_project'
+
+ disable_ddl_transaction!
+
+ def up
+ add_concurrent_index :merge_trains, [:target_project_id, :target_branch, :status], name: INDEX_NAME
+ remove_concurrent_index :merge_trains, :target_project_id
+ end
+
+ def down
+ add_concurrent_index :merge_trains, :target_project_id
+ remove_concurrent_index :merge_trains, [:target_project_id, :target_branch, :status], name: INDEX_NAME
+ end
+end
diff --git a/db/schema.rb b/db/schema.rb
index 621ae7f380e..d33060f0e37 100644
--- a/db/schema.rb
+++ b/db/schema.rb
@@ -325,6 +325,7 @@ ActiveRecord::Schema.define(version: 2019_12_02_031812) do
t.string "encrypted_asset_proxy_secret_key_iv"
t.string "static_objects_external_storage_url", limit: 255
t.string "static_objects_external_storage_auth_token", limit: 255
+ t.integer "max_personal_access_token_lifetime"
t.boolean "throttle_protected_paths_enabled", default: false, null: false
t.integer "throttle_protected_paths_requests_per_period", default: 10, null: false
t.integer "throttle_protected_paths_period_in_seconds", default: 60, null: false
@@ -2524,9 +2525,10 @@ ActiveRecord::Schema.define(version: 2019_12_02_031812) do
t.datetime_with_timezone "updated_at", null: false
t.integer "target_project_id", null: false
t.text "target_branch", null: false
+ t.integer "status", limit: 2, default: 0, null: false
t.index ["merge_request_id"], name: "index_merge_trains_on_merge_request_id", unique: true
t.index ["pipeline_id"], name: "index_merge_trains_on_pipeline_id"
- t.index ["target_project_id"], name: "index_merge_trains_on_target_project_id"
+ t.index ["target_project_id", "target_branch", "status"], name: "index_for_status_per_branch_per_project"
t.index ["user_id"], name: "index_merge_trains_on_user_id"
end
@@ -2925,6 +2927,7 @@ ActiveRecord::Schema.define(version: 2019_12_02_031812) do
t.boolean "impersonation", default: false, null: false
t.string "token_digest"
t.index ["token_digest"], name: "index_personal_access_tokens_on_token_digest", unique: true
+ t.index ["user_id", "expires_at"], name: "index_pat_on_user_id_and_expires_at"
t.index ["user_id"], name: "index_personal_access_tokens_on_user_id"
end
diff --git a/doc/api/settings.md b/doc/api/settings.md
index ad9ffcbf872..185cce6353e 100644
--- a/doc/api/settings.md
+++ b/doc/api/settings.md
@@ -269,6 +269,7 @@ are listed in the descriptions of the relevant settings.
| `max_artifacts_size` | integer | no | Maximum artifacts size in MB |
| `max_attachment_size` | integer | no | Limit attachment size in MB |
| `max_pages_size` | integer | no | Maximum size of pages repositories in MB |
+| `max_personal_access_token_lifetime` | integer | no | **(ULTIMATE ONLY)** Maximum allowable lifetime for personal access tokens in days |
| `metrics_enabled` | boolean | no | (**If enabled, requires:** `metrics_host`, `metrics_method_call_threshold`, `metrics_packet_size`, `metrics_pool_size`, `metrics_port`, `metrics_sample_interval` and `metrics_timeout`) Enable influxDB metrics. |
| `metrics_host` | string | required by: `metrics_enabled` | InfluxDB host. |
| `metrics_method_call_threshold` | integer | required by: `metrics_enabled` | A method call is only tracked when it takes longer than the given amount of milliseconds. |
diff --git a/doc/ci/environments.md b/doc/ci/environments.md
index bd989157486..6666b8d6145 100644
--- a/doc/ci/environments.md
+++ b/doc/ci/environments.md
@@ -303,6 +303,41 @@ You are not required to use the same prefix or only slashes (`/`) in the dynamic
names. However, using this format will enable the [grouping similar environments](#grouping-similar-environments)
feature.
+### Configuring Kubernetes deployments
+
+> [Introduced](https://gitlab.com/gitlab-org/gitlab/issues/27630) in GitLab 12.6.
+
+If you are deploying to a [Kubernetes cluster](../user/project/clusters/index.md)
+associated with your project, you can configure these deployments from your
+`gitlab-ci.yml` file.
+
+The following configuration options are supported:
+
+- [`namespace`](https://kubernetes.io/docs/concepts/overview/working-with-objects/namespaces/)
+
+In the following example, the job will deploy your application to the
+`production` Kubernetes namespace.
+
+```yaml
+deploy:
+ stage: deploy
+ script:
+ - echo "Deploy to production server"
+ environment:
+ name: production
+ url: https://example.com
+ kubernetes:
+ namespace: production
+ only:
+ - master
+```
+
+NOTE: **Note:**
+Kubernetes configuration is not supported for Kubernetes clusters
+that are [managed by GitLab](../user/project/clusters/index.md#gitlab-managed-clusters).
+To follow progress on support for Gitlab-managed clusters, see the
+[relevant issue](https://gitlab.com/gitlab-org/gitlab/issues/38054).
+
### Complete example
The configuration in this section provides a full development workflow where your app is:
diff --git a/doc/ci/yaml/README.md b/doc/ci/yaml/README.md
index 714de3bac36..8562dc646f1 100644
--- a/doc/ci/yaml/README.md
+++ b/doc/ci/yaml/README.md
@@ -1421,6 +1421,38 @@ The `stop_review_app` job is **required** to have the following keywords defined
- `stage` should be the same as the `review_app` in order for the environment
to stop automatically when the branch is deleted
+#### `environment:kubernetes`
+
+> [Introduced](https://gitlab.com/gitlab-org/gitlab/issues/27630) in GitLab 12.6.
+
+The `kubernetes` block is used to configure deployments to a
+[Kubernetes cluster](../../user/project/clusters/index.md) that is associated with your project.
+
+For example:
+
+```yaml
+deploy:
+ stage: deploy
+ script: make deploy-app
+ environment:
+ name: production
+ kubernetes:
+ namespace: production
+```
+
+This will set up the `deploy` job to deploy to the `production`
+environment, using the `production`
+[Kubernetes namespace](https://kubernetes.io/docs/concepts/overview/working-with-objects/namespaces/).
+
+For more information, see
+[Available settings for `kubernetes`](../environments.md#configuring-kubernetes-deployments).
+
+NOTE: **Note:**
+Kubernetes configuration is not supported for Kubernetes clusters
+that are [managed by GitLab](../../user/project/clusters/index.md#gitlab-managed-clusters).
+To follow progress on support for Gitlab-managed clusters, see the
+[relevant issue](https://gitlab.com/gitlab-org/gitlab/issues/38054).
+
#### Dynamic environments
> - [Introduced][ce-6323] in GitLab 8.12 and GitLab Runner 1.6.
diff --git a/doc/user/admin_area/settings/account_and_limit_settings.md b/doc/user/admin_area/settings/account_and_limit_settings.md
index e443127a8a0..9d82b3b4292 100644
--- a/doc/user/admin_area/settings/account_and_limit_settings.md
+++ b/doc/user/admin_area/settings/account_and_limit_settings.md
@@ -84,3 +84,35 @@ add the line below to `/etc/gitlab/gitlab.rb` before increasing the max attachme
```
nginx['client_max_body_size'] = "200m"
```
+
+## Limiting lifetime of personal access tokens **(ULTIMATE ONLY)**
+
+> [Introduced](https://gitlab.com/gitlab-org/gitlab/issues/3649) in [GitLab Ultimate](https://about.gitlab.com/pricing/) 12.6.
+
+Users can optionally specify an expiration date for
+[personal access tokens](../../profile/personal_access_tokens.md).
+This expiration date is not a requirement, and can be set to any arbitrary date.
+
+Since personal access tokens are the only token needed for programmatic access to GitLab,
+organizations with security requirements may want to enforce more protection to require
+regular rotation of these tokens.
+
+### Setting a limit
+
+Only a GitLab administrator can set a limit. Leaving it empty means
+there are no restrictions.
+
+To set a limit on how long personal access tokens are valid:
+
+1. Navigate to **Admin Area > Settings > General**.
+1. Expand the **Account and limit** section.
+1. Fill in the **Maximun allowable lifetime for personal access tokens (days)** field.
+1. Click **Save changes**.
+
+Once a lifetime for personal access tokens is set, GitLab will:
+
+- Apply the lifetime for new personal access tokens, and require users to set an expiration date
+ and a date no later than the allowed lifetime.
+- After three hours, revoke old tokens with no expiration date or with a lifetime longer than the
+ allowed lifetime. Three hours is given to allow administrators to change the allowed lifetime,
+ or remove it, before revocation takes place.
diff --git a/lib/gitlab/diff/file_collection/merge_request_diff.rb b/lib/gitlab/diff/file_collection/merge_request_diff.rb
index 1d99349304b..fe7df1062c0 100644
--- a/lib/gitlab/diff/file_collection/merge_request_diff.rb
+++ b/lib/gitlab/diff/file_collection/merge_request_diff.rb
@@ -4,45 +4,6 @@ module Gitlab
module Diff
module FileCollection
class MergeRequestDiff < MergeRequestDiffBase
- include Gitlab::Utils::StrongMemoize
-
- def diff_files
- strong_memoize(:diff_files) do
- diff_files = super
-
- diff_files.each { |diff_file| cache.decorate(diff_file) }
-
- diff_files
- end
- end
-
- override :write_cache
- def write_cache
- cache.write_if_empty
- end
-
- override :clear_cache
- def clear_cache
- cache.clear
- end
-
- def cache_key
- cache.key
- end
-
- def real_size
- @merge_request_diff.real_size
- end
-
- private
-
- def cache
- @cache ||= if Feature.enabled?(:hset_redis_diff_caching, project)
- Gitlab::Diff::HighlightCache.new(self)
- else
- Gitlab::Diff::DeprecatedHighlightCache.new(self)
- end
- end
end
end
end
diff --git a/lib/gitlab/diff/file_collection/merge_request_diff_base.rb b/lib/gitlab/diff/file_collection/merge_request_diff_base.rb
index a747a6ed475..06cf3d4d168 100644
--- a/lib/gitlab/diff/file_collection/merge_request_diff_base.rb
+++ b/lib/gitlab/diff/file_collection/merge_request_diff_base.rb
@@ -15,6 +15,44 @@ module Gitlab
diff_refs: merge_request_diff.diff_refs,
fallback_diff_refs: merge_request_diff.fallback_diff_refs)
end
+
+ def diff_files
+ strong_memoize(:diff_files) do
+ diff_files = super
+
+ diff_files.each { |diff_file| cache.decorate(diff_file) }
+
+ diff_files
+ end
+ end
+
+ override :write_cache
+ def write_cache
+ cache.write_if_empty
+ end
+
+ override :clear_cache
+ def clear_cache
+ cache.clear
+ end
+
+ def cache_key
+ cache.key
+ end
+
+ def real_size
+ @merge_request_diff.real_size
+ end
+
+ private
+
+ def cache
+ @cache ||= if Feature.enabled?(:hset_redis_diff_caching, project)
+ Gitlab::Diff::HighlightCache.new(self)
+ else
+ Gitlab::Diff::DeprecatedHighlightCache.new(self)
+ end
+ end
end
end
end
diff --git a/lib/gitlab/gon_helper.rb b/lib/gitlab/gon_helper.rb
index 487dcd58d01..2616a19fdaa 100644
--- a/lib/gitlab/gon_helper.rb
+++ b/lib/gitlab/gon_helper.rb
@@ -42,7 +42,6 @@ module Gitlab
# Initialize gon.features with any flags that should be
# made globally available to the frontend
push_frontend_feature_flag(:suppress_ajax_navigation_errors, default_enabled: true)
- push_frontend_feature_flag(:snippets_vue, default_enabled: false)
end
# Exposes the state of a feature flag to the frontend code.
diff --git a/locale/gitlab.pot b/locale/gitlab.pot
index 5a404b8976f..6dd524981a4 100644
--- a/locale/gitlab.pot
+++ b/locale/gitlab.pot
@@ -9128,6 +9128,9 @@ msgstr ""
msgid "Helps reduce request volume for protected paths"
msgstr ""
+msgid "Hi %{username}!"
+msgstr ""
+
msgid "Hide archived projects"
msgstr ""
@@ -10274,6 +10277,9 @@ msgstr ""
msgid "Leave Admin Mode"
msgstr ""
+msgid "Leave blank for no limit. Once set, existing personal access tokens may be revoked"
+msgstr ""
+
msgid "Leave edit mode? All unsaved changes will be lost."
msgstr ""
@@ -10764,6 +10770,9 @@ msgstr ""
msgid "Max seats used"
msgstr ""
+msgid "Maximum allowable lifetime for personal access token (days)"
+msgstr ""
+
msgid "Maximum artifacts size (MB)"
msgstr ""
@@ -10782,6 +10791,9 @@ msgstr ""
msgid "Maximum job timeout has a value which could not be accepted"
msgstr ""
+msgid "Maximum lifetime allowable for Personal Access Tokens is active, your expire date must be set before %{maximum_allowable_date}."
+msgstr ""
+
msgid "Maximum number of comments exceeded"
msgstr ""
@@ -11985,6 +11997,9 @@ msgstr[1] ""
msgid "One or more groups that you don't have access to."
msgstr ""
+msgid "One or more of you personal access tokens were revoked"
+msgstr ""
+
msgid "One or more of your Bitbucket projects cannot be imported into GitLab directly because they use Subversion or Mercurial for version control, rather than Git."
msgstr ""
@@ -17439,6 +17454,11 @@ msgstr ""
msgid "The following items will be exported:"
msgstr ""
+msgid "The following personal access token: %{token_names} was revoked, because a new policy to expire personal access tokens were set."
+msgid_plural "The following personal access tokens: %{token_names} were revoked, because a new policy to expire personal access tokens were set."
+msgstr[0] ""
+msgstr[1] ""
+
msgid "The fork relationship has been removed."
msgstr ""
@@ -20114,6 +20134,12 @@ msgstr ""
msgid "You can create files directly in GitLab using one of the following options."
msgstr ""
+msgid "You can create new ones at your %{pat_link_start}Personal Access Tokens%{pat_link_end} settings"
+msgstr ""
+
+msgid "You can create new ones at your Personal Access Tokens settings %{pat_link}"
+msgstr ""
+
msgid "You can easily contribute to them by requesting to join these groups."
msgstr ""
diff --git a/spec/controllers/projects/merge_requests/diffs_controller_spec.rb b/spec/controllers/projects/merge_requests/diffs_controller_spec.rb
index 06d9af33189..8f4f85b55bb 100644
--- a/spec/controllers/projects/merge_requests/diffs_controller_spec.rb
+++ b/spec/controllers/projects/merge_requests/diffs_controller_spec.rb
@@ -25,6 +25,16 @@ describe Projects::MergeRequests::DiffsController do
end
end
+ shared_examples 'cached diff collection' do
+ it 'ensures diff highlighting cache writing' do
+ expect_next_instance_of(Gitlab::Diff::HighlightCache) do |cache|
+ expect(cache).to receive(:write_if_empty).once
+ end
+
+ go
+ end
+ end
+
shared_examples 'persisted preferred diff view cookie' do
context 'with view param' do
before do
@@ -112,6 +122,7 @@ describe Projects::MergeRequests::DiffsController do
end
it_behaves_like 'persisted preferred diff view cookie'
+ it_behaves_like 'cached diff collection'
end
describe 'GET diffs_metadata' do
@@ -404,6 +415,7 @@ describe Projects::MergeRequests::DiffsController do
it_behaves_like 'forked project with submodules'
it_behaves_like 'persisted preferred diff view cookie'
+ it_behaves_like 'cached diff collection'
context 'diff unfolding' do
let!(:unfoldable_diff_note) do
diff --git a/spec/features/snippets/internal_snippet_spec.rb b/spec/features/snippets/internal_snippet_spec.rb
index fd7ef71db15..4ef3b0e5e7a 100644
--- a/spec/features/snippets/internal_snippet_spec.rb
+++ b/spec/features/snippets/internal_snippet_spec.rb
@@ -5,10 +5,6 @@ require 'spec_helper'
describe 'Internal Snippets', :js do
let(:internal_snippet) { create(:personal_snippet, :internal) }
- before do
- stub_feature_flags(snippets_vue: false)
- end
-
describe 'normal user' do
before do
sign_in(create(:user))
diff --git a/spec/features/snippets/notes_on_personal_snippets_spec.rb b/spec/features/snippets/notes_on_personal_snippets_spec.rb
index 57264f97ddc..2bd01be25e9 100644
--- a/spec/features/snippets/notes_on_personal_snippets_spec.rb
+++ b/spec/features/snippets/notes_on_personal_snippets_spec.rb
@@ -16,7 +16,6 @@ describe 'Comments on personal snippets', :js do
let!(:other_note) { create(:note_on_personal_snippet) }
before do
- stub_feature_flags(snippets_vue: false)
sign_in user
visit snippet_path(snippet)
diff --git a/spec/features/snippets/private_snippets_spec.rb b/spec/features/snippets/private_snippets_spec.rb
index 37f45f22a27..9df4cd01103 100644
--- a/spec/features/snippets/private_snippets_spec.rb
+++ b/spec/features/snippets/private_snippets_spec.rb
@@ -6,7 +6,6 @@ describe 'Private Snippets', :js do
let(:user) { create(:user) }
before do
- stub_feature_flags(snippets_vue: false)
sign_in(user)
end
diff --git a/spec/features/snippets/public_snippets_spec.rb b/spec/features/snippets/public_snippets_spec.rb
index 045afcf1c12..82edda509c2 100644
--- a/spec/features/snippets/public_snippets_spec.rb
+++ b/spec/features/snippets/public_snippets_spec.rb
@@ -3,10 +3,6 @@
require 'spec_helper'
describe 'Public Snippets', :js do
- before do
- stub_feature_flags(snippets_vue: false)
- end
-
it 'Unauthenticated user should see public snippets' do
public_snippet = create(:personal_snippet, :public)
diff --git a/spec/features/snippets/show_spec.rb b/spec/features/snippets/show_spec.rb
index 9c686be012b..450e520e293 100644
--- a/spec/features/snippets/show_spec.rb
+++ b/spec/features/snippets/show_spec.rb
@@ -6,10 +6,6 @@ describe 'Snippet', :js do
let(:project) { create(:project, :repository) }
let(:snippet) { create(:personal_snippet, :public, file_name: file_name, content: content) }
- before do
- stub_feature_flags(snippets_vue: false)
- end
-
context 'Ruby file' do
let(:file_name) { 'popen.rb' }
let(:content) { project.repository.blob_at('master', 'files/ruby/popen.rb').data }
diff --git a/spec/features/snippets/spam_snippets_spec.rb b/spec/features/snippets/spam_snippets_spec.rb
index 0c3ca6f17c8..3e71a4e7879 100644
--- a/spec/features/snippets/spam_snippets_spec.rb
+++ b/spec/features/snippets/spam_snippets_spec.rb
@@ -7,7 +7,6 @@ describe 'User creates snippet', :js do
before do
stub_feature_flags(allow_possible_spam: false)
- stub_feature_flags(snippets_vue: false)
stub_env('IN_MEMORY_APPLICATION_SETTINGS', 'false')
Gitlab::CurrentSettings.update!(
diff --git a/spec/features/snippets/user_creates_snippet_spec.rb b/spec/features/snippets/user_creates_snippet_spec.rb
index b373264bbe4..9a141dd463a 100644
--- a/spec/features/snippets/user_creates_snippet_spec.rb
+++ b/spec/features/snippets/user_creates_snippet_spec.rb
@@ -8,7 +8,6 @@ describe 'User creates snippet', :js do
let(:user) { create(:user) }
before do
- stub_feature_flags(snippets_vue: false)
sign_in(user)
visit new_snippet_path
end
diff --git a/spec/features/snippets/user_deletes_snippet_spec.rb b/spec/features/snippets/user_deletes_snippet_spec.rb
index 35619b92561..217419a220a 100644
--- a/spec/features/snippets/user_deletes_snippet_spec.rb
+++ b/spec/features/snippets/user_deletes_snippet_spec.rb
@@ -10,8 +10,6 @@ describe 'User deletes snippet' do
before do
sign_in(user)
- stub_feature_flags(snippets_vue: false)
-
visit snippet_path(snippet)
end
diff --git a/spec/features/snippets/user_edits_snippet_spec.rb b/spec/features/snippets/user_edits_snippet_spec.rb
index 1d26660a4f6..51d9baf44bc 100644
--- a/spec/features/snippets/user_edits_snippet_spec.rb
+++ b/spec/features/snippets/user_edits_snippet_spec.rb
@@ -12,7 +12,6 @@ describe 'User edits snippet', :js do
let(:snippet) { create(:personal_snippet, :public, file_name: file_name, content: content, author: user) }
before do
- stub_feature_flags(snippets_vue: false)
sign_in(user)
visit edit_snippet_path(snippet)
diff --git a/spec/features/snippets_spec.rb b/spec/features/snippets_spec.rb
index bc7fa161e87..9df6fe7d16b 100644
--- a/spec/features/snippets_spec.rb
+++ b/spec/features/snippets_spec.rb
@@ -6,38 +6,11 @@ describe 'Snippets' do
context 'when the project has snippets' do
let(:project) { create(:project, :public) }
let!(:snippets) { create_list(:project_snippet, 2, :public, author: project.owner, project: project) }
-
before do
allow(Snippet).to receive(:default_per_page).and_return(1)
-
- visit project_snippets_path(project)
+ visit snippets_path(username: project.owner.username)
end
it_behaves_like 'paginated snippets'
end
-
- describe 'rendering engine' do
- let_it_be(:snippet) { create(:personal_snippet, :public) }
- let(:snippets_vue_feature_flag_enabled) { true }
-
- before do
- stub_feature_flags(snippets_vue: snippets_vue_feature_flag_enabled)
-
- visit snippet_path(snippet)
- end
-
- it 'renders Vue application' do
- expect(page).to have_selector('#js-snippet-view')
- expect(page).not_to have_selector('.personal-snippets')
- end
-
- context 'when feature flag is disabled' do
- let(:snippets_vue_feature_flag_enabled) { false }
-
- it 'renders HAML application and not Vue' do
- expect(page).not_to have_selector('#js-snippet-view')
- expect(page).to have_selector('.personal-snippets')
- end
- end
- end
end
diff --git a/spec/frontend/monitoring/embed/embed_spec.js b/spec/frontend/monitoring/embed/embed_spec.js
index c5219f6130e..6d40719fa32 100644
--- a/spec/frontend/monitoring/embed/embed_spec.js
+++ b/spec/frontend/monitoring/embed/embed_spec.js
@@ -1,7 +1,7 @@
import { createLocalVue, shallowMount } from '@vue/test-utils';
import Vuex from 'vuex';
import Embed from '~/monitoring/components/embed.vue';
-import MonitorTimeSeriesChart from '~/monitoring/components/charts/time_series.vue';
+import PanelType from 'ee_else_ce/monitoring/components/panel_type.vue';
import { TEST_HOST } from 'helpers/test_constants';
import { groups, initialState, metricsData, metricsWithData } from './mock_data';
@@ -55,7 +55,7 @@ describe('Embed', () => {
it('shows an empty state when no metrics are present', () => {
expect(wrapper.find('.metrics-embed').exists()).toBe(true);
- expect(wrapper.find(MonitorTimeSeriesChart).exists()).toBe(false);
+ expect(wrapper.find(PanelType).exists()).toBe(false);
});
});
@@ -71,12 +71,12 @@ describe('Embed', () => {
it('shows a chart when metrics are present', () => {
wrapper.setProps({});
expect(wrapper.find('.metrics-embed').exists()).toBe(true);
- expect(wrapper.find(MonitorTimeSeriesChart).exists()).toBe(true);
- expect(wrapper.findAll(MonitorTimeSeriesChart).length).toBe(2);
+ expect(wrapper.find(PanelType).exists()).toBe(true);
+ expect(wrapper.findAll(PanelType).length).toBe(2);
});
it('includes groupId with dashboardUrl', () => {
- expect(wrapper.find(MonitorTimeSeriesChart).props('groupId')).toBe(TEST_HOST);
+ expect(wrapper.find(PanelType).props('groupId')).toBe(TEST_HOST);
});
});
});
diff --git a/spec/frontend/monitoring/panel_type_spec.js b/spec/frontend/monitoring/panel_type_spec.js
index b30ad747a12..7adecc56d18 100644
--- a/spec/frontend/monitoring/panel_type_spec.js
+++ b/spec/frontend/monitoring/panel_type_spec.js
@@ -102,6 +102,10 @@ describe('Panel Type component', () => {
expect(clipboardText()).toBe(exampleText);
});
+
+ it('includes a default group id', () => {
+ expect(panelType.vm.groupId).toBe('panel-type-chart');
+ });
});
describe('Anomaly Chart panel type', () => {
diff --git a/spec/lib/gitlab/diff/file_collection/merge_request_diff_batch_spec.rb b/spec/lib/gitlab/diff/file_collection/merge_request_diff_batch_spec.rb
index 265c6260ca9..7e945d1d140 100644
--- a/spec/lib/gitlab/diff/file_collection/merge_request_diff_batch_spec.rb
+++ b/spec/lib/gitlab/diff/file_collection/merge_request_diff_batch_spec.rb
@@ -123,4 +123,8 @@ describe Gitlab::Diff::FileCollection::MergeRequestDiffBatch do
collection_default_args)
end
end
+
+ it_behaves_like 'cacheable diff collection' do
+ let(:cacheable_files_count) { batch_size }
+ end
end
diff --git a/spec/lib/gitlab/diff/file_collection/merge_request_diff_spec.rb b/spec/lib/gitlab/diff/file_collection/merge_request_diff_spec.rb
index 7f207d5d2ee..d8f1fd26aeb 100644
--- a/spec/lib/gitlab/diff/file_collection/merge_request_diff_spec.rb
+++ b/spec/lib/gitlab/diff/file_collection/merge_request_diff_spec.rb
@@ -4,7 +4,8 @@ require 'spec_helper'
describe Gitlab::Diff::FileCollection::MergeRequestDiff do
let(:merge_request) { create(:merge_request) }
- let(:subject) { described_class.new(merge_request.merge_request_diff, diff_options: nil) }
+ let(:diffable) { merge_request.merge_request_diff }
+ let(:subject) { described_class.new(diffable, diff_options: nil) }
let(:diff_files) { subject.diff_files }
describe '#diff_files' do
@@ -52,6 +53,10 @@ describe Gitlab::Diff::FileCollection::MergeRequestDiff do
let(:stub_path) { '.gitignore' }
end
+ it_behaves_like 'cacheable diff collection' do
+ let(:cacheable_files_count) { diffable.size.to_i }
+ end
+
it 'returns a valid instance of a DiffCollection' do
expect(diff_files).to be_a(Gitlab::Git::DiffCollection)
end
diff --git a/spec/presenters/ci/legacy_stage_presenter_spec.rb b/spec/presenters/ci/legacy_stage_presenter_spec.rb
new file mode 100644
index 00000000000..932cfacafb1
--- /dev/null
+++ b/spec/presenters/ci/legacy_stage_presenter_spec.rb
@@ -0,0 +1,31 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+describe Ci::LegacyStagePresenter do
+ let(:legacy_stage) { create(:ci_stage) }
+ let(:presenter) { described_class.new(legacy_stage) }
+
+ let!(:build) { create(:ci_build, :tags, pipeline: legacy_stage.pipeline, stage: legacy_stage.name) }
+ let!(:retried_build) { create(:ci_build, :tags, :retried, pipeline: legacy_stage.pipeline, stage: legacy_stage.name) }
+
+ before do
+ create(:generic_commit_status, pipeline: legacy_stage.pipeline, stage: legacy_stage.name)
+ end
+
+ describe '#latest_ordered_statuses' do
+ subject(:latest_ordered_statuses) { presenter.latest_ordered_statuses }
+
+ it 'preloads build tags' do
+ expect(latest_ordered_statuses.second.association(:tags)).to be_loaded
+ end
+ end
+
+ describe '#retried_ordered_statuses' do
+ subject(:retried_ordered_statuses) { presenter.retried_ordered_statuses }
+
+ it 'preloads build tags' do
+ expect(retried_ordered_statuses.first.association(:tags)).to be_loaded
+ end
+ end
+end
diff --git a/spec/support/shared_examples/diff_file_collections.rb b/spec/support/shared_examples/diff_file_collections.rb
index 4c64abd2a97..c8bd137bf84 100644
--- a/spec/support/shared_examples/diff_file_collections.rb
+++ b/spec/support/shared_examples/diff_file_collections.rb
@@ -57,3 +57,45 @@ shared_examples 'unfoldable diff' do
subject.unfold_diff_files([position])
end
end
+
+shared_examples 'cacheable diff collection' do
+ let(:cache) { instance_double(Gitlab::Diff::HighlightCache) }
+
+ before do
+ expect(Gitlab::Diff::HighlightCache).to receive(:new).with(subject) { cache }
+ end
+
+ describe '#write_cache' do
+ it 'calls Gitlab::Diff::HighlightCache#write_if_empty' do
+ expect(cache).to receive(:write_if_empty).once
+
+ subject.write_cache
+ end
+ end
+
+ describe '#clear_cache' do
+ it 'calls Gitlab::Diff::HighlightCache#clear' do
+ expect(cache).to receive(:clear).once
+
+ subject.clear_cache
+ end
+ end
+
+ describe '#cache_key' do
+ it 'calls Gitlab::Diff::HighlightCache#key' do
+ expect(cache).to receive(:key).once
+
+ subject.cache_key
+ end
+ end
+
+ describe '#diff_files' do
+ it 'calls Gitlab::Diff::HighlightCache#decorate' do
+ expect(cache).to receive(:decorate)
+ .with(instance_of(Gitlab::Diff::File))
+ .exactly(cacheable_files_count).times
+
+ subject.diff_files
+ end
+ end
+end