Commit 53ae6b7e authored by GitLab Bot's avatar GitLab Bot

Add latest changes from gitlab-org/gitlab@master

parent cfe63cce
import flash from '~/flash'; import flash from '~/flash';
import $ from 'jquery';
import { sprintf, __ } from '../../locale'; import { sprintf, __ } from '../../locale';
// Renders diagrams and flowcharts from text using Mermaid in any element with the // Renders diagrams and flowcharts from text using Mermaid in any element with the
...@@ -18,7 +19,7 @@ import { sprintf, __ } from '../../locale'; ...@@ -18,7 +19,7 @@ import { sprintf, __ } from '../../locale';
// This is an arbitrary number; Can be iterated upon when suitable. // This is an arbitrary number; Can be iterated upon when suitable.
const MAX_CHAR_LIMIT = 5000; const MAX_CHAR_LIMIT = 5000;
export default function renderMermaid($els) { function renderMermaids($els) {
if (!$els.length) return; if (!$els.length) return;
// A diagram may have been truncated in search results which will cause errors, so abort the render. // 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) { ...@@ -95,3 +96,19 @@ export default function renderMermaid($els) {
flash(`Can't load mermaid module: ${err}`); 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'));
}
});
}
import $ from 'jquery'; import $ from 'jquery';
import Cookies from 'js-cookie'; import Cookies from 'js-cookie';
import Mousetrap from 'mousetrap'; 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 axios from '../../lib/utils/axios_utils';
import { refreshCurrentPage, visitUrl } from '../../lib/utils/url_utility'; import { refreshCurrentPage, visitUrl } from '../../lib/utils/url_utility';
import findAndFollowLink from '../../lib/utils/navigation_utility'; import findAndFollowLink from '../../lib/utils/navigation_utility';
...@@ -15,6 +18,15 @@ Mousetrap.stopCallback = (e, element, combo) => { ...@@ -15,6 +18,15 @@ Mousetrap.stopCallback = (e, element, combo) => {
return defaultStopCallback(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 { export default class Shortcuts {
constructor() { constructor() {
this.onToggleHelp = this.onToggleHelp.bind(this); this.onToggleHelp = this.onToggleHelp.bind(this);
...@@ -48,6 +60,14 @@ export default class Shortcuts { ...@@ -48,6 +60,14 @@ export default class Shortcuts {
$(this).remove(); $(this).remove();
e.preventDefault(); e.preventDefault();
}); });
$('.js-shortcuts-modal-trigger')
.off('click')
.on('click', this.onToggleHelp);
if (shouldDisableShortcuts()) {
disableShortcuts();
}
} }
onToggleHelp(e) { onToggleHelp(e) {
...@@ -104,7 +124,8 @@ export default class Shortcuts { ...@@ -104,7 +124,8 @@ export default class Shortcuts {
} }
return $('.js-more-help-button').remove(); return $('.js-more-help-button').remove();
}); })
.then(initToggleButton);
} }
focusFilter(e) { focusFilter(e) {
......
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();
}
<script>
import { GlToggle, GlSprintf } from '@gitlab/ui';
import AccessorUtilities from '~/lib/utils/accessor';
import { disableShortcuts, enableShortcuts, shouldDisableShortcuts } from './shortcuts_toggle';
export default {
components: {
GlSprintf,
GlToggle,
},
data() {
return {
localStorageUsable: AccessorUtilities.isLocalStorageAccessSafe(),
shortcutsEnabled: !shouldDisableShortcuts(),
};
},
methods: {
onChange(value) {
this.shortcutsEnabled = value;
if (value) {
enableShortcuts();
} else {
disableShortcuts();
}
},
},
};
</script>
<template>
<div v-if="localStorageUsable" class="d-inline-flex align-items-center js-toggle-shortcuts">
<gl-toggle
v-model="shortcutsEnabled"
aria-describedby="shortcutsToggle"
class="prepend-left-10 mb-0"
label-position="right"
@change="onChange"
>
<template #labelOn>
<gl-sprintf
:message="__('%{screenreaderOnlyStart}Keyboard shorcuts%{screenreaderOnlyEnd} Enabled')"
>
<template #screenreaderOnly="{ content }">
<span class="sr-only">{{ content }}</span>
</template>
</gl-sprintf>
</template>
<template #labelOff>
<gl-sprintf
:message="__('%{screenreaderOnlyStart}Keyboard shorcuts%{screenreaderOnlyEnd} Disabled')"
>
<template #screenreaderOnly="{ content }">
<span class="sr-only">{{ content }}</span>
</template>
</gl-sprintf>
</template>
</gl-toggle>
<div id="shortcutsToggle" class="sr-only">{{ __('Enable or disable keyboard shortcuts') }}</div>
</div>
</template>
import Vue from 'vue'; import Vue from 'vue';
import pdfLab from '../../pdf/index.vue'; import pdfLab from '../../pdf/index.vue';
import { GlLoadingIcon } from '@gitlab/ui';
export default () => { export default () => {
const el = document.getElementById('js-pdf-viewer'); const el = document.getElementById('js-pdf-viewer');
...@@ -8,6 +9,7 @@ export default () => { ...@@ -8,6 +9,7 @@ export default () => {
el, el,
components: { components: {
pdfLab, pdfLab,
GlLoadingIcon,
}, },
data() { data() {
return { return {
...@@ -32,11 +34,7 @@ export default () => { ...@@ -32,11 +34,7 @@ export default () => {
<div <div
class="text-center loading" class="text-center loading"
v-if="loading && !error"> v-if="loading && !error">
<i <gl-loading-icon class="mt-5" size="lg"/>
class="fa fa-spinner fa-spin"
aria-hidden="true"
aria-label="PDF loading">
</i>
</div> </div>
<pdf-lab <pdf-lab
v-if="!loadError" v-if="!loadError"
......
...@@ -12,7 +12,7 @@ export default class FilteredSearchDropdown { ...@@ -12,7 +12,7 @@ export default class FilteredSearchDropdown {
this.filter = filter; this.filter = filter;
this.dropdown = dropdown; this.dropdown = dropdown;
this.loadingTemplate = `<div class="filter-dropdown-loading"> this.loadingTemplate = `<div class="filter-dropdown-loading">
<i class="fa fa-spinner fa-spin"></i> <span class="spinner"></span>
</div>`; </div>`;
this.bindEvents(); this.bindEvents();
} }
......
...@@ -18,6 +18,7 @@ export const HISTORY_ONLY_FILTER_VALUE = 2; ...@@ -18,6 +18,7 @@ export const HISTORY_ONLY_FILTER_VALUE = 2;
export const DISCUSSION_FILTERS_DEFAULT_VALUE = 0; export const DISCUSSION_FILTERS_DEFAULT_VALUE = 0;
export const DISCUSSION_TAB_LABEL = 'show'; export const DISCUSSION_TAB_LABEL = 'show';
export const NOTE_UNDERSCORE = 'note_'; export const NOTE_UNDERSCORE = 'note_';
export const TIME_DIFFERENCE_VALUE = 10;
export const NOTEABLE_TYPE_MAPPING = { export const NOTEABLE_TYPE_MAPPING = {
Issue: ISSUE_NOTEABLE_TYPE, Issue: ISSUE_NOTEABLE_TYPE,
......
...@@ -3,10 +3,12 @@ ...@@ -3,10 +3,12 @@
export default { export default {
computed: { computed: {
canSeeDescriptionVersion() {}, canSeeDescriptionVersion() {},
canDeleteDescriptionVersion() {},
shouldShowDescriptionVersion() {}, shouldShowDescriptionVersion() {},
descriptionVersionToggleIcon() {}, descriptionVersionToggleIcon() {},
}, },
methods: { methods: {
toggleDescriptionVersion() {}, toggleDescriptionVersion() {},
deleteDescriptionVersion() {},
}, },
}; };
...@@ -491,23 +491,66 @@ export const convertToDiscussion = ({ commit }, noteId) => ...@@ -491,23 +491,66 @@ export const convertToDiscussion = ({ commit }, noteId) =>
export const removeConvertedDiscussion = ({ commit }, noteId) => export const removeConvertedDiscussion = ({ commit }, noteId) =>
commit(types.REMOVE_CONVERTED_DISCUSSION, 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; let requestUrl = endpoint;
if (startingVersion) { if (startingVersion) {
requestUrl = mergeUrlParams({ start_version_id: startingVersion }, requestUrl); requestUrl = mergeUrlParams({ start_version_id: startingVersion }, requestUrl);
} }
dispatch('requestDescriptionVersion');
return axios return axios
.get(requestUrl) .get(requestUrl)
.then(res => res.data) .then(res => {
.catch(() => { dispatch('receiveDescriptionVersion', res.data);
})
.catch(error => {
dispatch('receiveDescriptionVersionError', error);
Flash(__('Something went wrong while fetching description changes. Please try again.')); Flash(__('Something went wrong while fetching description changes. Please try again.'));
}); });
}; };
export const setCurrentDiscussionId = ({ commit }, discussionId) => export const requestDescriptionVersion = ({ commit }) => {
commit(types.SET_CURRENT_DISCUSSION_ID, discussionId); 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 // prevent babel-plugin-rewire from generating an invalid default during karma tests
export default () => {}; export default () => {};
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 * Checks the time difference between two notes from their 'created_at' dates
...@@ -45,7 +45,11 @@ export const collapseSystemNotes = notes => { ...@@ -45,7 +45,11 @@ export const collapseSystemNotes = notes => {
const timeDifferenceMinutes = getTimeDifferenceMinutes(lastDescriptionSystemNote, note); const timeDifferenceMinutes = getTimeDifferenceMinutes(lastDescriptionSystemNote, note);
// are they less than 10 minutes apart from the same user? // 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 // update the previous system note
lastDescriptionSystemNote = note; lastDescriptionSystemNote = note;
lastDescriptionSystemNoteIndex = acc.length; lastDescriptionSystemNoteIndex = acc.length;
......
...@@ -14,6 +14,7 @@ export default () => ({ ...@@ -14,6 +14,7 @@ export default () => ({
isToggleStateButtonLoading: false, isToggleStateButtonLoading: false,
isNotesFetched: false, isNotesFetched: false,
isLoading: true, isLoading: true,
isLoadingDescriptionVersion: false,
// holds endpoints and permissions provided through haml // holds endpoints and permissions provided through haml
notesData: { notesData: {
...@@ -27,6 +28,7 @@ export default () => ({ ...@@ -27,6 +28,7 @@ export default () => ({
commentsDisabled: false, commentsDisabled: false,
resolvableDiscussionsCount: 0, resolvableDiscussionsCount: 0,
unresolvedDiscussionsCount: 0, unresolvedDiscussionsCount: 0,
descriptionVersion: null,
}, },
actions, actions,
getters, getters,
......
...@@ -31,3 +31,11 @@ export const SET_CURRENT_DISCUSSION_ID = 'SET_CURRENT_DISCUSSION_ID'; ...@@ -31,3 +31,11 @@ export const SET_CURRENT_DISCUSSION_ID = 'SET_CURRENT_DISCUSSION_ID';
export const CLOSE_ISSUE = 'CLOSE_ISSUE'; export const CLOSE_ISSUE = 'CLOSE_ISSUE';
export const REOPEN_ISSUE = 'REOPEN_ISSUE'; export const REOPEN_ISSUE = 'REOPEN_ISSUE';
export const TOGGLE_STATE_BUTTON_LOADING = 'TOGGLE_STATE_BUTTON_LOADING'; 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';
...@@ -284,4 +284,25 @@ export default { ...@@ -284,4 +284,25 @@ export default {
[types.SET_CURRENT_DISCUSSION_ID](state, discussionId) { [types.SET_CURRENT_DISCUSSION_ID](state, discussionId) {
state.currentDiscussionId = 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;
},
}; };
...@@ -132,7 +132,7 @@ export default () => { ...@@ -132,7 +132,7 @@ export default () => {
}); });
axios axios
.get(dataset.testReportEndpoint) .get(dataset.testReportsCountEndpoint)
.then(({ data }) => { .then(({ data }) => {
document.querySelector('.js-test-report-badge-counter').innerHTML = data.total_count; document.querySelector('.js-test-report-badge-counter').innerHTML = data.total_count;
}) })
......
...@@ -17,11 +17,12 @@ ...@@ -17,11 +17,12 @@
* /> * />
*/ */
import $ from 'jquery'; import $ from 'jquery';
import { mapGetters, mapActions } from 'vuex'; import { mapGetters, mapActions, mapState } from 'vuex';
import { GlSkeletonLoading } from '@gitlab/ui'; import { GlButton, GlSkeletonLoading, GlTooltipDirective } from '@gitlab/ui';
import descriptionVersionHistoryMixin from 'ee_else_ce/notes/mixins/description_version_history'; import descriptionVersionHistoryMixin from 'ee_else_ce/notes/mixins/description_version_history';
import noteHeader from '~/notes/components/note_header.vue'; import noteHeader from '~/notes/components/note_header.vue';
import Icon from '~/vue_shared/components/icon.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 TimelineEntryItem from './timeline_entry_item.vue';
import { spriteIcon } from '../../../lib/utils/common_utils'; import { spriteIcon } from '../../../lib/utils/common_utils';
import initMRPopovers from '~/mr_popover/'; import initMRPopovers from '~/mr_popover/';
...@@ -34,9 +35,13 @@ export default { ...@@ -34,9 +35,13 @@ export default {
Icon, Icon,
noteHeader, noteHeader,
TimelineEntryItem, TimelineEntryItem,
GlButton,
GlSkeletonLoading, GlSkeletonLoading,
}, },
mixins: [descriptionVersionHistoryMixin], directives: {
GlTooltip: GlTooltipDirective,
},
mixins: [descriptionVersionHistoryMixin, glFeatureFlagsMixin()],
props: { props: {
note: { note: {
type: Object, type: Object,
...@@ -50,6 +55,7 @@ export default { ...@@ -50,6 +55,7 @@ export default {
}, },
computed: { computed: {
...mapGetters(['targetNoteHash']), ...mapGetters(['targetNoteHash']),
...mapState(['descriptionVersion', 'isLoadingDescriptionVersion']),
noteAnchorId() { noteAnchorId() {
return `note_${this.note.id}`; return `note_${this.note.id}`;
}, },
...@@ -80,7 +86,7 @@ export default { ...@@ -80,7 +86,7 @@ export default {
initMRPopovers(this.$el.querySelectorAll('.gfm-merge_request')); initMRPopovers(this.$el.querySelectorAll('.gfm-merge_request'));
}, },
methods: { methods: {
...mapActions(['fetchDescriptionVersion']), ...mapActions(['fetchDescriptionVersion', 'softDeleteDescriptionVersion']),
}, },
}; };
</script> </script>
...@@ -122,6 +128,16 @@ export default { ...@@ -122,6 +128,16 @@ export default {
<gl-skeleton-loading /> <gl-skeleton-loading />
</pre> </pre>
<pre v-else class="wrapper mt-2" v-html="descriptionVersion"></pre> <pre v-else class="wrapper mt-2" v-html="descriptionVersion"></pre>
<gl-button
v-if="canDeleteDescriptionVersion"
ref="deleteDescriptionVersionButton"
v-gl-tooltip
:title="__('Remove description history')"
class="btn-transparent delete-description-history"
@click="deleteDescriptionVersion"
>
<icon name="remove" />
</gl-button>
</div> </div>
</div> </div>
</div> </div>
......
...@@ -311,13 +311,18 @@ $note-form-margin-left: 72px; ...@@ -311,13 +311,18 @@ $note-form-margin-left: 72px;
overflow: hidden; overflow: hidden;
.description-version { .description-version {
position: relative;
.btn.delete-description-history {
position: absolute;
top: 18px;
right: 0;
}
pre { pre {
max-height: $dropdown-max-height-lg; max-height: $dropdown-max-height-lg;
white-space: pre-wrap; white-space: pre-wrap;
padding-right: 30px;
&.loading-state {
height: 94px;
}
} }
} }
......
...@@ -18,6 +18,7 @@ module CommitStatusEnums ...@@ -18,6 +18,7 @@ module CommitStatusEnums
unmet_prerequisites: 10, unmet_prerequisites: 10,
scheduler_failure: 11, scheduler_failure: 11,
data_integrity_failure: 12, data_integrity_failure: 12,
forward_deployment_failure: 13,
insufficient_bridge_permissions: 1_001, insufficient_bridge_permissions: 1_001,
downstream_bridge_project_not_found: 1_002, downstream_bridge_project_not_found: 1_002,
invalid_bridge_trigger: 1_003, invalid_bridge_trigger: 1_003,
......
...@@ -41,6 +41,9 @@ class Deployment < ApplicationRecord ...@@ -41,6 +41,9 @@ class Deployment < ApplicationRecord
scope :visible, -> { where(status: %i[running success failed canceled]) } scope :visible, -> { where(status: %i[running success failed canceled]) }
scope :stoppable, -> { where.not(on_stop: nil).where.not(deployable_id: nil).success } 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 state_machine :status, initial: :created do
event :run do event :run do
...@@ -74,6 +77,14 @@ class Deployment < ApplicationRecord ...@@ -74,6 +77,14 @@ class Deployment < ApplicationRecord
Deployments::FinishedWorker.perform_async(id) Deployments::FinishedWorker.perform_async(id)
end end
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 end
enum status: { enum status: {
......
...@@ -12,6 +12,7 @@ class Environment < ApplicationRecord ...@@ -12,6 +12,7 @@ class Environment < ApplicationRecord
has_many :deployments, -> { visible }, dependent: :destroy # rubocop:disable Cop/ActiveRecordDependent has_many :deployments, -> { visible }, dependent: :destroy # rubocop:disable Cop/ActiveRecordDependent
has_many :successful_deployments, -> { success }, class_name: 'Deployment' has_many :successful_deployments, -> { success }, class_name: 'Deployment'
has_many :active_deployments, -> { active }, class_name: 'Deployment'
has_many :prometheus_alerts, inverse_of: :environment has_many :prometheus_alerts, inverse_of: :environment
has_one :last_deployment, -> { success.order('deployments.id DESC') }, class_name: 'Deployment' has_one :last_deployment, -> { success.order('deployments.id DESC') }, class_name: 'Deployment'
......
...@@ -75,6 +75,7 @@ class Member < ApplicationRecord ...@@ -75,6 +75,7 @@ class Member < ApplicationRecord
scope :reporters, -> { active.where(access_level: REPORTER) } scope :reporters, -> { active.where(access_level: REPORTER) }
scope :developers, -> { active.where(access_level: DEVELOPER) } scope :developers, -> { active.where(access_level: DEVELOPER) }
scope :maintainers, -> { active.where(access_level: MAINTAINER) } scope :maintainers, -> { active.where(access_level: MAINTAINER) }
scope :non_guests, -> { where('members.access_level > ?', GUEST) }
scope :masters, -> { maintainers } # @deprecated scope :masters, -> { maintainers } # @deprecated
scope :owners, -> { active.where(access_level: OWNER) } scope :owners, -> { active.where(access_level: OWNER) }
scope :owners_and_maintainers, -> { active.where(access_level: [OWNER, MAINTAINER]) } scope :owners_and_maintainers, -> { active.where(access_level: [OWNER, MAINTAINER]) }
......
...@@ -343,6 +343,7 @@ class Project < ApplicationRecord ...@@ -343,6 +343,7 @@ class Project < ApplicationRecord
delegate :last_pipeline, to: :commit, allow_nil: true delegate :last_pipeline, to: :commit, allow_nil: true
delegate :external_dashboard_url, to: :metrics_setting, allow_nil: true, prefix: 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 :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 # Validations
validates :creator, presence: true, on: :create validates :creator, presence: true, on: :create
......
...@@ -18,6 +18,8 @@ class ProjectCiCdSetting < ApplicationRecord ...@@ -18,6 +18,8 @@ class ProjectCiCdSetting < ApplicationRecord
}, },
allow_nil: true allow_nil: true
default_value_for :forward_deployment_enabled, true
def self.available? def self.available?
@available ||= @available ||=
ActiveRecord::Migrator.current_version >= MINIMUM_SCHEMA_VERSION ActiveRecord::Migrator.current_version >= MINIMUM_SCHEMA_VERSION
...@@ -28,6 +30,10 @@ class ProjectCiCdSetting < ApplicationRecord ...@@ -28,6 +30,10 @@ class ProjectCiCdSetting < ApplicationRecord
super super
end end
def forward_deployment_enabled?
super && ::Feature.enabled?(:forward_deployment_enabled, project)
end
private private
def set_default_git_depth def set_default_git_depth
......
...@@ -14,6 +14,7 @@ class CommitStatusPresenter < Gitlab::View::Presenter::Delegated ...@@ -14,6 +14,7 @@ class CommitStatusPresenter < Gitlab::View::Presenter::Delegated
unmet_prerequisites: 'The job failed to complete prerequisite tasks', 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', 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', 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', 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', 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', insufficient_bridge_permissions: 'This job could not be executed because of insufficient permissions to create a downstream pipeline',
......
# 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
...@@ -6,6 +6,7 @@ ...@@ -6,6 +6,7 @@
= _('Keyboard Shortcuts') = _('Keyboard Shortcuts')
%small %small
= link_to _('(Show all)'), '#', class: 'js-more-help-button' = link_to _('(Show all)'), '#', class: 'js-more-help-button'
.js-toggle-shortcuts
%button.close{ type: "button", "data-dismiss": "modal", "aria-label" => _('Close') } %button.close{ type: "button", "data-dismiss": "modal", "aria-label" => _('Close') }
%span{ "aria-hidden": true } &times; %span{ "aria-hidden": true } &times;
.modal-body .modal-body
......
...@@ -4,6 +4,10 @@ ...@@ -4,6 +4,10 @@
= link_to _("Help"), help_path = link_to _("Help"), help_path
%li %li
= link_to _("Support"), support_url = 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" = render_if_exists "shared/learn_gitlab_menu_item"
%li.divider %li.divider
%li %li
......
...@@ -21,4 +21,5 @@ ...@@ -21,4 +21,5 @@
= render "projects/pipelines/with_tabs", pipeline: @pipeline = render "projects/pipelines/with_tabs", pipeline: @pipeline
.js-pipeline-details-vue{ data: { endpoint: project_pipeline_path(@project, @pipeline, format: :json), .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) } }
...@@ -225,6 +225,12 @@ ...@@ -225,6 +225,12 @@
:latency_sensitive: :latency_sensitive:
:resource_boundary: :cpu :resource_boundary: :cpu
:weight: 3 :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 - :name: deployment:deployments_success
:feature_category: :continuous_delivery :feature_category: :continuous_delivery
:has_external_dependencies: :has_external_dependencies:
......
# 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
---
title: Allow keyboard shortcuts to be disabled
merge_request: 18782
author:
type: added
---
title: Allow to deploy only forward deployments
merge_request: 22959
author:
type: changed
---
title: Correctly render mermaid digrams inside details blocks
merge_request: 23662
author:
type: fixed
# 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
...@@ -4,32 +4,11 @@ class ScheduleLinkLfsObjects < ActiveRecord::Migration[6.0] ...@@ -4,32 +4,11 @@ class ScheduleLinkLfsObjects < ActiveRecord::Migration[6.0]
include Gitlab::Database::MigrationHelpers include Gitlab::Database::MigrationHelpers
DOWNTIME = false DOWNTIME = false
MIGRATION = 'LinkLfsObjects'
BATCH_SIZE = 1_000
disable_ddl_transaction! disable_ddl_transaction!
class Project < ActiveRecord::Base
include EachBatch
self.table_name = 'projects'
end
def up def up
fork_network_members = # no-op as background migration being schedule times out in some instances
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
)
end end
def down def down
......
...@@ -3166,6 +3166,7 @@ ActiveRecord::Schema.define(version: 2020_02_13_204737) do ...@@ -3166,6 +3166,7 @@ ActiveRecord::Schema.define(version: 2020_02_13_204737) do
t.boolean "group_runners_enabled", default: true, null: false t.boolean "group_runners_enabled", default: true, null: false
t.boolean "merge_pipelines_enabled" t.boolean "merge_pipelines_enabled"
t.integer "default_git_depth" 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 t.index ["project_id"], name: "index_project_ci_cd_settings_on_project_id", unique: true
end end
......
...@@ -6,7 +6,10 @@ disqus_identifier: 'https://docs.gitlab.com/ee/workflow/shortcuts.html' ...@@ -6,7 +6,10 @@ disqus_identifier: 'https://docs.gitlab.com/ee/workflow/shortcuts.html'
# GitLab keyboard shortcuts # GitLab keyboard shortcuts
GitLab has many useful keyboard shortcuts to make it easier to access different features. 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 <kbd>Shift</kbd> + <kbd>?</kbd>. You can see a modal listing keyboard shortcuts within GitLab itself by pressing <kbd>?</kbd>,
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 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 be in specific pages for the other shortcuts to be available, as explained in each
......
...@@ -24,24 +24,7 @@ module Gitlab ...@@ -24,24 +24,7 @@ module Gitlab
end end
def perform(start_id, end_id) def perform(start_id, end_id)
select_query = # no-op as some queries times out
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)
end end
end end
end end
......
...@@ -19,6 +19,7 @@ module Gitlab ...@@ -19,6 +19,7 @@ module Gitlab
unmet_prerequisites: 'unmet prerequisites', unmet_prerequisites: 'unmet prerequisites',
scheduler_failure: 'scheduler failure', scheduler_failure: 'scheduler failure',
data_integrity_failure: 'data integrity failure', data_integrity_failure: 'data integrity failure',
forward_deployment_failure: 'forward deployment failure',
invalid_bridge_trigger: 'downstream pipeline trigger definition is invalid', invalid_bridge_trigger: 'downstream pipeline trigger definition is invalid',
downstream_bridge_project_not_found: 'downstream project could not be found', downstream_bridge_project_not_found: 'downstream project could not be found',
insufficient_bridge_permissions: 'no permissions to trigger downstream pipeline', insufficient_bridge_permissions: 'no permissions to trigger downstream pipeline',
......
...@@ -63,7 +63,7 @@ module Quality ...@@ -63,7 +63,7 @@ module Quality
'get', 'get',
RESOURCE_LIST, RESOURCE_LIST,
%(--namespace "#{namespace}"), %(--namespace "#{namespace}"),
'-o custom-columns=NAME:.metadata.name' '-o name'
] ]
run_command(command).lines.map(&:strip) run_command(command).lines.map(&:strip)
end end
......
...@@ -375,6 +375,12 @@ msgid_plural "%{releases} releases" ...@@ -375,6 +375,12 @@ msgid_plural "%{releases} releases"
msgstr[0] "" msgstr[0] ""
msgstr[1] "" msgstr[1] ""
msgid "%{screenreaderOnlyStart}Keyboard shorcuts%{screenreaderOnlyEnd} Disabled"
msgstr ""
msgid "%{screenreaderOnlyStart}Keyboard shorcuts%{screenreaderOnlyEnd} Enabled"
msgstr ""
msgid "%{service_title} activated." msgid "%{service_title} activated."
msgstr "" msgstr ""
...@@ -7155,6 +7161,9 @@ msgstr "" ...@@ -7155,6 +7161,9 @@ msgstr ""
msgid "Enable mirror configuration" msgid "Enable mirror configuration"
msgstr "" msgstr ""
msgid "Enable or disable keyboard shortcuts"
msgstr ""
msgid "Enable or disable the Pseudonymizer data collection." msgid "Enable or disable the Pseudonymizer data collection."
msgstr "" msgstr ""
...@@ -10945,6 +10954,9 @@ msgstr "" ...@@ -10945,6 +10954,9 @@ msgstr ""
msgid "Keyboard Shortcuts" msgid "Keyboard Shortcuts"
msgstr "" msgstr ""
msgid "Keyboard shortcuts"
msgstr ""
msgid "Kubernetes" msgid "Kubernetes"
msgstr "" msgstr ""
...@@ -15857,6 +15869,9 @@ msgstr "" ...@@ -15857,6 +15869,9 @@ msgstr ""
msgid "Remove child epic from an epic" msgid "Remove child epic from an epic"
msgstr "" msgstr ""
msgid "Remove description history"
msgstr ""
msgid "Remove due date" msgid "Remove due date"
msgstr "" msgstr ""
...@@ -17780,6 +17795,9 @@ msgstr "" ...@@ -17780,6 +17795,9 @@ msgstr ""
msgid "Something went wrong while closing the %{issuable}. Please try again later" msgid "Something went wrong while closing the %{issuable}. Please try again later"
msgstr "" msgstr ""
msgid "Something went wrong while deleting description changes. Please try again."
msgstr ""
msgid "Something went wrong while deleting the image." msgid "Something went wrong while deleting the image."
msgstr "" msgstr ""
......
...@@ -37,6 +37,7 @@ FactoryBot.define do ...@@ -37,6 +37,7 @@ FactoryBot.define do
group_runners_enabled { nil } group_runners_enabled { nil }
import_status { nil } import_status { nil }
import_jid { nil } import_jid { nil }
forward_deployment_enabled { nil }
end end
after(:create) do |project, evaluator| after(:create) do |project, evaluator|
......
...@@ -68,4 +68,34 @@ describe 'Mermaid rendering', :js do ...@@ -68,4 +68,34 @@ describe 'Mermaid rendering', :js do
expect(page).to have_selector('pre.mermaid') expect(page).to have_selector('pre.mermaid')
end end
end end
it 'correctly sizes mermaid diagram inside <details> block', :js do
description = <<~MERMAID
<details>
<summary>Click to show diagram</summary>
```mermaid
graph TD;
A-->B;
A-->C;
B-->D;
C-->D;
```
</details>
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 end
...@@ -17,6 +17,59 @@ describe 'User uses shortcuts', :js do ...@@ -17,6 +17,59 @@ describe 'User uses shortcuts', :js do
wait_for_requests wait_for_requests
end 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 context 'when navigating to the Project pages' do
it 'redirects to the details page' do it 'redirects to the details page' do
visit project_issues_path(project) visit project_issues_path(project)
......
# 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
...@@ -102,7 +102,7 @@ RSpec.describe Quality::KubernetesClient do ...@@ -102,7 +102,7 @@ RSpec.describe Quality::KubernetesClient do
it 'calls kubectl to retrieve the resource names' do it 'calls kubectl to retrieve the resource names' do
expect(Gitlab::Popen).to receive(:popen_with_detail) expect(Gitlab::Popen).to receive(:popen_with_detail)
.with(["kubectl get #{described_class::RESOURCE_LIST} " + .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))) .and_return(Gitlab::Popen::Result.new([], raw_resource_names_str, '', double(success?: true)))
expect(subject.__send__(:raw_resource_names)).to eq(raw_resource_names) expect(subject.__send__(:raw_resource_names)).to eq(raw_resource_names)
......
# 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
...@@ -281,6 +281,45 @@ describe Deployment do ...@@ -281,6 +281,45 @@ describe Deployment do
expect(last_deployments).to match_array(deployments.last(2)) expect(last_deployments).to match_array(deployments.last(2))
end end
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 end
describe '#includes_commit?' do describe '#includes_commit?' do
......
...@@ -32,6 +32,12 @@ describe ProjectCiCdSetting do ...@@ -32,6 +32,12 @@ describe ProjectCiCdSetting do
end end
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 describe '#default_git_depth' do
let(:default_value) { described_class::DEFAULT_GIT_DEPTH } let(:default_value) { described_class::DEFAULT_GIT_DEPTH }
......
# 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
Markdown is supported
0%
or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment