From 53ae6b7e3f83591ad251a3f771f5bf3b8cf087ba Mon Sep 17 00:00:00 2001 From: GitLab Bot Date: Mon, 17 Feb 2020 09:08:52 +0000 Subject: Add latest changes from gitlab-org/gitlab@master --- .../behaviors/markdown/render_mermaid.js | 19 ++++- .../javascripts/behaviors/shortcuts/shortcuts.js | 23 +++++- .../behaviors/shortcuts/shortcuts_toggle.js | 22 ++++++ .../behaviors/shortcuts/shortcuts_toggle.vue | 60 ++++++++++++++++ app/assets/javascripts/blob/pdf/index.js | 8 +-- .../filtered_search/filtered_search_dropdown.js | 2 +- app/assets/javascripts/notes/constants.js | 1 + .../notes/mixins/description_version_history.js | 2 + app/assets/javascripts/notes/stores/actions.js | 53 ++++++++++++-- .../javascripts/notes/stores/collapse_utils.js | 8 ++- .../javascripts/notes/stores/modules/index.js | 2 + .../javascripts/notes/stores/mutation_types.js | 8 +++ app/assets/javascripts/notes/stores/mutations.js | 21 ++++++ .../pipelines/pipeline_details_bundle.js | 2 +- .../vue_shared/components/notes/system_note.vue | 24 +++++-- app/assets/stylesheets/pages/notes.scss | 13 ++-- app/models/commit_status_enums.rb | 1 + app/models/deployment.rb | 11 +++ app/models/environment.rb | 1 + app/models/member.rb | 1 + app/models/project.rb | 1 + app/models/project_ci_cd_setting.rb | 6 ++ app/presenters/commit_status_presenter.rb | 1 + .../deployments/older_deployments_drop_service.rb | 31 ++++++++ app/views/help/_shortcuts.html.haml | 1 + app/views/layouts/header/_help_dropdown.html.haml | 4 ++ app/views/projects/pipelines/show.html.haml | 3 +- app/workers/all_queues.yml | 6 ++ .../deployments/forward_deployment_worker.rb | 14 ++++ ...oard-shortcuts-or-allow-them-to-be-disabled.yml | 5 ++ .../25276-allow-forward-deploys-only.yml | 5 ++ changelogs/unreleased/psi-mermaid-details.yml | 5 ++ ...t_deployment_order_to_project_ci_cd_settings.rb | 9 +++ .../20200210062432_schedule_link_lfs_objects.rb | 23 +----- db/schema.rb | 1 + doc/user/shortcuts.md | 5 +- .../background_migration/link_lfs_objects.rb | 19 +---- lib/gitlab/ci/status/build/failed.rb | 1 + lib/quality/kubernetes_client.rb | 2 +- locale/gitlab.pot | 18 +++++ spec/factories/projects.rb | 1 + spec/features/markdown/mermaid_spec.rb | 30 ++++++++ spec/features/projects/user_uses_shortcuts_spec.rb | 53 ++++++++++++++ .../background_migration/link_lfs_objects_spec.rb | 66 ----------------- spec/lib/quality/kubernetes_client_spec.rb | 2 +- spec/migrations/schedule_link_lfs_objects_spec.rb | 82 ---------------------- spec/models/deployment_spec.rb | 39 ++++++++++ spec/models/project_ci_cd_setting_spec.rb | 6 ++ .../older_deployments_drop_service_spec.rb | 78 ++++++++++++++++++++ 49 files changed, 583 insertions(+), 216 deletions(-) create mode 100644 app/assets/javascripts/behaviors/shortcuts/shortcuts_toggle.js create mode 100644 app/assets/javascripts/behaviors/shortcuts/shortcuts_toggle.vue create mode 100644 app/services/deployments/older_deployments_drop_service.rb create mode 100644 app/workers/deployments/forward_deployment_worker.rb create mode 100644 changelogs/unreleased/22113-improve-keyboard-shortcuts-or-allow-them-to-be-disabled.yml create mode 100644 changelogs/unreleased/25276-allow-forward-deploys-only.yml create mode 100644 changelogs/unreleased/psi-mermaid-details.yml create mode 100644 db/migrate/20200124143014_add_restrict_deployment_order_to_project_ci_cd_settings.rb delete mode 100644 spec/lib/gitlab/background_migration/link_lfs_objects_spec.rb delete mode 100644 spec/migrations/schedule_link_lfs_objects_spec.rb create mode 100644 spec/services/deployments/older_deployments_drop_service_spec.rb diff --git a/app/assets/javascripts/behaviors/markdown/render_mermaid.js b/app/assets/javascripts/behaviors/markdown/render_mermaid.js index c86a4c9f178..3856832de90 100644 --- a/app/assets/javascripts/behaviors/markdown/render_mermaid.js +++ b/app/assets/javascripts/behaviors/markdown/render_mermaid.js @@ -1,4 +1,5 @@ import flash from '~/flash'; +import $ from 'jquery'; import { sprintf, __ } from '../../locale'; // Renders diagrams and flowcharts from text using Mermaid in any element with the @@ -18,7 +19,7 @@ import { sprintf, __ } from '../../locale'; // This is an arbitrary number; Can be iterated upon when suitable. const MAX_CHAR_LIMIT = 5000; -export default function renderMermaid($els) { +function renderMermaids($els) { if (!$els.length) return; // A diagram may have been truncated in search results which will cause errors, so abort the render. @@ -95,3 +96,19 @@ export default function renderMermaid($els) { flash(`Can't load mermaid module: ${err}`); }); } + +export default function renderMermaid($els) { + if (!$els.length) return; + + const visibleMermaids = $els.filter(function filter() { + return $(this).closest('details').length === 0; + }); + + renderMermaids(visibleMermaids); + + $els.closest('details').one('toggle', function toggle() { + if (this.open) { + renderMermaids($(this).find('.js-render-mermaid')); + } + }); +} diff --git a/app/assets/javascripts/behaviors/shortcuts/shortcuts.js b/app/assets/javascripts/behaviors/shortcuts/shortcuts.js index 66cb9fd7672..85636f3e5d2 100644 --- a/app/assets/javascripts/behaviors/shortcuts/shortcuts.js +++ b/app/assets/javascripts/behaviors/shortcuts/shortcuts.js @@ -1,6 +1,9 @@ import $ from 'jquery'; import Cookies from 'js-cookie'; import Mousetrap from 'mousetrap'; +import Vue from 'vue'; +import { disableShortcuts, shouldDisableShortcuts } from './shortcuts_toggle'; +import ShortcutsToggle from './shortcuts_toggle.vue'; import axios from '../../lib/utils/axios_utils'; import { refreshCurrentPage, visitUrl } from '../../lib/utils/url_utility'; import findAndFollowLink from '../../lib/utils/navigation_utility'; @@ -15,6 +18,15 @@ Mousetrap.stopCallback = (e, element, combo) => { return defaultStopCallback(e, element, combo); }; +function initToggleButton() { + return new Vue({ + el: document.querySelector('.js-toggle-shortcuts'), + render(createElement) { + return createElement(ShortcutsToggle); + }, + }); +} + export default class Shortcuts { constructor() { this.onToggleHelp = this.onToggleHelp.bind(this); @@ -48,6 +60,14 @@ export default class Shortcuts { $(this).remove(); e.preventDefault(); }); + + $('.js-shortcuts-modal-trigger') + .off('click') + .on('click', this.onToggleHelp); + + if (shouldDisableShortcuts()) { + disableShortcuts(); + } } onToggleHelp(e) { @@ -104,7 +124,8 @@ export default class Shortcuts { } return $('.js-more-help-button').remove(); - }); + }) + .then(initToggleButton); } focusFilter(e) { diff --git a/app/assets/javascripts/behaviors/shortcuts/shortcuts_toggle.js b/app/assets/javascripts/behaviors/shortcuts/shortcuts_toggle.js new file mode 100644 index 00000000000..66aa1b752ae --- /dev/null +++ b/app/assets/javascripts/behaviors/shortcuts/shortcuts_toggle.js @@ -0,0 +1,22 @@ +import Mousetrap from 'mousetrap'; +import 'mousetrap/plugins/pause/mousetrap-pause'; + +const shorcutsDisabledKey = 'shortcutsDisabled'; + +export const shouldDisableShortcuts = () => { + try { + return localStorage.getItem(shorcutsDisabledKey) === 'true'; + } catch (e) { + return false; + } +}; + +export function enableShortcuts() { + localStorage.setItem(shorcutsDisabledKey, false); + Mousetrap.unpause(); +} + +export function disableShortcuts() { + localStorage.setItem(shorcutsDisabledKey, true); + Mousetrap.pause(); +} diff --git a/app/assets/javascripts/behaviors/shortcuts/shortcuts_toggle.vue b/app/assets/javascripts/behaviors/shortcuts/shortcuts_toggle.vue new file mode 100644 index 00000000000..a53b1b06be9 --- /dev/null +++ b/app/assets/javascripts/behaviors/shortcuts/shortcuts_toggle.vue @@ -0,0 +1,60 @@ + + + diff --git a/app/assets/javascripts/blob/pdf/index.js b/app/assets/javascripts/blob/pdf/index.js index e9a0b3979e6..19778d07983 100644 --- a/app/assets/javascripts/blob/pdf/index.js +++ b/app/assets/javascripts/blob/pdf/index.js @@ -1,5 +1,6 @@ import Vue from 'vue'; import pdfLab from '../../pdf/index.vue'; +import { GlLoadingIcon } from '@gitlab/ui'; export default () => { const el = document.getElementById('js-pdf-viewer'); @@ -8,6 +9,7 @@ export default () => { el, components: { pdfLab, + GlLoadingIcon, }, data() { return { @@ -32,11 +34,7 @@ export default () => {
- +
- + `; this.bindEvents(); } diff --git a/app/assets/javascripts/notes/constants.js b/app/assets/javascripts/notes/constants.js index 68c117183a1..e9a81bc9553 100644 --- a/app/assets/javascripts/notes/constants.js +++ b/app/assets/javascripts/notes/constants.js @@ -18,6 +18,7 @@ export const HISTORY_ONLY_FILTER_VALUE = 2; export const DISCUSSION_FILTERS_DEFAULT_VALUE = 0; export const DISCUSSION_TAB_LABEL = 'show'; export const NOTE_UNDERSCORE = 'note_'; +export const TIME_DIFFERENCE_VALUE = 10; export const NOTEABLE_TYPE_MAPPING = { Issue: ISSUE_NOTEABLE_TYPE, diff --git a/app/assets/javascripts/notes/mixins/description_version_history.js b/app/assets/javascripts/notes/mixins/description_version_history.js index 12d80f3faa2..66e6685cfd8 100644 --- a/app/assets/javascripts/notes/mixins/description_version_history.js +++ b/app/assets/javascripts/notes/mixins/description_version_history.js @@ -3,10 +3,12 @@ export default { computed: { canSeeDescriptionVersion() {}, + canDeleteDescriptionVersion() {}, shouldShowDescriptionVersion() {}, descriptionVersionToggleIcon() {}, }, methods: { toggleDescriptionVersion() {}, + deleteDescriptionVersion() {}, }, }; diff --git a/app/assets/javascripts/notes/stores/actions.js b/app/assets/javascripts/notes/stores/actions.js index f3dc6187c3f..594e3a14d56 100644 --- a/app/assets/javascripts/notes/stores/actions.js +++ b/app/assets/javascripts/notes/stores/actions.js @@ -491,23 +491,66 @@ export const convertToDiscussion = ({ commit }, noteId) => export const removeConvertedDiscussion = ({ commit }, noteId) => commit(types.REMOVE_CONVERTED_DISCUSSION, noteId); -export const fetchDescriptionVersion = (_, { endpoint, startingVersion }) => { +export const setCurrentDiscussionId = ({ commit }, discussionId) => + commit(types.SET_CURRENT_DISCUSSION_ID, discussionId); + +export const fetchDescriptionVersion = ({ dispatch }, { endpoint, startingVersion }) => { let requestUrl = endpoint; if (startingVersion) { requestUrl = mergeUrlParams({ start_version_id: startingVersion }, requestUrl); } + dispatch('requestDescriptionVersion'); return axios .get(requestUrl) - .then(res => res.data) - .catch(() => { + .then(res => { + dispatch('receiveDescriptionVersion', res.data); + }) + .catch(error => { + dispatch('receiveDescriptionVersionError', error); Flash(__('Something went wrong while fetching description changes. Please try again.')); }); }; -export const setCurrentDiscussionId = ({ commit }, discussionId) => - commit(types.SET_CURRENT_DISCUSSION_ID, discussionId); +export const requestDescriptionVersion = ({ commit }) => { + commit(types.REQUEST_DESCRIPTION_VERSION); +}; +export const receiveDescriptionVersion = ({ commit }, descriptionVersion) => { + commit(types.RECEIVE_DESCRIPTION_VERSION, descriptionVersion); +}; +export const receiveDescriptionVersionError = ({ commit }, error) => { + commit(types.RECEIVE_DESCRIPTION_VERSION_ERROR, error); +}; + +export const softDeleteDescriptionVersion = ({ dispatch }, { endpoint, startingVersion }) => { + let requestUrl = endpoint; + + if (startingVersion) { + requestUrl = mergeUrlParams({ start_version_id: startingVersion }, requestUrl); + } + dispatch('requestDeleteDescriptionVersion'); + + return axios + .delete(requestUrl) + .then(() => { + dispatch('receiveDeleteDescriptionVersion'); + }) + .catch(error => { + dispatch('receiveDeleteDescriptionVersionError', error); + Flash(__('Something went wrong while deleting description changes. Please try again.')); + }); +}; + +export const requestDeleteDescriptionVersion = ({ commit }) => { + commit(types.REQUEST_DELETE_DESCRIPTION_VERSION); +}; +export const receiveDeleteDescriptionVersion = ({ commit }) => { + commit(types.RECEIVE_DELETE_DESCRIPTION_VERSION, __('Deleted')); +}; +export const receiveDeleteDescriptionVersionError = ({ commit }, error) => { + commit(types.RECEIVE_DELETE_DESCRIPTION_VERSION_ERROR, error); +}; // prevent babel-plugin-rewire from generating an invalid default during karma tests export default () => {}; diff --git a/app/assets/javascripts/notes/stores/collapse_utils.js b/app/assets/javascripts/notes/stores/collapse_utils.js index 3cdcc7a05b8..d94fc626a3f 100644 --- a/app/assets/javascripts/notes/stores/collapse_utils.js +++ b/app/assets/javascripts/notes/stores/collapse_utils.js @@ -1,4 +1,4 @@ -import { DESCRIPTION_TYPE } from '../constants'; +import { DESCRIPTION_TYPE, TIME_DIFFERENCE_VALUE } from '../constants'; /** * Checks the time difference between two notes from their 'created_at' dates @@ -45,7 +45,11 @@ export const collapseSystemNotes = notes => { const timeDifferenceMinutes = getTimeDifferenceMinutes(lastDescriptionSystemNote, note); // are they less than 10 minutes apart from the same user? - if (timeDifferenceMinutes > 10 || note.author.id !== lastDescriptionSystemNote.author.id) { + if ( + timeDifferenceMinutes > TIME_DIFFERENCE_VALUE || + note.author.id !== lastDescriptionSystemNote.author.id || + lastDescriptionSystemNote.description_version_deleted + ) { // update the previous system note lastDescriptionSystemNote = note; lastDescriptionSystemNoteIndex = acc.length; diff --git a/app/assets/javascripts/notes/stores/modules/index.js b/app/assets/javascripts/notes/stores/modules/index.js index 771b80108b8..0e991f2f4f0 100644 --- a/app/assets/javascripts/notes/stores/modules/index.js +++ b/app/assets/javascripts/notes/stores/modules/index.js @@ -14,6 +14,7 @@ export default () => ({ isToggleStateButtonLoading: false, isNotesFetched: false, isLoading: true, + isLoadingDescriptionVersion: false, // holds endpoints and permissions provided through haml notesData: { @@ -27,6 +28,7 @@ export default () => ({ commentsDisabled: false, resolvableDiscussionsCount: 0, unresolvedDiscussionsCount: 0, + descriptionVersion: null, }, actions, getters, diff --git a/app/assets/javascripts/notes/stores/mutation_types.js b/app/assets/javascripts/notes/stores/mutation_types.js index 8eb426d3f9b..6554aee0d5b 100644 --- a/app/assets/javascripts/notes/stores/mutation_types.js +++ b/app/assets/javascripts/notes/stores/mutation_types.js @@ -31,3 +31,11 @@ export const SET_CURRENT_DISCUSSION_ID = 'SET_CURRENT_DISCUSSION_ID'; export const CLOSE_ISSUE = 'CLOSE_ISSUE'; export const REOPEN_ISSUE = 'REOPEN_ISSUE'; export const TOGGLE_STATE_BUTTON_LOADING = 'TOGGLE_STATE_BUTTON_LOADING'; + +// Description version +export const REQUEST_DESCRIPTION_VERSION = 'REQUEST_DESCRIPTION_VERSION'; +export const RECEIVE_DESCRIPTION_VERSION = 'RECEIVE_DESCRIPTION_VERSION'; +export const RECEIVE_DESCRIPTION_VERSION_ERROR = 'RECEIVE_DESCRIPTION_VERSION_ERROR'; +export const REQUEST_DELETE_DESCRIPTION_VERSION = 'REQUEST_DELETE_DESCRIPTION_VERSION'; +export const RECEIVE_DELETE_DESCRIPTION_VERSION = 'RECEIVE_DELETE_DESCRIPTION_VERSION'; +export const RECEIVE_DELETE_DESCRIPTION_VERSION_ERROR = 'RECEIVE_DELETE_DESCRIPTION_VERSION_ERROR'; diff --git a/app/assets/javascripts/notes/stores/mutations.js b/app/assets/javascripts/notes/stores/mutations.js index 71091d26b85..d32a88e4c71 100644 --- a/app/assets/javascripts/notes/stores/mutations.js +++ b/app/assets/javascripts/notes/stores/mutations.js @@ -284,4 +284,25 @@ export default { [types.SET_CURRENT_DISCUSSION_ID](state, discussionId) { state.currentDiscussionId = discussionId; }, + + [types.REQUEST_DESCRIPTION_VERSION](state) { + state.isLoadingDescriptionVersion = true; + }, + [types.RECEIVE_DESCRIPTION_VERSION](state, descriptionVersion) { + state.isLoadingDescriptionVersion = false; + state.descriptionVersion = descriptionVersion; + }, + [types.RECEIVE_DESCRIPTION_VERSION_ERROR](state) { + state.isLoadingDescriptionVersion = false; + }, + [types.REQUEST_DELETE_DESCRIPTION_VERSION](state) { + state.isLoadingDescriptionVersion = true; + }, + [types.RECEIVE_DELETE_DESCRIPTION_VERSION](state, descriptionVersion) { + state.isLoadingDescriptionVersion = false; + state.descriptionVersion = descriptionVersion; + }, + [types.RECEIVE_DELETE_DESCRIPTION_VERSION_ERROR](state) { + state.isLoadingDescriptionVersion = false; + }, }; diff --git a/app/assets/javascripts/pipelines/pipeline_details_bundle.js b/app/assets/javascripts/pipelines/pipeline_details_bundle.js index 7e14b810c13..d9192d3d76b 100644 --- a/app/assets/javascripts/pipelines/pipeline_details_bundle.js +++ b/app/assets/javascripts/pipelines/pipeline_details_bundle.js @@ -132,7 +132,7 @@ export default () => { }); axios - .get(dataset.testReportEndpoint) + .get(dataset.testReportsCountEndpoint) .then(({ data }) => { document.querySelector('.js-test-report-badge-counter').innerHTML = data.total_count; }) diff --git a/app/assets/javascripts/vue_shared/components/notes/system_note.vue b/app/assets/javascripts/vue_shared/components/notes/system_note.vue index 15ca64ba297..0c4d75fb0ad 100644 --- a/app/assets/javascripts/vue_shared/components/notes/system_note.vue +++ b/app/assets/javascripts/vue_shared/components/notes/system_note.vue @@ -17,11 +17,12 @@ * /> */ import $ from 'jquery'; -import { mapGetters, mapActions } from 'vuex'; -import { GlSkeletonLoading } from '@gitlab/ui'; +import { mapGetters, mapActions, mapState } from 'vuex'; +import { GlButton, GlSkeletonLoading, GlTooltipDirective } from '@gitlab/ui'; import descriptionVersionHistoryMixin from 'ee_else_ce/notes/mixins/description_version_history'; import noteHeader from '~/notes/components/note_header.vue'; import Icon from '~/vue_shared/components/icon.vue'; +import glFeatureFlagsMixin from '~/vue_shared/mixins/gl_feature_flags_mixin'; import TimelineEntryItem from './timeline_entry_item.vue'; import { spriteIcon } from '../../../lib/utils/common_utils'; import initMRPopovers from '~/mr_popover/'; @@ -34,9 +35,13 @@ export default { Icon, noteHeader, TimelineEntryItem, + GlButton, GlSkeletonLoading, }, - mixins: [descriptionVersionHistoryMixin], + directives: { + GlTooltip: GlTooltipDirective, + }, + mixins: [descriptionVersionHistoryMixin, glFeatureFlagsMixin()], props: { note: { type: Object, @@ -50,6 +55,7 @@ export default { }, computed: { ...mapGetters(['targetNoteHash']), + ...mapState(['descriptionVersion', 'isLoadingDescriptionVersion']), noteAnchorId() { return `note_${this.note.id}`; }, @@ -80,7 +86,7 @@ export default { initMRPopovers(this.$el.querySelectorAll('.gfm-merge_request')); }, methods: { - ...mapActions(['fetchDescriptionVersion']), + ...mapActions(['fetchDescriptionVersion', 'softDeleteDescriptionVersion']), }, }; @@ -122,6 +128,16 @@ export default {

+          
+            
+          
         
       
     
diff --git a/app/assets/stylesheets/pages/notes.scss b/app/assets/stylesheets/pages/notes.scss
index 1da9f691639..1a06ae1ed41 100644
--- a/app/assets/stylesheets/pages/notes.scss
+++ b/app/assets/stylesheets/pages/notes.scss
@@ -311,13 +311,18 @@ $note-form-margin-left: 72px;
       overflow: hidden;
 
       .description-version {
+        position: relative;
+
+        .btn.delete-description-history {
+          position: absolute;
+          top: 18px;
+          right: 0;
+        }
+
         pre {
           max-height: $dropdown-max-height-lg;
           white-space: pre-wrap;
-
-          &.loading-state {
-            height: 94px;
-          }
+          padding-right: 30px;
         }
       }
 
diff --git a/app/models/commit_status_enums.rb b/app/models/commit_status_enums.rb
index dcf23d112d6..caebff91022 100644
--- a/app/models/commit_status_enums.rb
+++ b/app/models/commit_status_enums.rb
@@ -18,6 +18,7 @@ module CommitStatusEnums
       unmet_prerequisites: 10,
       scheduler_failure: 11,
       data_integrity_failure: 12,
+      forward_deployment_failure: 13,
       insufficient_bridge_permissions: 1_001,
       downstream_bridge_project_not_found: 1_002,
       invalid_bridge_trigger: 1_003,
diff --git a/app/models/deployment.rb b/app/models/deployment.rb
index 68f50b13a07..fe42fb93633 100644
--- a/app/models/deployment.rb
+++ b/app/models/deployment.rb
@@ -41,6 +41,9 @@ class Deployment < ApplicationRecord
 
   scope :visible, -> { where(status: %i[running success failed canceled]) }
   scope :stoppable, -> { where.not(on_stop: nil).where.not(deployable_id: nil).success }
+  scope :active, -> { where(status: %i[created running]) }
+  scope :older_than, -> (deployment) { where('id < ?', deployment.id) }
+  scope :with_deployable, -> { includes(:deployable).where('deployable_id IS NOT NULL') }
 
   state_machine :status, initial: :created do
     event :run do
@@ -74,6 +77,14 @@ class Deployment < ApplicationRecord
         Deployments::FinishedWorker.perform_async(id)
       end
     end
+
+    after_transition any => :running do |deployment|
+      next unless deployment.project.forward_deployment_enabled?
+
+      deployment.run_after_commit do
+        Deployments::ForwardDeploymentWorker.perform_async(id)
+      end
+    end
   end
 
   enum status: {
diff --git a/app/models/environment.rb b/app/models/environment.rb
index 4635b05fcc7..bb41c4a066e 100644
--- a/app/models/environment.rb
+++ b/app/models/environment.rb
@@ -12,6 +12,7 @@ class Environment < ApplicationRecord
 
   has_many :deployments, -> { visible }, dependent: :destroy # rubocop:disable Cop/ActiveRecordDependent
   has_many :successful_deployments, -> { success }, class_name: 'Deployment'
+  has_many :active_deployments, -> { active }, class_name: 'Deployment'
   has_many :prometheus_alerts, inverse_of: :environment
 
   has_one :last_deployment, -> { success.order('deployments.id DESC') }, class_name: 'Deployment'
diff --git a/app/models/member.rb b/app/models/member.rb
index 57924161b63..a26a0615a6e 100644
--- a/app/models/member.rb
+++ b/app/models/member.rb
@@ -75,6 +75,7 @@ class Member < ApplicationRecord
   scope :reporters, -> { active.where(access_level: REPORTER) }
   scope :developers, -> { active.where(access_level: DEVELOPER) }
   scope :maintainers, -> { active.where(access_level: MAINTAINER) }
+  scope :non_guests, -> { where('members.access_level > ?', GUEST) }
   scope :masters, -> { maintainers } # @deprecated
   scope :owners,  -> { active.where(access_level: OWNER) }
   scope :owners_and_maintainers, -> { active.where(access_level: [OWNER, MAINTAINER]) }
diff --git a/app/models/project.rb b/app/models/project.rb
index 1e27ce9f344..e16bd568153 100644
--- a/app/models/project.rb
+++ b/app/models/project.rb
@@ -343,6 +343,7 @@ class Project < ApplicationRecord
   delegate :last_pipeline, to: :commit, allow_nil: true
   delegate :external_dashboard_url, to: :metrics_setting, allow_nil: true, prefix: true
   delegate :default_git_depth, :default_git_depth=, to: :ci_cd_settings, prefix: :ci
+  delegate :forward_deployment_enabled, :forward_deployment_enabled=, :forward_deployment_enabled?, to: :ci_cd_settings
 
   # Validations
   validates :creator, presence: true, on: :create
diff --git a/app/models/project_ci_cd_setting.rb b/app/models/project_ci_cd_setting.rb
index a495d34c07c..b26a3025b61 100644
--- a/app/models/project_ci_cd_setting.rb
+++ b/app/models/project_ci_cd_setting.rb
@@ -18,6 +18,8 @@ class ProjectCiCdSetting < ApplicationRecord
     },
     allow_nil: true
 
+  default_value_for :forward_deployment_enabled, true
+
   def self.available?
     @available ||=
       ActiveRecord::Migrator.current_version >= MINIMUM_SCHEMA_VERSION
@@ -28,6 +30,10 @@ class ProjectCiCdSetting < ApplicationRecord
     super
   end
 
+  def forward_deployment_enabled?
+    super && ::Feature.enabled?(:forward_deployment_enabled, project)
+  end
+
   private
 
   def set_default_git_depth
diff --git a/app/presenters/commit_status_presenter.rb b/app/presenters/commit_status_presenter.rb
index ed76f95ac62..258852c77c6 100644
--- a/app/presenters/commit_status_presenter.rb
+++ b/app/presenters/commit_status_presenter.rb
@@ -14,6 +14,7 @@ class CommitStatusPresenter < Gitlab::View::Presenter::Delegated
     unmet_prerequisites: 'The job failed to complete prerequisite tasks',
     scheduler_failure: 'The scheduler failed to assign job to the runner, please try again or contact system administrator',
     data_integrity_failure: 'There has been a structural integrity problem detected, please contact system administrator',
+    forward_deployment_failure: 'The deployment job is older than the previously succeeded deployment job, and therefore cannot be run',
     invalid_bridge_trigger: 'This job could not be executed because downstream pipeline trigger definition is invalid',
     downstream_bridge_project_not_found: 'This job could not be executed because downstream bridge project could not be found',
     insufficient_bridge_permissions: 'This job could not be executed because of insufficient permissions to create a downstream pipeline',
diff --git a/app/services/deployments/older_deployments_drop_service.rb b/app/services/deployments/older_deployments_drop_service.rb
new file mode 100644
index 00000000000..122f8ac89ed
--- /dev/null
+++ b/app/services/deployments/older_deployments_drop_service.rb
@@ -0,0 +1,31 @@
+# frozen_string_literal: true
+
+module Deployments
+  class OlderDeploymentsDropService
+    attr_reader :deployment
+
+    def initialize(deployment_id)
+      @deployment = Deployment.find_by_id(deployment_id)
+    end
+
+    def execute
+      return unless @deployment&.running?
+
+      older_deployments.find_each do |older_deployment|
+        older_deployment.deployable&.drop!(:forward_deployment_failure)
+      rescue => e
+        Gitlab::ErrorTracking.track_exception(e, subject_id: @deployment.id, deployment_id: older_deployment.id)
+      end
+    end
+
+    private
+
+    def older_deployments
+      @deployment
+        .environment
+        .active_deployments
+        .older_than(@deployment)
+        .with_deployable
+    end
+  end
+end
diff --git a/app/views/help/_shortcuts.html.haml b/app/views/help/_shortcuts.html.haml
index 5f8f2333e40..4b9304cfdb9 100644
--- a/app/views/help/_shortcuts.html.haml
+++ b/app/views/help/_shortcuts.html.haml
@@ -6,6 +6,7 @@
           = _('Keyboard Shortcuts')
           %small
             = link_to _('(Show all)'), '#', class: 'js-more-help-button'
+        .js-toggle-shortcuts
         %button.close{ type: "button", "data-dismiss": "modal", "aria-label" => _('Close') }
           %span{ "aria-hidden": true } ×
       .modal-body
diff --git a/app/views/layouts/header/_help_dropdown.html.haml b/app/views/layouts/header/_help_dropdown.html.haml
index 93854c212df..a003d6f8903 100644
--- a/app/views/layouts/header/_help_dropdown.html.haml
+++ b/app/views/layouts/header/_help_dropdown.html.haml
@@ -4,6 +4,10 @@
       = link_to _("Help"), help_path
     %li
       = link_to _("Support"), support_url
+    %li
+      %button.js-shortcuts-modal-trigger{ type: "button" }
+        = _("Keyboard shortcuts")
+        %span.text-secondary.float-right{ "aria-hidden": true }= '?'.html_safe
     = render_if_exists "shared/learn_gitlab_menu_item"
     %li.divider
   %li
diff --git a/app/views/projects/pipelines/show.html.haml b/app/views/projects/pipelines/show.html.haml
index f0b3ab24ea0..f39968eecef 100644
--- a/app/views/projects/pipelines/show.html.haml
+++ b/app/views/projects/pipelines/show.html.haml
@@ -21,4 +21,5 @@
     = render "projects/pipelines/with_tabs", pipeline: @pipeline
 
 .js-pipeline-details-vue{ data: { endpoint: project_pipeline_path(@project, @pipeline, format: :json),
-  test_report_endpoint: test_report_project_pipeline_path(@project, @pipeline, format: :json) } }
+  test_report_endpoint: test_report_project_pipeline_path(@project, @pipeline, format: :json),
+  test_reports_count_endpoint: test_reports_count_project_pipeline_path(@project, @pipeline, format: :json) } }
diff --git a/app/workers/all_queues.yml b/app/workers/all_queues.yml
index 5ff1a331b09..f6daab73689 100644
--- a/app/workers/all_queues.yml
+++ b/app/workers/all_queues.yml
@@ -225,6 +225,12 @@
   :latency_sensitive: 
   :resource_boundary: :cpu
   :weight: 3
+- :name: deployment:deployments_forward_deployment
+  :feature_category: :continuous_delivery
+  :has_external_dependencies: 
+  :latency_sensitive: 
+  :resource_boundary: :unknown
+  :weight: 3
 - :name: deployment:deployments_success
   :feature_category: :continuous_delivery
   :has_external_dependencies: 
diff --git a/app/workers/deployments/forward_deployment_worker.rb b/app/workers/deployments/forward_deployment_worker.rb
new file mode 100644
index 00000000000..a25b8ca0478
--- /dev/null
+++ b/app/workers/deployments/forward_deployment_worker.rb
@@ -0,0 +1,14 @@
+# frozen_string_literal: true
+
+module Deployments
+  class ForwardDeploymentWorker
+    include ApplicationWorker
+
+    queue_namespace :deployment
+    feature_category :continuous_delivery
+
+    def perform(deployment_id)
+      Deployments::OlderDeploymentsDropService.new(deployment_id).execute
+    end
+  end
+end
diff --git a/changelogs/unreleased/22113-improve-keyboard-shortcuts-or-allow-them-to-be-disabled.yml b/changelogs/unreleased/22113-improve-keyboard-shortcuts-or-allow-them-to-be-disabled.yml
new file mode 100644
index 00000000000..45957b4acb2
--- /dev/null
+++ b/changelogs/unreleased/22113-improve-keyboard-shortcuts-or-allow-them-to-be-disabled.yml
@@ -0,0 +1,5 @@
+---
+title: Allow keyboard shortcuts to be disabled
+merge_request: 18782
+author:
+type: added
diff --git a/changelogs/unreleased/25276-allow-forward-deploys-only.yml b/changelogs/unreleased/25276-allow-forward-deploys-only.yml
new file mode 100644
index 00000000000..a3d017880eb
--- /dev/null
+++ b/changelogs/unreleased/25276-allow-forward-deploys-only.yml
@@ -0,0 +1,5 @@
+---
+title: Allow to deploy only forward deployments
+merge_request: 22959
+author:
+type: changed
diff --git a/changelogs/unreleased/psi-mermaid-details.yml b/changelogs/unreleased/psi-mermaid-details.yml
new file mode 100644
index 00000000000..b5c8f7481bd
--- /dev/null
+++ b/changelogs/unreleased/psi-mermaid-details.yml
@@ -0,0 +1,5 @@
+---
+title: Correctly render mermaid digrams inside details blocks
+merge_request: 23662
+author:
+type: fixed
diff --git a/db/migrate/20200124143014_add_restrict_deployment_order_to_project_ci_cd_settings.rb b/db/migrate/20200124143014_add_restrict_deployment_order_to_project_ci_cd_settings.rb
new file mode 100644
index 00000000000..6a222ce8d51
--- /dev/null
+++ b/db/migrate/20200124143014_add_restrict_deployment_order_to_project_ci_cd_settings.rb
@@ -0,0 +1,9 @@
+# frozen_string_literal: true
+
+class AddRestrictDeploymentOrderToProjectCiCdSettings < ActiveRecord::Migration[5.2]
+  DOWNTIME = false
+
+  def change
+    add_column :project_ci_cd_settings, :forward_deployment_enabled, :boolean
+  end
+end
diff --git a/db/post_migrate/20200210062432_schedule_link_lfs_objects.rb b/db/post_migrate/20200210062432_schedule_link_lfs_objects.rb
index c955639c259..b401065dd94 100644
--- a/db/post_migrate/20200210062432_schedule_link_lfs_objects.rb
+++ b/db/post_migrate/20200210062432_schedule_link_lfs_objects.rb
@@ -4,32 +4,11 @@ class ScheduleLinkLfsObjects < ActiveRecord::Migration[6.0]
   include Gitlab::Database::MigrationHelpers
 
   DOWNTIME = false
-  MIGRATION = 'LinkLfsObjects'
-  BATCH_SIZE = 1_000
 
   disable_ddl_transaction!
 
-  class Project < ActiveRecord::Base
-    include EachBatch
-
-    self.table_name = 'projects'
-  end
-
   def up
-    fork_network_members =
-      Gitlab::BackgroundMigration::LinkLfsObjects::ForkNetworkMember
-        .select(1)
-        .with_non_existing_lfs_objects
-        .where('fork_network_members.project_id = projects.id')
-
-    forks = Project.where('EXISTS (?)', fork_network_members)
-
-    queue_background_migration_jobs_by_range_at_intervals(
-      forks,
-      MIGRATION,
-      BackgroundMigrationWorker.minimum_interval,
-      batch_size: BATCH_SIZE
-    )
+    # no-op as background migration being schedule times out in some instances
   end
 
   def down
diff --git a/db/schema.rb b/db/schema.rb
index f95662582ec..7b27b2524ce 100644
--- a/db/schema.rb
+++ b/db/schema.rb
@@ -3166,6 +3166,7 @@ ActiveRecord::Schema.define(version: 2020_02_13_204737) do
     t.boolean "group_runners_enabled", default: true, null: false
     t.boolean "merge_pipelines_enabled"
     t.integer "default_git_depth"
+    t.boolean "forward_deployment_enabled"
     t.index ["project_id"], name: "index_project_ci_cd_settings_on_project_id", unique: true
   end
 
diff --git a/doc/user/shortcuts.md b/doc/user/shortcuts.md
index e7cb5e59be8..1df1a8a8ba6 100644
--- a/doc/user/shortcuts.md
+++ b/doc/user/shortcuts.md
@@ -6,7 +6,10 @@ disqus_identifier: 'https://docs.gitlab.com/ee/workflow/shortcuts.html'
 # GitLab keyboard shortcuts
 
 GitLab has many useful keyboard shortcuts to make it easier to access different features.
-You can see the quick reference sheet within GitLab itself with Shift + ?.
+You can see a modal listing keyboard shortcuts within GitLab itself by pressing ?,
+or clicking **Keyboard shortcuts** in the Help menu at the top right.
+From [GitLab 12.8 onwards](https://gitlab.com/gitlab-org/gitlab/issues/22113),
+keyboard shortcuts can be disabled using the **Enable**/**Disable** toggle in this modal window.
 
 The [Global Shortcuts](#global-shortcuts) work from any area of GitLab, but you must
 be in specific pages for the other shortcuts to be available, as explained in each
diff --git a/lib/gitlab/background_migration/link_lfs_objects.rb b/lib/gitlab/background_migration/link_lfs_objects.rb
index 014bebc4258..69c03f617bf 100644
--- a/lib/gitlab/background_migration/link_lfs_objects.rb
+++ b/lib/gitlab/background_migration/link_lfs_objects.rb
@@ -24,24 +24,7 @@ module Gitlab
       end
 
       def perform(start_id, end_id)
-        select_query =
-          ForkNetworkMember
-            .select('lop.lfs_object_id, fork_network_members.project_id')
-            .with_non_existing_lfs_objects
-            .where(project_id: start_id..end_id)
-
-        return if select_query.empty?
-
-        execute <<-SQL
-          INSERT INTO lfs_objects_projects (lfs_object_id, project_id)
-          #{select_query.to_sql}
-        SQL
-      end
-
-      private
-
-      def execute(sql)
-        ::ActiveRecord::Base.connection.execute(sql)
+        # no-op as some queries times out
       end
     end
   end
diff --git a/lib/gitlab/ci/status/build/failed.rb b/lib/gitlab/ci/status/build/failed.rb
index 0915a98a0fa..b0b01538a30 100644
--- a/lib/gitlab/ci/status/build/failed.rb
+++ b/lib/gitlab/ci/status/build/failed.rb
@@ -19,6 +19,7 @@ module Gitlab
             unmet_prerequisites: 'unmet prerequisites',
             scheduler_failure: 'scheduler failure',
             data_integrity_failure: 'data integrity failure',
+            forward_deployment_failure: 'forward deployment failure',
             invalid_bridge_trigger: 'downstream pipeline trigger definition is invalid',
             downstream_bridge_project_not_found: 'downstream project could not be found',
             insufficient_bridge_permissions: 'no permissions to trigger downstream pipeline',
diff --git a/lib/quality/kubernetes_client.rb b/lib/quality/kubernetes_client.rb
index db21c0b013b..453b9d21adb 100644
--- a/lib/quality/kubernetes_client.rb
+++ b/lib/quality/kubernetes_client.rb
@@ -63,7 +63,7 @@ module Quality
         'get',
         RESOURCE_LIST,
         %(--namespace "#{namespace}"),
-        '-o custom-columns=NAME:.metadata.name'
+        '-o name'
       ]
       run_command(command).lines.map(&:strip)
     end
diff --git a/locale/gitlab.pot b/locale/gitlab.pot
index c7590ccb1fb..b8ed2c62e0c 100644
--- a/locale/gitlab.pot
+++ b/locale/gitlab.pot
@@ -375,6 +375,12 @@ msgid_plural "%{releases} releases"
 msgstr[0] ""
 msgstr[1] ""
 
+msgid "%{screenreaderOnlyStart}Keyboard shorcuts%{screenreaderOnlyEnd} Disabled"
+msgstr ""
+
+msgid "%{screenreaderOnlyStart}Keyboard shorcuts%{screenreaderOnlyEnd} Enabled"
+msgstr ""
+
 msgid "%{service_title} activated."
 msgstr ""
 
@@ -7155,6 +7161,9 @@ msgstr ""
 msgid "Enable mirror configuration"
 msgstr ""
 
+msgid "Enable or disable keyboard shortcuts"
+msgstr ""
+
 msgid "Enable or disable the Pseudonymizer data collection."
 msgstr ""
 
@@ -10945,6 +10954,9 @@ msgstr ""
 msgid "Keyboard Shortcuts"
 msgstr ""
 
+msgid "Keyboard shortcuts"
+msgstr ""
+
 msgid "Kubernetes"
 msgstr ""
 
@@ -15857,6 +15869,9 @@ msgstr ""
 msgid "Remove child epic from an epic"
 msgstr ""
 
+msgid "Remove description history"
+msgstr ""
+
 msgid "Remove due date"
 msgstr ""
 
@@ -17780,6 +17795,9 @@ msgstr ""
 msgid "Something went wrong while closing the %{issuable}. Please try again later"
 msgstr ""
 
+msgid "Something went wrong while deleting description changes. Please try again."
+msgstr ""
+
 msgid "Something went wrong while deleting the image."
 msgstr ""
 
diff --git a/spec/factories/projects.rb b/spec/factories/projects.rb
index 490ae9e84e7..9be0b308680 100644
--- a/spec/factories/projects.rb
+++ b/spec/factories/projects.rb
@@ -37,6 +37,7 @@ FactoryBot.define do
       group_runners_enabled { nil }
       import_status { nil }
       import_jid { nil }
+      forward_deployment_enabled { nil }
     end
 
     after(:create) do |project, evaluator|
diff --git a/spec/features/markdown/mermaid_spec.rb b/spec/features/markdown/mermaid_spec.rb
index 4520d1bb2da..542caccb18d 100644
--- a/spec/features/markdown/mermaid_spec.rb
+++ b/spec/features/markdown/mermaid_spec.rb
@@ -68,4 +68,34 @@ describe 'Mermaid rendering', :js do
       expect(page).to have_selector('pre.mermaid')
     end
   end
+
+  it 'correctly sizes mermaid diagram inside 
block', :js do + description = <<~MERMAID +
+ Click to show diagram + + ```mermaid + graph TD; + A-->B; + A-->C; + B-->D; + C-->D; + ``` + +
+ MERMAID + + project = create(:project, :public) + issue = create(:issue, project: project, description: description) + + visit project_issue_path(project, issue) + + page.within('.description') do + page.find('summary').click + svg = page.find('svg.mermaid') + + expect(svg[:width].to_i).to be_within(5).of(120) + expect(svg[:height].to_i).to be_within(5).of(220) + end + end end diff --git a/spec/features/projects/user_uses_shortcuts_spec.rb b/spec/features/projects/user_uses_shortcuts_spec.rb index c6efe1f1896..beed1c07e51 100644 --- a/spec/features/projects/user_uses_shortcuts_spec.rb +++ b/spec/features/projects/user_uses_shortcuts_spec.rb @@ -17,6 +17,59 @@ describe 'User uses shortcuts', :js do wait_for_requests end + context 'disabling shortcuts' do + before do + page.evaluate_script("localStorage.removeItem('shortcutsDisabled')") + end + + it 'can disable shortcuts from help menu' do + open_modal_shortcut_keys + click_toggle_button + close_modal + + open_modal_shortcut_keys + + # modal-shortcuts still in the DOM, but hidden + expect(find('#modal-shortcuts', visible: false)).not_to be_visible + + page.refresh + open_modal_shortcut_keys + + # after reload, shortcuts modal doesn't exist at all until we add it + expect(page).not_to have_selector('#modal-shortcuts') + end + + it 're-enables shortcuts' do + open_modal_shortcut_keys + click_toggle_button + close_modal + + open_modal_from_help_menu + click_toggle_button + close_modal + + open_modal_shortcut_keys + expect(find('#modal-shortcuts')).to be_visible + end + + def open_modal_shortcut_keys + find('body').native.send_key('?') + end + + def open_modal_from_help_menu + find('.header-help-dropdown-toggle').click + find('button', text: 'Keyboard shortcuts').click + end + + def click_toggle_button + find('.js-toggle-shortcuts .gl-toggle').click + end + + def close_modal + find('.modal button[aria-label="Close"]').click + end + end + context 'when navigating to the Project pages' do it 'redirects to the details page' do visit project_issues_path(project) diff --git a/spec/lib/gitlab/background_migration/link_lfs_objects_spec.rb b/spec/lib/gitlab/background_migration/link_lfs_objects_spec.rb deleted file mode 100644 index 2a33af2c644..00000000000 --- a/spec/lib/gitlab/background_migration/link_lfs_objects_spec.rb +++ /dev/null @@ -1,66 +0,0 @@ -# frozen_string_literal: true - -require 'spec_helper' - -describe Gitlab::BackgroundMigration::LinkLfsObjects, :migration, schema: 2020_02_10_062432 do - let(:namespaces) { table(:namespaces) } - let(:projects) { table(:projects) } - let(:fork_networks) { table(:fork_networks) } - let(:fork_network_members) { table(:fork_network_members) } - let(:lfs_objects) { table(:lfs_objects) } - let(:lfs_objects_projects) { table(:lfs_objects_projects) } - let(:namespace) { namespaces.create(name: 'GitLab', path: 'gitlab') } - let(:source_project) { projects.create(namespace_id: namespace.id) } - let(:another_source_project) { projects.create(namespace_id: namespace.id) } - let(:project) { projects.create(namespace_id: namespace.id) } - let(:another_project) { projects.create(namespace_id: namespace.id) } - let(:other_project) { projects.create(namespace_id: namespace.id) } - let(:linked_project) { projects.create(namespace_id: namespace.id) } - let(:fork_network) { fork_networks.create(root_project_id: source_project.id) } - let(:another_fork_network) { fork_networks.create(root_project_id: another_source_project.id) } - let(:lfs_object) { lfs_objects.create(oid: 'abc123', size: 100) } - let(:another_lfs_object) { lfs_objects.create(oid: 'def456', size: 200) } - - before do - # Create links between projects - fork_network_members.create(fork_network_id: fork_network.id, project_id: source_project.id, forked_from_project_id: nil) - - [project, another_project, linked_project].each do |p| - fork_network_members.create( - fork_network_id: fork_network.id, - project_id: p.id, - forked_from_project_id: fork_network.root_project_id - ) - end - - fork_network_members.create(fork_network_id: another_fork_network.id, project_id: another_source_project.id, forked_from_project_id: nil) - fork_network_members.create(fork_network_id: another_fork_network.id, project_id: other_project.id, forked_from_project_id: another_fork_network.root_project_id) - - # Links LFS objects to some projects - [source_project, another_source_project, linked_project].each do |p| - lfs_objects_projects.create(lfs_object_id: lfs_object.id, project_id: p.id) - lfs_objects_projects.create(lfs_object_id: another_lfs_object.id, project_id: p.id) - end - end - - it 'creates LfsObjectsProject records for forks within the specified range of project IDs' do - expect { subject.perform(project.id, other_project.id) }.to change { lfs_objects_projects.count }.by(6) - - expect(lfs_object_ids_for(project)).to match_array(lfs_object_ids_for(source_project)) - expect(lfs_object_ids_for(another_project)).to match_array(lfs_object_ids_for(source_project)) - expect(lfs_object_ids_for(other_project)).to match_array(lfs_object_ids_for(another_source_project)) - - expect { subject.perform(project.id, other_project.id) }.not_to change { lfs_objects_projects.count } - end - - context 'when it is not necessary to create LfsObjectProject records' do - it 'does not create LfsObjectProject records' do - expect { subject.perform(linked_project.id, linked_project.id) } - .not_to change { lfs_objects_projects.count } - end - end - - def lfs_object_ids_for(project) - lfs_objects_projects.where(project_id: project.id).pluck(:lfs_object_id) - end -end diff --git a/spec/lib/quality/kubernetes_client_spec.rb b/spec/lib/quality/kubernetes_client_spec.rb index 6a62ef456c1..3a362dfccbf 100644 --- a/spec/lib/quality/kubernetes_client_spec.rb +++ b/spec/lib/quality/kubernetes_client_spec.rb @@ -102,7 +102,7 @@ RSpec.describe Quality::KubernetesClient do it 'calls kubectl to retrieve the resource names' do expect(Gitlab::Popen).to receive(:popen_with_detail) .with(["kubectl get #{described_class::RESOURCE_LIST} " + - %(--namespace "#{namespace}" -o custom-columns=NAME:.metadata.name)]) + %(--namespace "#{namespace}" -o name)]) .and_return(Gitlab::Popen::Result.new([], raw_resource_names_str, '', double(success?: true))) expect(subject.__send__(:raw_resource_names)).to eq(raw_resource_names) diff --git a/spec/migrations/schedule_link_lfs_objects_spec.rb b/spec/migrations/schedule_link_lfs_objects_spec.rb deleted file mode 100644 index 87632bc7648..00000000000 --- a/spec/migrations/schedule_link_lfs_objects_spec.rb +++ /dev/null @@ -1,82 +0,0 @@ -# frozen_string_literal: true - -require 'spec_helper' -require Rails.root.join('db', 'post_migrate', '20200210062432_schedule_link_lfs_objects.rb') - -describe ScheduleLinkLfsObjects, :migration, :sidekiq do - let(:namespaces) { table(:namespaces) } - let(:projects) { table(:projects) } - let(:fork_networks) { table(:fork_networks) } - let(:fork_network_members) { table(:fork_network_members) } - let(:lfs_objects) { table(:lfs_objects) } - let(:lfs_objects_projects) { table(:lfs_objects_projects) } - let(:namespace) { namespaces.create(name: 'GitLab', path: 'gitlab') } - let(:fork_network) { fork_networks.create(root_project_id: source_project.id) } - let(:another_fork_network) { fork_networks.create(root_project_id: another_source_project.id) } - let(:source_project) { projects.create(namespace_id: namespace.id) } - let(:another_source_project) { projects.create(namespace_id: namespace.id) } - let(:project) { projects.create(namespace_id: namespace.id) } - let(:another_project) { projects.create(namespace_id: namespace.id) } - let(:other_project) { projects.create(namespace_id: namespace.id) } - let(:linked_project) { projects.create(namespace_id: namespace.id) } - let(:lfs_object) { lfs_objects.create(oid: 'abc123', size: 100) } - let(:another_lfs_object) { lfs_objects.create(oid: 'def456', size: 200) } - - before do - # Create links between projects - fork_network_members.create(fork_network_id: fork_network.id, project_id: source_project.id, forked_from_project_id: nil) - - [project, another_project, linked_project].each do |p| - fork_network_members.create( - fork_network_id: fork_network.id, - project_id: p.id, - forked_from_project_id: fork_network.root_project_id - ) - end - - fork_network_members.create(fork_network_id: another_fork_network.id, project_id: another_source_project.id, forked_from_project_id: nil) - fork_network_members.create(fork_network_id: another_fork_network.id, project_id: other_project.id, forked_from_project_id: another_fork_network.root_project_id) - end - - context 'when there are forks to be backfilled' do - before do - stub_const("#{described_class.name}::BATCH_SIZE", 2) - - # Links LFS objects to some projects - [source_project, another_source_project, linked_project].each do |p| - lfs_objects_projects.create(lfs_object_id: lfs_object.id, project_id: p.id) - lfs_objects_projects.create(lfs_object_id: another_lfs_object.id, project_id: p.id) - end - end - - it 'schedules background migration to link LFS objects' do - Sidekiq::Testing.fake! do - migrate! - - expect(BackgroundMigrationWorker.jobs.size).to eq(2) - expect(described_class::MIGRATION) - .to be_scheduled_delayed_migration(2.minutes, project.id, another_project.id) - expect(described_class::MIGRATION) - .to be_scheduled_delayed_migration(4.minutes, other_project.id, other_project.id) - end - end - end - - context 'when there are no forks to be backfilled' do - before do - # Links LFS objects to all projects - projects.all.each do |p| - lfs_objects_projects.create(lfs_object_id: lfs_object.id, project_id: p.id) - lfs_objects_projects.create(lfs_object_id: another_lfs_object.id, project_id: p.id) - end - end - - it 'does not schedule any job' do - Sidekiq::Testing.fake! do - migrate! - - expect(BackgroundMigrationWorker.jobs.size).to eq(0) - end - end - end -end diff --git a/spec/models/deployment_spec.rb b/spec/models/deployment_spec.rb index 257f699a459..ab7e12cd43c 100644 --- a/spec/models/deployment_spec.rb +++ b/spec/models/deployment_spec.rb @@ -281,6 +281,45 @@ describe Deployment do expect(last_deployments).to match_array(deployments.last(2)) end end + + describe 'active' do + subject { described_class.active } + + it 'retrieves the active deployments' do + deployment1 = create(:deployment, status: :created ) + deployment2 = create(:deployment, status: :running ) + create(:deployment, status: :failed ) + create(:deployment, status: :canceled ) + + is_expected.to contain_exactly(deployment1, deployment2) + end + end + + describe 'older_than' do + let(:deployment) { create(:deployment) } + + subject { described_class.older_than(deployment) } + + it 'retrives the correct older deployments' do + older_deployment1 = create(:deployment) + older_deployment2 = create(:deployment) + deployment + create(:deployment) + + is_expected.to contain_exactly(older_deployment1, older_deployment2) + end + end + + describe 'with_deployable' do + subject { described_class.with_deployable } + + it 'retrieves deployments with deployable builds' do + with_deployable = create(:deployment) + create(:deployment, deployable: nil) + + is_expected.to contain_exactly(with_deployable) + end + end end describe '#includes_commit?' do diff --git a/spec/models/project_ci_cd_setting_spec.rb b/spec/models/project_ci_cd_setting_spec.rb index eb3a7e527c9..312cbbb0948 100644 --- a/spec/models/project_ci_cd_setting_spec.rb +++ b/spec/models/project_ci_cd_setting_spec.rb @@ -32,6 +32,12 @@ describe ProjectCiCdSetting do end end + describe '#forward_deployment_enabled' do + it 'is true by default' do + expect(described_class.new.forward_deployment_enabled).to be_truthy + end + end + describe '#default_git_depth' do let(:default_value) { described_class::DEFAULT_GIT_DEPTH } diff --git a/spec/services/deployments/older_deployments_drop_service_spec.rb b/spec/services/deployments/older_deployments_drop_service_spec.rb new file mode 100644 index 00000000000..44e9af07e46 --- /dev/null +++ b/spec/services/deployments/older_deployments_drop_service_spec.rb @@ -0,0 +1,78 @@ +# frozen_string_literal: true + +require 'spec_helper' + +describe Deployments::OlderDeploymentsDropService do + let(:environment) { create(:environment) } + let(:deployment) { create(:deployment, environment: environment) } + let(:service) { described_class.new(deployment) } + + describe '#execute' do + subject { service.execute } + + shared_examples 'it does not drop any build' do + it do + expect { subject }.to not_change(Ci::Build.failed, :count) + end + end + + context 'when deployment is nil' do + let(:deployment) { nil } + + it_behaves_like 'it does not drop any build' + end + + context 'when a deployment is passed in' do + context 'and there is no active deployment for the related environment' do + let(:deployment) { create(:deployment, :canceled, environment: environment) } + let(:deployment2) { create(:deployment, :canceled, environment: environment) } + + before do + deployment + deployment2 + end + + it_behaves_like 'it does not drop any build' + end + + context 'and there are active deployment for the related environment' do + let(:deployment) { create(:deployment, :running, environment: environment) } + let(:deployment2) { create(:deployment, :running, environment: environment) } + + context 'and there is no older deployment than "deployment"' do + before do + deployment + deployment2 + end + + it_behaves_like 'it does not drop any build' + end + + context 'and there is an older deployment than "deployment"' do + let(:older_deployment) { create(:deployment, :running, environment: environment) } + + before do + older_deployment + deployment + deployment2 + end + + it 'drops that older deployment' do + deployable = older_deployment.deployable + expect(deployable.failed?).to be_falsey + + subject + + expect(deployable.reload.failed?).to be_truthy + end + + context 'and there is no deployable for that older deployment' do + let(:older_deployment) { create(:deployment, :running, environment: environment, deployable: nil) } + + it_behaves_like 'it does not drop any build' + end + end + end + end + end +end -- cgit v1.2.3