Add latest changes from gitlab-org/gitlab@master
This commit is contained in:
parent
2cfa1fc75d
commit
c0d8f9f3f9
|
@ -1,22 +1,23 @@
|
|||
<script>
|
||||
/* eslint-disable @gitlab/vue-i18n/no-bare-strings */
|
||||
import { format } from 'timeago.js';
|
||||
import _ from 'underscore';
|
||||
import { GlTooltipDirective } from '@gitlab/ui';
|
||||
import environmentItemMixin from 'ee_else_ce/environments/mixins/environment_item_mixin';
|
||||
import { __, sprintf } from '~/locale';
|
||||
import { convertObjectPropsToCamelCase } from '~/lib/utils/common_utils';
|
||||
import timeagoMixin from '~/vue_shared/mixins/timeago';
|
||||
import UserAvatarLink from '~/vue_shared/components/user_avatar/user_avatar_link.vue';
|
||||
import CommitComponent from '~/vue_shared/components/commit.vue';
|
||||
import Icon from '~/vue_shared/components/icon.vue';
|
||||
import TooltipOnTruncate from '~/vue_shared/components/tooltip_on_truncate.vue';
|
||||
import { __, sprintf } from '~/locale';
|
||||
import environmentItemMixin from 'ee_else_ce/environments/mixins/environment_item_mixin';
|
||||
import eventHub from '../event_hub';
|
||||
import ActionsComponent from './environment_actions.vue';
|
||||
import ExternalUrlComponent from './environment_external_url.vue';
|
||||
import StopComponent from './environment_stop.vue';
|
||||
import RollbackComponent from './environment_rollback.vue';
|
||||
import TerminalButtonComponent from './environment_terminal_button.vue';
|
||||
import MonitoringButtonComponent from './environment_monitoring.vue';
|
||||
import CommitComponent from '../../vue_shared/components/commit.vue';
|
||||
import eventHub from '../event_hub';
|
||||
import { convertObjectPropsToCamelCase } from '~/lib/utils/common_utils';
|
||||
import PinComponent from './environment_pin.vue';
|
||||
import RollbackComponent from './environment_rollback.vue';
|
||||
import StopComponent from './environment_stop.vue';
|
||||
import TerminalButtonComponent from './environment_terminal_button.vue';
|
||||
|
||||
/**
|
||||
* Environment Item Component
|
||||
|
@ -26,21 +27,22 @@ import { convertObjectPropsToCamelCase } from '~/lib/utils/common_utils';
|
|||
|
||||
export default {
|
||||
components: {
|
||||
CommitComponent,
|
||||
Icon,
|
||||
ActionsComponent,
|
||||
CommitComponent,
|
||||
ExternalUrlComponent,
|
||||
StopComponent,
|
||||
RollbackComponent,
|
||||
TerminalButtonComponent,
|
||||
Icon,
|
||||
MonitoringButtonComponent,
|
||||
PinComponent,
|
||||
RollbackComponent,
|
||||
StopComponent,
|
||||
TerminalButtonComponent,
|
||||
TooltipOnTruncate,
|
||||
UserAvatarLink,
|
||||
},
|
||||
directives: {
|
||||
GlTooltip: GlTooltipDirective,
|
||||
},
|
||||
mixins: [environmentItemMixin],
|
||||
mixins: [environmentItemMixin, timeagoMixin],
|
||||
|
||||
props: {
|
||||
canReadEnvironment: {
|
||||
|
@ -52,7 +54,12 @@ export default {
|
|||
model: {
|
||||
type: Object,
|
||||
required: true,
|
||||
default: () => ({}),
|
||||
},
|
||||
|
||||
shouldShowAutoStopDate: {
|
||||
type: Boolean,
|
||||
required: false,
|
||||
default: false,
|
||||
},
|
||||
|
||||
tableData: {
|
||||
|
@ -76,6 +83,16 @@ export default {
|
|||
return false;
|
||||
},
|
||||
|
||||
/**
|
||||
* Checkes whether the row displayed is a folder.
|
||||
*
|
||||
* @returns {Boolean}
|
||||
*/
|
||||
|
||||
isFolder() {
|
||||
return this.model.isFolder;
|
||||
},
|
||||
|
||||
/**
|
||||
* Checkes whether the environment is protected.
|
||||
* (`is_protected` currently only set in EE)
|
||||
|
@ -112,24 +129,64 @@ export default {
|
|||
},
|
||||
|
||||
/**
|
||||
* Verifies if the date to be shown is present.
|
||||
* Verifies if the autostop date is present.
|
||||
*
|
||||
* @returns {Boolean}
|
||||
*/
|
||||
canShowAutoStopDate() {
|
||||
if (!this.model.auto_stop_at) {
|
||||
return false;
|
||||
}
|
||||
|
||||
const autoStopDate = new Date(this.model.auto_stop_at);
|
||||
const now = new Date();
|
||||
|
||||
return now < autoStopDate;
|
||||
},
|
||||
|
||||
/**
|
||||
* Human readable deployment date.
|
||||
*
|
||||
* @returns {String}
|
||||
*/
|
||||
autoStopDate() {
|
||||
if (this.canShowAutoStopDate) {
|
||||
return {
|
||||
formatted: this.timeFormatted(this.model.auto_stop_at),
|
||||
tooltip: this.tooltipTitle(this.model.auto_stop_at),
|
||||
};
|
||||
}
|
||||
return {
|
||||
formatted: '',
|
||||
tooltip: '',
|
||||
};
|
||||
},
|
||||
|
||||
/**
|
||||
* Verifies if the deployment date is present.
|
||||
*
|
||||
* @returns {Boolean|Undefined}
|
||||
*/
|
||||
canShowDate() {
|
||||
canShowDeploymentDate() {
|
||||
return this.model && this.model.last_deployment && this.model.last_deployment.deployed_at;
|
||||
},
|
||||
|
||||
/**
|
||||
* Human readable date.
|
||||
* Human readable deployment date.
|
||||
*
|
||||
* @returns {String}
|
||||
*/
|
||||
deployedDate() {
|
||||
if (this.canShowDate) {
|
||||
return format(this.model.last_deployment.deployed_at);
|
||||
if (this.canShowDeploymentDate) {
|
||||
return {
|
||||
formatted: this.timeFormatted(this.model.last_deployment.deployed_at),
|
||||
tooltip: this.tooltipTitle(this.model.last_deployment.deployed_at),
|
||||
};
|
||||
}
|
||||
return '';
|
||||
return {
|
||||
formatted: '',
|
||||
tooltip: '',
|
||||
};
|
||||
},
|
||||
|
||||
actions() {
|
||||
|
@ -344,6 +401,15 @@ export default {
|
|||
return {};
|
||||
},
|
||||
|
||||
/**
|
||||
* Checkes whether to display no deployment text.
|
||||
*
|
||||
* @returns {Boolean}
|
||||
*/
|
||||
showNoDeployments() {
|
||||
return !this.hasLastDeploymentKey && !this.isFolder;
|
||||
},
|
||||
|
||||
/**
|
||||
* Verifies if the build name column should be rendered by verifing
|
||||
* if all the information needed is present
|
||||
|
@ -353,7 +419,7 @@ export default {
|
|||
*/
|
||||
shouldRenderBuildName() {
|
||||
return (
|
||||
!this.model.isFolder &&
|
||||
!this.isFolder &&
|
||||
!_.isEmpty(this.model.last_deployment) &&
|
||||
!_.isEmpty(this.model.last_deployment.deployable)
|
||||
);
|
||||
|
@ -383,11 +449,7 @@ export default {
|
|||
* @return {String}
|
||||
*/
|
||||
externalURL() {
|
||||
if (this.model && this.model.external_url) {
|
||||
return this.model.external_url;
|
||||
}
|
||||
|
||||
return '';
|
||||
return this.model.external_url || '';
|
||||
},
|
||||
|
||||
/**
|
||||
|
@ -399,26 +461,22 @@ export default {
|
|||
*/
|
||||
shouldRenderDeploymentID() {
|
||||
return (
|
||||
!this.model.isFolder &&
|
||||
!this.isFolder &&
|
||||
!_.isEmpty(this.model.last_deployment) &&
|
||||
this.model.last_deployment.iid !== undefined
|
||||
);
|
||||
},
|
||||
|
||||
environmentPath() {
|
||||
if (this.model && this.model.environment_path) {
|
||||
return this.model.environment_path;
|
||||
}
|
||||
|
||||
return '';
|
||||
return this.model.environment_path || '';
|
||||
},
|
||||
|
||||
monitoringUrl() {
|
||||
if (this.model && this.model.metrics_path) {
|
||||
return this.model.metrics_path;
|
||||
}
|
||||
return this.model.metrics_path || '';
|
||||
},
|
||||
|
||||
return '';
|
||||
autoStopUrl() {
|
||||
return this.model.cancel_auto_stop_path || '';
|
||||
},
|
||||
|
||||
displayEnvironmentActions() {
|
||||
|
@ -447,7 +505,7 @@ export default {
|
|||
<div
|
||||
:class="{
|
||||
'js-child-row environment-child-row': model.isChildren,
|
||||
'folder-row': model.isFolder,
|
||||
'folder-row': isFolder,
|
||||
}"
|
||||
class="gl-responsive-table-row"
|
||||
role="row"
|
||||
|
@ -457,7 +515,7 @@ export default {
|
|||
:class="tableData.name.spacing"
|
||||
role="gridcell"
|
||||
>
|
||||
<div v-if="!model.isFolder" class="table-mobile-header" role="rowheader">
|
||||
<div v-if="!isFolder" class="table-mobile-header" role="rowheader">
|
||||
{{ tableData.name.title }}
|
||||
</div>
|
||||
|
||||
|
@ -466,7 +524,7 @@ export default {
|
|||
</span>
|
||||
|
||||
<span
|
||||
v-if="!model.isFolder"
|
||||
v-if="!isFolder"
|
||||
v-gl-tooltip
|
||||
:title="model.name"
|
||||
class="environment-name table-mobile-content"
|
||||
|
@ -506,7 +564,7 @@ export default {
|
|||
{{ deploymentInternalId }}
|
||||
</span>
|
||||
|
||||
<span v-if="!model.isFolder && deploymentHasUser" class="text-break-word">
|
||||
<span v-if="!isFolder && deploymentHasUser" class="text-break-word">
|
||||
by
|
||||
<user-avatar-link
|
||||
:link-href="deploymentUser.web_url"
|
||||
|
@ -516,6 +574,10 @@ export default {
|
|||
class="js-deploy-user-container float-none"
|
||||
/>
|
||||
</span>
|
||||
|
||||
<div v-if="showNoDeployments" class="commit-title table-mobile-content">
|
||||
{{ s__('Environments|No deployments yet') }}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div
|
||||
|
@ -536,14 +598,8 @@ export default {
|
|||
</a>
|
||||
</div>
|
||||
|
||||
<div
|
||||
v-if="!model.isFolder"
|
||||
class="table-section"
|
||||
:class="tableData.commit.spacing"
|
||||
role="gridcell"
|
||||
>
|
||||
<div v-if="!isFolder" class="table-section" :class="tableData.commit.spacing" role="gridcell">
|
||||
<div role="rowheader" class="table-mobile-header">{{ tableData.commit.title }}</div>
|
||||
|
||||
<div v-if="hasLastDeploymentKey" class="js-commit-component table-mobile-content">
|
||||
<commit-component
|
||||
:tag="commitTag"
|
||||
|
@ -554,31 +610,51 @@ export default {
|
|||
:author="commitAuthor"
|
||||
/>
|
||||
</div>
|
||||
<div v-if="!hasLastDeploymentKey" class="commit-title table-mobile-content">
|
||||
{{ s__('Environments|No deployments yet') }}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div
|
||||
v-if="!model.isFolder"
|
||||
class="table-section"
|
||||
:class="tableData.date.spacing"
|
||||
role="gridcell"
|
||||
>
|
||||
<div v-if="!isFolder" class="table-section" :class="tableData.date.spacing" role="gridcell">
|
||||
<div role="rowheader" class="table-mobile-header">{{ tableData.date.title }}</div>
|
||||
|
||||
<span v-if="canShowDate" class="environment-created-date-timeago table-mobile-content">
|
||||
{{ deployedDate }}
|
||||
<span
|
||||
v-if="canShowDeploymentDate"
|
||||
v-gl-tooltip
|
||||
:title="deployedDate.tooltip"
|
||||
class="environment-created-date-timeago table-mobile-content flex-truncate-parent"
|
||||
>
|
||||
<span class="flex-truncate-child">
|
||||
{{ deployedDate.formatted }}
|
||||
</span>
|
||||
</span>
|
||||
</div>
|
||||
|
||||
<div
|
||||
v-if="!model.isFolder && displayEnvironmentActions"
|
||||
v-if="!isFolder && shouldShowAutoStopDate"
|
||||
class="table-section"
|
||||
:class="tableData.autoStop.spacing"
|
||||
role="gridcell"
|
||||
>
|
||||
<div role="rowheader" class="table-mobile-header">{{ tableData.autoStop.title }}</div>
|
||||
<span
|
||||
v-if="canShowAutoStopDate"
|
||||
v-gl-tooltip
|
||||
:title="autoStopDate.tooltip"
|
||||
class="table-mobile-content flex-truncate-parent"
|
||||
>
|
||||
<span class="flex-truncate-child js-auto-stop">{{ autoStopDate.formatted }}</span>
|
||||
</span>
|
||||
</div>
|
||||
|
||||
<div
|
||||
v-if="!isFolder && displayEnvironmentActions"
|
||||
class="table-section table-button-footer"
|
||||
:class="tableData.actions.spacing"
|
||||
role="gridcell"
|
||||
>
|
||||
<div class="btn-group table-action-buttons" role="group">
|
||||
<pin-component
|
||||
v-if="canShowAutoStopDate && shouldShowAutoStopDate"
|
||||
:auto-stop-url="autoStopUrl"
|
||||
/>
|
||||
|
||||
<external-url-component
|
||||
v-if="externalURL && canReadEnvironment"
|
||||
:external-url="externalURL"
|
||||
|
|
|
@ -0,0 +1,37 @@
|
|||
<script>
|
||||
/**
|
||||
* Renders a prevent auto-stop button.
|
||||
* Used in environments table.
|
||||
*/
|
||||
import { GlButton, GlTooltipDirective } from '@gitlab/ui';
|
||||
import Icon from '~/vue_shared/components/icon.vue';
|
||||
import { __ } from '~/locale';
|
||||
import eventHub from '../event_hub';
|
||||
|
||||
export default {
|
||||
components: {
|
||||
Icon,
|
||||
GlButton,
|
||||
},
|
||||
directives: {
|
||||
GlTooltip: GlTooltipDirective,
|
||||
},
|
||||
props: {
|
||||
autoStopUrl: {
|
||||
type: String,
|
||||
required: true,
|
||||
},
|
||||
},
|
||||
methods: {
|
||||
onPinClick() {
|
||||
eventHub.$emit('cancelAutoStop', this.autoStopUrl);
|
||||
},
|
||||
},
|
||||
title: __('Prevent environment from auto-stopping'),
|
||||
};
|
||||
</script>
|
||||
<template>
|
||||
<gl-button v-gl-tooltip :title="$options.title" :aria-label="$options.title" @click="onPinClick">
|
||||
<icon name="thumbtack" />
|
||||
</gl-button>
|
||||
</template>
|
|
@ -6,6 +6,7 @@ import { GlLoadingIcon } from '@gitlab/ui';
|
|||
import _ from 'underscore';
|
||||
import environmentTableMixin from 'ee_else_ce/environments/mixins/environments_table_mixin';
|
||||
import { s__ } from '~/locale';
|
||||
import glFeatureFlagsMixin from '~/vue_shared/mixins/gl_feature_flags_mixin';
|
||||
import EnvironmentItem from './environment_item.vue';
|
||||
|
||||
export default {
|
||||
|
@ -16,7 +17,7 @@ export default {
|
|||
CanaryDeploymentCallout: () =>
|
||||
import('ee_component/environments/components/canary_deployment_callout.vue'),
|
||||
},
|
||||
mixins: [environmentTableMixin],
|
||||
mixins: [environmentTableMixin, glFeatureFlagsMixin()],
|
||||
props: {
|
||||
environments: {
|
||||
type: Array,
|
||||
|
@ -42,6 +43,9 @@ export default {
|
|||
: env,
|
||||
);
|
||||
},
|
||||
shouldShowAutoStopDate() {
|
||||
return this.glFeatures.autoStopEnvironments;
|
||||
},
|
||||
tableData() {
|
||||
return {
|
||||
// percent spacing for cols, should add up to 100
|
||||
|
@ -65,8 +69,12 @@ export default {
|
|||
title: s__('Environments|Updated'),
|
||||
spacing: 'section-10',
|
||||
},
|
||||
autoStop: {
|
||||
title: s__('Environments|Auto stop in'),
|
||||
spacing: 'section-5',
|
||||
},
|
||||
actions: {
|
||||
spacing: 'section-30',
|
||||
spacing: this.shouldShowAutoStopDate ? 'section-25' : 'section-30',
|
||||
},
|
||||
};
|
||||
},
|
||||
|
@ -123,6 +131,14 @@ export default {
|
|||
<div class="table-section" :class="tableData.date.spacing" role="columnheader">
|
||||
{{ tableData.date.title }}
|
||||
</div>
|
||||
<div
|
||||
v-if="shouldShowAutoStopDate"
|
||||
class="table-section"
|
||||
:class="tableData.autoStop.spacing"
|
||||
role="columnheader"
|
||||
>
|
||||
{{ tableData.autoStop.title }}
|
||||
</div>
|
||||
</div>
|
||||
<template v-for="(model, i) in sortedEnvironments" :model="model">
|
||||
<div
|
||||
|
@ -130,6 +146,7 @@ export default {
|
|||
:key="`environment-item-${i}`"
|
||||
:model="model"
|
||||
:can-read-environment="canReadEnvironment"
|
||||
:should-show-auto-stop-date="shouldShowAutoStopDate"
|
||||
:table-data="tableData"
|
||||
/>
|
||||
|
||||
|
|
|
@ -90,16 +90,19 @@ export default {
|
|||
Flash(s__('Environments|An error occurred while fetching the environments.'));
|
||||
},
|
||||
|
||||
postAction({ endpoint, errorMessage }) {
|
||||
postAction({
|
||||
endpoint,
|
||||
errorMessage = s__('Environments|An error occurred while making the request.'),
|
||||
}) {
|
||||
if (!this.isMakingRequest) {
|
||||
this.isLoading = true;
|
||||
|
||||
this.service
|
||||
.postAction(endpoint)
|
||||
.then(() => this.fetchEnvironments())
|
||||
.catch(() => {
|
||||
.catch(err => {
|
||||
this.isLoading = false;
|
||||
Flash(errorMessage || s__('Environments|An error occurred while making the request.'));
|
||||
Flash(_.isFunction(errorMessage) ? errorMessage(err.response.data) : errorMessage);
|
||||
});
|
||||
}
|
||||
},
|
||||
|
@ -138,6 +141,13 @@ export default {
|
|||
);
|
||||
this.postAction({ endpoint: retryUrl, errorMessage });
|
||||
},
|
||||
|
||||
cancelAutoStop(autoStopPath) {
|
||||
const errorMessage = ({ message }) =>
|
||||
message ||
|
||||
s__('Environments|An error occurred while canceling the auto stop, please try again');
|
||||
this.postAction({ endpoint: autoStopPath, errorMessage });
|
||||
},
|
||||
},
|
||||
|
||||
computed: {
|
||||
|
@ -199,6 +209,8 @@ export default {
|
|||
|
||||
eventHub.$on('requestRollbackEnvironment', this.updateRollbackModal);
|
||||
eventHub.$on('rollbackEnvironment', this.rollbackEnvironment);
|
||||
|
||||
eventHub.$on('cancelAutoStop', this.cancelAutoStop);
|
||||
},
|
||||
|
||||
beforeDestroy() {
|
||||
|
@ -208,5 +220,7 @@ export default {
|
|||
|
||||
eventHub.$off('requestRollbackEnvironment', this.updateRollbackModal);
|
||||
eventHub.$off('rollbackEnvironment', this.rollbackEnvironment);
|
||||
|
||||
eventHub.$off('cancelAutoStop', this.cancelAutoStop);
|
||||
},
|
||||
};
|
||||
|
|
|
@ -4,4 +4,13 @@ class Profiles::ActiveSessionsController < Profiles::ApplicationController
|
|||
def index
|
||||
@sessions = ActiveSession.list(current_user).reject(&:is_impersonated)
|
||||
end
|
||||
|
||||
def destroy
|
||||
ActiveSession.destroy_with_public_id(current_user, params[:id])
|
||||
|
||||
respond_to do |format|
|
||||
format.html { redirect_to profile_active_sessions_url, status: :found }
|
||||
format.js { head :ok }
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
|
@ -15,6 +15,9 @@ class Projects::EnvironmentsController < Projects::ApplicationController
|
|||
before_action only: [:metrics, :additional_metrics, :metrics_dashboard] do
|
||||
push_frontend_feature_flag(:prometheus_computed_alerts)
|
||||
end
|
||||
before_action do
|
||||
push_frontend_feature_flag(:auto_stop_environments)
|
||||
end
|
||||
after_action :expire_etag_cache, only: [:cancel_auto_stop]
|
||||
|
||||
def index
|
||||
|
|
|
@ -17,7 +17,7 @@ class PipelinesFinder
|
|||
return Ci::Pipeline.none
|
||||
end
|
||||
|
||||
items = pipelines
|
||||
items = pipelines.no_child
|
||||
items = by_scope(items)
|
||||
items = by_status(items)
|
||||
items = by_ref(items)
|
||||
|
|
|
@ -6,9 +6,11 @@ class ActiveSession
|
|||
SESSION_BATCH_SIZE = 200
|
||||
ALLOWED_NUMBER_OF_ACTIVE_SESSIONS = 100
|
||||
|
||||
attr_writer :session_id
|
||||
|
||||
attr_accessor :created_at, :updated_at,
|
||||
:session_id, :ip_address,
|
||||
:browser, :os, :device_name, :device_type,
|
||||
:ip_address, :browser, :os,
|
||||
:device_name, :device_type,
|
||||
:is_impersonated
|
||||
|
||||
def current?(session)
|
||||
|
@ -21,6 +23,11 @@ class ActiveSession
|
|||
device_type&.titleize
|
||||
end
|
||||
|
||||
def public_id
|
||||
encrypted_id = Gitlab::CryptoHelper.aes256_gcm_encrypt(session_id)
|
||||
CGI.escape(encrypted_id)
|
||||
end
|
||||
|
||||
def self.set(user, request)
|
||||
Gitlab::Redis::SharedState.with do |redis|
|
||||
session_id = request.session.id
|
||||
|
@ -70,6 +77,11 @@ class ActiveSession
|
|||
end
|
||||
end
|
||||
|
||||
def self.destroy_with_public_id(user, public_id)
|
||||
session_id = decrypt_public_id(public_id)
|
||||
destroy(user, session_id) unless session_id.nil?
|
||||
end
|
||||
|
||||
def self.destroy_sessions(redis, user, session_ids)
|
||||
key_names = session_ids.map {|session_id| key_name(user.id, session_id) }
|
||||
session_names = session_ids.map {|session_id| "#{Gitlab::Redis::SharedState::SESSION_NAMESPACE}:#{session_id}" }
|
||||
|
@ -146,9 +158,9 @@ class ActiveSession
|
|||
# remove sessions if there are more than ALLOWED_NUMBER_OF_ACTIVE_SESSIONS.
|
||||
sessions = active_session_entries(session_ids, user.id, redis)
|
||||
sessions.sort_by! {|session| session.updated_at }.reverse!
|
||||
sessions = sessions.drop(ALLOWED_NUMBER_OF_ACTIVE_SESSIONS)
|
||||
sessions = sessions.map { |session| session.session_id }
|
||||
destroy_sessions(redis, user, sessions) if sessions.any?
|
||||
destroyable_sessions = sessions.drop(ALLOWED_NUMBER_OF_ACTIVE_SESSIONS)
|
||||
destroyable_session_ids = destroyable_sessions.map { |session| session.send :session_id } # rubocop:disable GitlabSecurity/PublicSend
|
||||
destroy_sessions(redis, user, destroyable_session_ids) if destroyable_session_ids.any?
|
||||
end
|
||||
|
||||
def self.cleaned_up_lookup_entries(redis, user)
|
||||
|
@ -167,4 +179,15 @@ class ActiveSession
|
|||
|
||||
entries.compact
|
||||
end
|
||||
|
||||
private_class_method def self.decrypt_public_id(public_id)
|
||||
decoded_id = CGI.unescape(public_id)
|
||||
Gitlab::CryptoHelper.aes256_gcm_decrypt(decoded_id)
|
||||
rescue
|
||||
nil
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
attr_reader :session_id
|
||||
end
|
||||
|
|
|
@ -54,6 +54,10 @@ module Ci
|
|||
def to_partial_path
|
||||
'projects/generic_commit_statuses/generic_commit_status'
|
||||
end
|
||||
|
||||
def yaml_for_downstream
|
||||
nil
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
|
|
|
@ -61,7 +61,9 @@ module Ci
|
|||
has_one :chat_data, class_name: 'Ci::PipelineChatData'
|
||||
|
||||
has_many :triggered_pipelines, through: :sourced_pipelines, source: :pipeline
|
||||
has_many :child_pipelines, -> { merge(Ci::Sources::Pipeline.same_project) }, through: :sourced_pipelines, source: :pipeline
|
||||
has_one :triggered_by_pipeline, through: :source_pipeline, source: :source_pipeline
|
||||
has_one :parent_pipeline, -> { merge(Ci::Sources::Pipeline.same_project) }, through: :source_pipeline, source: :source_pipeline
|
||||
has_one :source_job, through: :source_pipeline, source: :source_job
|
||||
|
||||
has_one :pipeline_config, class_name: 'Ci::PipelineConfig', inverse_of: :pipeline
|
||||
|
@ -213,6 +215,7 @@ module Ci
|
|||
end
|
||||
|
||||
scope :internal, -> { where(source: internal_sources) }
|
||||
scope :no_child, -> { where.not(source: :parent_pipeline) }
|
||||
scope :ci_sources, -> { where(config_source: ::Ci::PipelineEnums.ci_config_sources_values) }
|
||||
scope :for_user, -> (user) { where(user: user) }
|
||||
scope :for_sha, -> (sha) { where(sha: sha) }
|
||||
|
@ -508,10 +511,6 @@ module Ci
|
|||
builds.skipped.after_stage(stage_idx).find_each(&:process)
|
||||
end
|
||||
|
||||
def child?
|
||||
false
|
||||
end
|
||||
|
||||
def latest?
|
||||
return false unless git_ref && commit.present?
|
||||
|
||||
|
@ -694,6 +693,24 @@ module Ci
|
|||
all_merge_requests.order(id: :desc)
|
||||
end
|
||||
|
||||
# If pipeline is a child of another pipeline, include the parent
|
||||
# and the siblings, otherwise return only itself.
|
||||
def same_family_pipeline_ids
|
||||
if (parent = parent_pipeline)
|
||||
[parent.id] + parent.child_pipelines.pluck(:id)
|
||||
else
|
||||
[self.id]
|
||||
end
|
||||
end
|
||||
|
||||
def child?
|
||||
parent_pipeline.present?
|
||||
end
|
||||
|
||||
def parent?
|
||||
child_pipelines.exists?
|
||||
end
|
||||
|
||||
def detailed_status(current_user)
|
||||
Gitlab::Ci::Status::Pipeline::Factory
|
||||
.new(self, current_user)
|
||||
|
|
|
@ -23,10 +23,11 @@ module Ci
|
|||
schedule: 4,
|
||||
api: 5,
|
||||
external: 6,
|
||||
pipeline: 7,
|
||||
cross_project_pipeline: 7,
|
||||
chat: 8,
|
||||
merge_request_event: 10,
|
||||
external_pull_request_event: 11
|
||||
external_pull_request_event: 11,
|
||||
parent_pipeline: 12
|
||||
}
|
||||
end
|
||||
|
||||
|
@ -38,7 +39,8 @@ module Ci
|
|||
repository_source: 1,
|
||||
auto_devops_source: 2,
|
||||
remote_source: 4,
|
||||
external_project_source: 5
|
||||
external_project_source: 5,
|
||||
bridge_source: 6
|
||||
}
|
||||
end
|
||||
|
||||
|
|
|
@ -18,6 +18,8 @@ module Ci
|
|||
validates :source_project, presence: true
|
||||
validates :source_job, presence: true
|
||||
validates :source_pipeline, presence: true
|
||||
|
||||
scope :same_project, -> { where(arel_table[:source_project_id].eq(arel_table[:project_id])) }
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
|
@ -13,6 +13,7 @@ module Issuable
|
|||
include CacheMarkdownField
|
||||
include Participable
|
||||
include Mentionable
|
||||
include Milestoneable
|
||||
include Subscribable
|
||||
include StripAttribute
|
||||
include Awardable
|
||||
|
@ -56,7 +57,6 @@ module Issuable
|
|||
belongs_to :author, class_name: 'User'
|
||||
belongs_to :updated_by, class_name: 'User'
|
||||
belongs_to :last_edited_by, class_name: 'User'
|
||||
belongs_to :milestone
|
||||
|
||||
has_many :notes, as: :noteable, inverse_of: :noteable, dependent: :destroy do # rubocop:disable Cop/ActiveRecordDependent
|
||||
def authors_loaded?
|
||||
|
@ -89,18 +89,12 @@ module Issuable
|
|||
# to avoid breaking the existing Issuables which may have their descriptions longer
|
||||
validates :description, length: { maximum: DESCRIPTION_LENGTH_MAX }, allow_blank: true, on: :create
|
||||
validate :description_max_length_for_new_records_is_valid, on: :update
|
||||
validate :milestone_is_valid
|
||||
|
||||
before_validation :truncate_description_on_import!
|
||||
|
||||
scope :authored, ->(user) { where(author_id: user) }
|
||||
scope :recent, -> { reorder(id: :desc) }
|
||||
scope :of_projects, ->(ids) { where(project_id: ids) }
|
||||
scope :of_milestones, ->(ids) { where(milestone_id: ids) }
|
||||
scope :any_milestone, -> { where('milestone_id IS NOT NULL') }
|
||||
scope :with_milestone, ->(title) { left_joins_milestones.where(milestones: { title: title }) }
|
||||
scope :any_release, -> { joins_milestone_releases }
|
||||
scope :with_release, -> (tag, project_id) { joins_milestone_releases.where( milestones: { releases: { tag: tag, project_id: project_id } } ) }
|
||||
scope :opened, -> { with_state(:opened) }
|
||||
scope :only_opened, -> { with_state(:opened) }
|
||||
scope :closed, -> { with_state(:closed) }
|
||||
|
@ -118,20 +112,6 @@ module Issuable
|
|||
end
|
||||
# rubocop:enable GitlabSecurity/SqlInjection
|
||||
|
||||
scope :left_joins_milestones, -> { joins("LEFT OUTER JOIN milestones ON #{table_name}.milestone_id = milestones.id") }
|
||||
scope :order_milestone_due_desc, -> { left_joins_milestones.reorder(Arel.sql('milestones.due_date IS NULL, milestones.id IS NULL, milestones.due_date DESC')) }
|
||||
scope :order_milestone_due_asc, -> { left_joins_milestones.reorder(Arel.sql('milestones.due_date IS NULL, milestones.id IS NULL, milestones.due_date ASC')) }
|
||||
|
||||
scope :without_release, -> do
|
||||
joins("LEFT OUTER JOIN milestone_releases ON #{table_name}.milestone_id = milestone_releases.milestone_id")
|
||||
.where('milestone_releases.release_id IS NULL')
|
||||
end
|
||||
|
||||
scope :joins_milestone_releases, -> do
|
||||
joins("JOIN milestone_releases ON #{table_name}.milestone_id = milestone_releases.milestone_id
|
||||
JOIN releases ON milestone_releases.release_id = releases.id").distinct
|
||||
end
|
||||
|
||||
scope :without_label, -> { joins("LEFT OUTER JOIN label_links ON label_links.target_type = '#{name}' AND label_links.target_id = #{table_name}.id").where(label_links: { id: nil }) }
|
||||
scope :any_label, -> { joins(:label_links).group(:id) }
|
||||
scope :join_project, -> { joins(:project) }
|
||||
|
@ -164,10 +144,6 @@ module Issuable
|
|||
|
||||
private
|
||||
|
||||
def milestone_is_valid
|
||||
errors.add(:milestone_id, message: "is invalid") if respond_to?(:milestone_id) && milestone_id.present? && !milestone_available?
|
||||
end
|
||||
|
||||
def description_max_length_for_new_records_is_valid
|
||||
if new_record? && description.length > Issuable::DESCRIPTION_LENGTH_MAX
|
||||
errors.add(:description, :too_long, count: Issuable::DESCRIPTION_LENGTH_MAX)
|
||||
|
@ -332,10 +308,6 @@ module Issuable
|
|||
project
|
||||
end
|
||||
|
||||
def milestone_available?
|
||||
project_id == milestone&.project_id || project.ancestors_upto.compact.include?(milestone&.group)
|
||||
end
|
||||
|
||||
def assignee_or_author?(user)
|
||||
author_id == user.id || assignees.exists?(user.id)
|
||||
end
|
||||
|
@ -482,13 +454,6 @@ module Issuable
|
|||
def wipless_title_changed(old_title)
|
||||
old_title != title
|
||||
end
|
||||
|
||||
##
|
||||
# Overridden on EE module
|
||||
#
|
||||
def supports_milestone?
|
||||
respond_to?(:milestone_id)
|
||||
end
|
||||
end
|
||||
|
||||
Issuable.prepend_if_ee('EE::Issuable') # rubocop: disable Cop/InjectEnterpriseEditionModule
|
||||
|
|
|
@ -0,0 +1,62 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
# == Milestoneable concern
|
||||
#
|
||||
# Contains functionality related to objects that can be assigned Milestones
|
||||
#
|
||||
# Used by Issuable
|
||||
#
|
||||
module Milestoneable
|
||||
extend ActiveSupport::Concern
|
||||
|
||||
included do
|
||||
belongs_to :milestone
|
||||
|
||||
validate :milestone_is_valid
|
||||
|
||||
after_save :write_to_new_milestone_relationship
|
||||
|
||||
scope :of_milestones, ->(ids) { where(milestone_id: ids) }
|
||||
scope :any_milestone, -> { where('milestone_id IS NOT NULL') }
|
||||
scope :with_milestone, ->(title) { left_joins_milestones.where(milestones: { title: title }) }
|
||||
scope :any_release, -> { joins_milestone_releases }
|
||||
scope :with_release, -> (tag, project_id) { joins_milestone_releases.where( milestones: { releases: { tag: tag, project_id: project_id } } ) }
|
||||
|
||||
scope :left_joins_milestones, -> { joins("LEFT OUTER JOIN milestones ON #{table_name}.milestone_id = milestones.id") }
|
||||
scope :order_milestone_due_desc, -> { left_joins_milestones.reorder(Arel.sql('milestones.due_date IS NULL, milestones.id IS NULL, milestones.due_date DESC')) }
|
||||
scope :order_milestone_due_asc, -> { left_joins_milestones.reorder(Arel.sql('milestones.due_date IS NULL, milestones.id IS NULL, milestones.due_date ASC')) }
|
||||
|
||||
scope :without_release, -> do
|
||||
joins("LEFT OUTER JOIN milestone_releases ON #{table_name}.milestone_id = milestone_releases.milestone_id")
|
||||
.where('milestone_releases.release_id IS NULL')
|
||||
end
|
||||
|
||||
scope :joins_milestone_releases, -> do
|
||||
joins("JOIN milestone_releases ON #{table_name}.milestone_id = milestone_releases.milestone_id
|
||||
JOIN releases ON milestone_releases.release_id = releases.id").distinct
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def milestone_is_valid
|
||||
errors.add(:milestone_id, message: "is invalid") if respond_to?(:milestone_id) && milestone_id.present? && !milestone_available?
|
||||
end
|
||||
|
||||
def write_to_new_milestone_relationship
|
||||
self.milestones = [milestone].compact if supports_milestone? && saved_change_to_milestone_id?
|
||||
end
|
||||
end
|
||||
|
||||
def milestone_available?
|
||||
project_id == milestone&.project_id || project.ancestors_upto.compact.include?(milestone&.group)
|
||||
end
|
||||
|
||||
##
|
||||
# Overridden on EE module
|
||||
#
|
||||
def supports_milestone?
|
||||
respond_to?(:milestone_id)
|
||||
end
|
||||
end
|
||||
|
||||
Milestoneable.prepend_if_ee('EE::Milestoneable')
|
|
@ -33,6 +33,9 @@ class Issue < ApplicationRecord
|
|||
|
||||
has_internal_id :iid, scope: :project, init: ->(s) { s&.project&.issues&.maximum(:iid) }
|
||||
|
||||
has_many :issue_milestones
|
||||
has_many :milestones, through: :issue_milestones
|
||||
|
||||
has_many :events, as: :target, dependent: :delete_all # rubocop:disable Cop/ActiveRecordDependent
|
||||
|
||||
has_many :merge_requests_closing_issues,
|
||||
|
|
|
@ -0,0 +1,6 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
class IssueMilestone < ApplicationRecord
|
||||
belongs_to :milestone
|
||||
belongs_to :issue
|
||||
end
|
|
@ -35,6 +35,9 @@ class MergeRequest < ApplicationRecord
|
|||
|
||||
has_many :merge_request_diffs
|
||||
|
||||
has_many :merge_request_milestones
|
||||
has_many :milestones, through: :merge_request_milestones
|
||||
|
||||
has_one :merge_request_diff,
|
||||
-> { order('merge_request_diffs.id DESC') }, inverse_of: :merge_request
|
||||
|
||||
|
|
|
@ -0,0 +1,6 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
class MergeRequestMilestone < ApplicationRecord
|
||||
belongs_to :milestone
|
||||
belongs_to :merge_request
|
||||
end
|
|
@ -38,6 +38,9 @@ class Milestone < ApplicationRecord
|
|||
has_many :merge_requests
|
||||
has_many :events, as: :target, dependent: :delete_all # rubocop:disable Cop/ActiveRecordDependent
|
||||
|
||||
has_many :issue_milestones
|
||||
has_many :merge_request_milestones
|
||||
|
||||
scope :of_projects, ->(ids) { where(project_id: ids) }
|
||||
scope :of_groups, ->(ids) { where(group_id: ids) }
|
||||
scope :active, -> { with_state(:active) }
|
||||
|
|
|
@ -1,6 +1,8 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
class PipelineDetailsEntity < PipelineEntity
|
||||
expose :project, using: ProjectEntity
|
||||
|
||||
expose :flags do
|
||||
expose :latest?, as: :latest
|
||||
end
|
||||
|
|
|
@ -41,6 +41,7 @@ class PipelineSerializer < BaseSerializer
|
|||
def preloaded_relations
|
||||
[
|
||||
:latest_statuses_ordered_by_stage,
|
||||
:project,
|
||||
:stages,
|
||||
{
|
||||
failed_builds: %i(project metadata)
|
||||
|
|
|
@ -23,7 +23,7 @@ module Ci
|
|||
Gitlab::Ci::Pipeline::Chain::Limit::JobActivity].freeze
|
||||
|
||||
# rubocop: disable Metrics/ParameterLists
|
||||
def execute(source, ignore_skip_ci: false, save_on_errors: true, trigger_request: nil, schedule: nil, merge_request: nil, external_pull_request: nil, **options, &block)
|
||||
def execute(source, ignore_skip_ci: false, save_on_errors: true, trigger_request: nil, schedule: nil, merge_request: nil, external_pull_request: nil, bridge: nil, **options, &block)
|
||||
@pipeline = Ci::Pipeline.new
|
||||
|
||||
command = Gitlab::Ci::Pipeline::Chain::Command.new(
|
||||
|
@ -46,6 +46,7 @@ module Ci
|
|||
current_user: current_user,
|
||||
push_options: params[:push_options] || {},
|
||||
chat_data: params[:chat_data],
|
||||
bridge: bridge,
|
||||
**extra_options(options))
|
||||
|
||||
sequence = Gitlab::Ci::Pipeline::Chain::Sequence
|
||||
|
@ -104,14 +105,14 @@ module Ci
|
|||
if Feature.enabled?(:ci_support_interruptible_pipelines, project, default_enabled: true)
|
||||
project.ci_pipelines
|
||||
.where(ref: pipeline.ref)
|
||||
.where.not(id: pipeline.id)
|
||||
.where.not(id: pipeline.same_family_pipeline_ids)
|
||||
.where.not(sha: project.commit(pipeline.ref).try(:id))
|
||||
.alive_or_scheduled
|
||||
.with_only_interruptible_builds
|
||||
else
|
||||
project.ci_pipelines
|
||||
.where(ref: pipeline.ref)
|
||||
.where.not(id: pipeline.id)
|
||||
.where.not(id: pipeline.same_family_pipeline_ids)
|
||||
.where.not(sha: project.commit(pipeline.ref).try(:id))
|
||||
.created_or_pending
|
||||
end
|
||||
|
|
|
@ -44,7 +44,7 @@ module Ci
|
|||
return error("400 Job has to be running", 400) unless job.running?
|
||||
|
||||
pipeline = Ci::CreatePipelineService.new(project, job.user, ref: params[:ref])
|
||||
.execute(:pipeline, ignore_skip_ci: true) do |pipeline|
|
||||
.execute(:cross_project_pipeline, ignore_skip_ci: true) do |pipeline|
|
||||
source = job.sourced_pipelines.build(
|
||||
source_pipeline: job.pipeline,
|
||||
source_project: job.project,
|
||||
|
|
|
@ -24,3 +24,9 @@
|
|||
%strong= _('Signed in')
|
||||
= s_('ProfileSession|on')
|
||||
= l(active_session.created_at, format: :short)
|
||||
|
||||
- unless is_current_session
|
||||
.float-right
|
||||
= link_to profile_active_session_path(active_session.public_id), data: { confirm: _('Are you sure? The device will be signed out of GitLab.') }, method: :delete, class: "btn btn-danger prepend-left-10" do
|
||||
%span.sr-only= _('Revoke')
|
||||
= _('Revoke')
|
||||
|
|
|
@ -0,0 +1,3 @@
|
|||
- if environment.auto_stop_at? && environment.available?
|
||||
= button_to cancel_auto_stop_project_environment_path(environment.project, environment), class: 'btn btn-secondary has-tooltip', title: _('Prevent environment from auto-stopping') do
|
||||
= sprite_icon('thumbtack')
|
|
@ -32,9 +32,14 @@
|
|||
= button_to stop_project_environment_path(@project, @environment), class: 'btn btn-danger has-tooltip', method: :post do
|
||||
= s_('Environments|Stop environment')
|
||||
|
||||
.top-area
|
||||
%h3.page-title= @environment.name
|
||||
.nav-controls.ml-auto.my-2
|
||||
.top-area.justify-content-between
|
||||
.d-flex
|
||||
%h3.page-title= @environment.name
|
||||
- if @environment.auto_stop_at?
|
||||
%p.align-self-end.prepend-left-8
|
||||
= s_('Environments|Auto stops %{auto_stop_time}').html_safe % {auto_stop_time: time_ago_with_tooltip(@environment.auto_stop_at)}
|
||||
.nav-controls.my-2
|
||||
= render 'projects/environments/pin_button', environment: @environment
|
||||
= render 'projects/environments/terminal_button', environment: @environment
|
||||
= render 'projects/environments/external_url', environment: @environment
|
||||
= render 'projects/environments/metrics_button', environment: @environment
|
||||
|
|
|
@ -4,6 +4,9 @@
|
|||
%h4.sub-header
|
||||
= _("Programming languages used in this repository")
|
||||
|
||||
%p
|
||||
= _("Measured in bytes of code. Excludes generated and vendored code.")
|
||||
|
||||
.row
|
||||
.col-md-4
|
||||
%ul.bordered-list
|
||||
|
|
|
@ -0,0 +1,101 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
#
|
||||
# A concern that helps run exactly one instance of a worker, over and over,
|
||||
# until it returns false or raises.
|
||||
#
|
||||
# To ensure the worker is always up, you can schedule it every minute with
|
||||
# sidekiq-cron. Excess jobs will immediately exit due to an exclusive lease.
|
||||
#
|
||||
# The worker must define:
|
||||
#
|
||||
# - `#perform`
|
||||
# - `#lease_timeout`
|
||||
#
|
||||
# The worker spec should include `it_behaves_like 'reenqueuer'` and
|
||||
# `it_behaves_like 'it is rate limited to 1 call per'`.
|
||||
#
|
||||
# Optionally override `#minimum_duration` to adjust the rate limit.
|
||||
#
|
||||
# When `#perform` returns false, the job will not be reenqueued. Instead, we
|
||||
# will wait for the next one scheduled by sidekiq-cron.
|
||||
#
|
||||
# #lease_timeout should be longer than the longest possible `#perform`.
|
||||
# The lease is normally released in an ensure block, but it is possible to
|
||||
# orphan the lease by killing Sidekiq, so it should also be as short as
|
||||
# possible. Consider that long-running jobs are generally not recommended.
|
||||
# Ideally, every job finishes within 25 seconds because that is the default
|
||||
# wait time for graceful termination.
|
||||
#
|
||||
# Timing: It runs as often as Sidekiq allows. We rate limit with sleep for
|
||||
# now: https://gitlab.com/gitlab-org/gitlab/issues/121697
|
||||
module Reenqueuer
|
||||
extend ActiveSupport::Concern
|
||||
|
||||
prepended do
|
||||
include ExclusiveLeaseGuard
|
||||
include ReenqueuerSleeper
|
||||
|
||||
sidekiq_options retry: false
|
||||
end
|
||||
|
||||
def perform(*args)
|
||||
try_obtain_lease do
|
||||
reenqueue(*args) do
|
||||
ensure_minimum_duration(minimum_duration) do
|
||||
super
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def reenqueue(*args)
|
||||
self.class.perform_async(*args) if yield
|
||||
end
|
||||
|
||||
# Override as needed
|
||||
def minimum_duration
|
||||
5.seconds
|
||||
end
|
||||
|
||||
# We intend to get rid of sleep:
|
||||
# https://gitlab.com/gitlab-org/gitlab/issues/121697
|
||||
module ReenqueuerSleeper
|
||||
# The block will run, and then sleep until the minimum duration. Returns the
|
||||
# block's return value.
|
||||
#
|
||||
# Usage:
|
||||
#
|
||||
# ensure_minimum_duration(5.seconds) do
|
||||
# # do something
|
||||
# end
|
||||
#
|
||||
def ensure_minimum_duration(minimum_duration)
|
||||
start_time = Time.now
|
||||
|
||||
result = yield
|
||||
|
||||
sleep_if_time_left(minimum_duration, start_time)
|
||||
|
||||
result
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def sleep_if_time_left(minimum_duration, start_time)
|
||||
time_left = calculate_time_left(minimum_duration, start_time)
|
||||
|
||||
sleep(time_left) if time_left > 0
|
||||
end
|
||||
|
||||
def calculate_time_left(minimum_duration, start_time)
|
||||
minimum_duration - elapsed_time(start_time)
|
||||
end
|
||||
|
||||
def elapsed_time(start_time)
|
||||
Time.now - start_time
|
||||
end
|
||||
end
|
||||
end
|
|
@ -0,0 +1,5 @@
|
|||
---
|
||||
title: Auto stop environments after a certain period
|
||||
merge_request: 20372
|
||||
author:
|
||||
type: added
|
|
@ -0,0 +1,6 @@
|
|||
---
|
||||
title: Restores user's ability to revoke sessions from the active sessions
|
||||
page.
|
||||
merge_request: 17462
|
||||
author: Jesse Hall @jessehall3
|
||||
type: changed
|
|
@ -0,0 +1,5 @@
|
|||
---
|
||||
title: Setup storage for multiple milestones
|
||||
merge_request: 22043
|
||||
author:
|
||||
type: added
|
|
@ -0,0 +1,5 @@
|
|||
---
|
||||
title: Allow an upstream pipeline to create a downstream pipeline in the same project
|
||||
merge_request: 20930
|
||||
author:
|
||||
type: added
|
|
@ -0,0 +1,5 @@
|
|||
---
|
||||
title: Add measurement details for programming languages graph
|
||||
merge_request: 20592
|
||||
author:
|
||||
type: changed
|
|
@ -0,0 +1,5 @@
|
|||
---
|
||||
title: 25968-activity-filter-to-notes-api
|
||||
merge_request: 21159
|
||||
author: jhenkens
|
||||
type: added
|
|
@ -0,0 +1,14 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
class SupportMultipleMilestonesForIssues < ActiveRecord::Migration[5.2]
|
||||
DOWNTIME = false
|
||||
|
||||
def change
|
||||
create_table :issue_milestones, id: false do |t|
|
||||
t.references :issue, foreign_key: { on_delete: :cascade }, index: { unique: true }, null: false
|
||||
t.references :milestone, foreign_key: { on_delete: :cascade }, index: true, null: false
|
||||
end
|
||||
|
||||
add_index :issue_milestones, [:issue_id, :milestone_id], unique: true
|
||||
end
|
||||
end
|
|
@ -0,0 +1,14 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
class SupportMultipleMilestonesForMergeRequests < ActiveRecord::Migration[5.2]
|
||||
DOWNTIME = false
|
||||
|
||||
def change
|
||||
create_table :merge_request_milestones, id: false do |t|
|
||||
t.references :merge_request, foreign_key: { on_delete: :cascade }, index: { unique: true }, null: false
|
||||
t.references :milestone, foreign_key: { on_delete: :cascade }, index: true, null: false
|
||||
end
|
||||
|
||||
add_index :merge_request_milestones, [:merge_request_id, :milestone_id], name: 'index_mrs_milestones_on_mr_id_and_milestone_id', unique: true
|
||||
end
|
||||
end
|
20
db/schema.rb
20
db/schema.rb
|
@ -2099,6 +2099,14 @@ ActiveRecord::Schema.define(version: 2019_12_29_140154) do
|
|||
t.index ["issue_id"], name: "index_issue_metrics"
|
||||
end
|
||||
|
||||
create_table "issue_milestones", id: false, force: :cascade do |t|
|
||||
t.bigint "issue_id", null: false
|
||||
t.bigint "milestone_id", null: false
|
||||
t.index ["issue_id", "milestone_id"], name: "index_issue_milestones_on_issue_id_and_milestone_id", unique: true
|
||||
t.index ["issue_id"], name: "index_issue_milestones_on_issue_id", unique: true
|
||||
t.index ["milestone_id"], name: "index_issue_milestones_on_milestone_id"
|
||||
end
|
||||
|
||||
create_table "issue_tracker_data", force: :cascade do |t|
|
||||
t.integer "service_id", null: false
|
||||
t.datetime_with_timezone "created_at", null: false
|
||||
|
@ -2486,6 +2494,14 @@ ActiveRecord::Schema.define(version: 2019_12_29_140154) do
|
|||
t.index ["pipeline_id"], name: "index_merge_request_metrics_on_pipeline_id"
|
||||
end
|
||||
|
||||
create_table "merge_request_milestones", id: false, force: :cascade do |t|
|
||||
t.bigint "merge_request_id", null: false
|
||||
t.bigint "milestone_id", null: false
|
||||
t.index ["merge_request_id", "milestone_id"], name: "index_mrs_milestones_on_mr_id_and_milestone_id", unique: true
|
||||
t.index ["merge_request_id"], name: "index_merge_request_milestones_on_merge_request_id", unique: true
|
||||
t.index ["milestone_id"], name: "index_merge_request_milestones_on_milestone_id"
|
||||
end
|
||||
|
||||
create_table "merge_request_user_mentions", force: :cascade do |t|
|
||||
t.integer "merge_request_id", null: false
|
||||
t.integer "note_id"
|
||||
|
@ -4595,6 +4611,8 @@ ActiveRecord::Schema.define(version: 2019_12_29_140154) do
|
|||
add_foreign_key "issue_links", "issues", column: "source_id", name: "fk_c900194ff2", on_delete: :cascade
|
||||
add_foreign_key "issue_links", "issues", column: "target_id", name: "fk_e71bb44f1f", on_delete: :cascade
|
||||
add_foreign_key "issue_metrics", "issues", on_delete: :cascade
|
||||
add_foreign_key "issue_milestones", "issues", on_delete: :cascade
|
||||
add_foreign_key "issue_milestones", "milestones", on_delete: :cascade
|
||||
add_foreign_key "issue_tracker_data", "services", on_delete: :cascade
|
||||
add_foreign_key "issue_user_mentions", "issues", on_delete: :cascade
|
||||
add_foreign_key "issue_user_mentions", "notes", on_delete: :cascade
|
||||
|
@ -4638,6 +4656,8 @@ ActiveRecord::Schema.define(version: 2019_12_29_140154) do
|
|||
add_foreign_key "merge_request_metrics", "merge_requests", on_delete: :cascade
|
||||
add_foreign_key "merge_request_metrics", "users", column: "latest_closed_by_id", name: "fk_ae440388cc", on_delete: :nullify
|
||||
add_foreign_key "merge_request_metrics", "users", column: "merged_by_id", name: "fk_7f28d925f3", on_delete: :nullify
|
||||
add_foreign_key "merge_request_milestones", "merge_requests", on_delete: :cascade
|
||||
add_foreign_key "merge_request_milestones", "milestones", on_delete: :cascade
|
||||
add_foreign_key "merge_request_user_mentions", "merge_requests", on_delete: :cascade
|
||||
add_foreign_key "merge_request_user_mentions", "notes", on_delete: :cascade
|
||||
add_foreign_key "merge_requests", "ci_pipelines", column: "head_pipeline_id", name: "fk_fd82eae0b9", on_delete: :nullify
|
||||
|
|
|
@ -2665,7 +2665,7 @@ the currently running/pending `deploy-to-production` job is finished. As a resul
|
|||
you can ensure that concurrent deployments will never happen to the production environment.
|
||||
|
||||
There can be multiple `resource_group`s defined per environment. A good use case for this
|
||||
is when deploying to physical devices. You may have more than one phyisical device, and each
|
||||
is when deploying to physical devices. You may have more than one physical device, and each
|
||||
one can be deployed to, but there can be only one deployment per device at any given time.
|
||||
|
||||
### `include`
|
||||
|
|
|
@ -24,6 +24,11 @@ review the sessions, and revoke any you don't recognize.
|
|||
GitLab allows users to have up to 100 active sessions at once. If the number of active sessions
|
||||
exceeds 100, the oldest ones are deleted.
|
||||
|
||||
## Revoking a session
|
||||
|
||||
1. Use the previous steps to navigate to **Active Sessions**.
|
||||
1. Click on **Revoke** besides a session. The current session cannot be revoked, as this would sign you out of GitLab.
|
||||
|
||||
<!-- ## Troubleshooting
|
||||
|
||||
Include any troubleshooting steps that you can foresee. If you know beforehand what issues
|
||||
|
|
Binary file not shown.
Before Width: | Height: | Size: 18 KiB After Width: | Height: | Size: 22 KiB |
|
@ -24,6 +24,8 @@ module API
|
|||
desc: 'Return notes ordered by `created_at` or `updated_at` fields.'
|
||||
optional :sort, type: String, values: %w[asc desc], default: 'desc',
|
||||
desc: 'Return notes sorted in `asc` or `desc` order.'
|
||||
optional :activity_filter, type: String, values: UserPreference::NOTES_FILTERS.stringify_keys.keys, default: 'all_notes',
|
||||
desc: 'The type of notables which are returned.'
|
||||
use :pagination
|
||||
end
|
||||
# rubocop: disable CodeReuse/ActiveRecord
|
||||
|
@ -35,7 +37,8 @@ module API
|
|||
# at the DB query level (which we cannot in that case), the current
|
||||
# page can have less elements than :per_page even if
|
||||
# there's more than one page.
|
||||
raw_notes = noteable.notes.with_metadata.reorder(order_options_with_tie_breaker)
|
||||
notes_filter = UserPreference::NOTES_FILTERS[params[:activity_filter].to_sym]
|
||||
raw_notes = noteable.notes.with_metadata.with_notes_filter(notes_filter).reorder(order_options_with_tie_breaker)
|
||||
|
||||
# paginate() only works with a relation. This could lead to a
|
||||
# mismatch between the pagination headers info and the actual notes
|
||||
|
|
|
@ -10,7 +10,7 @@ module Gitlab
|
|||
:trigger_request, :schedule, :merge_request, :external_pull_request,
|
||||
:ignore_skip_ci, :save_incompleted,
|
||||
:seeds_block, :variables_attributes, :push_options,
|
||||
:chat_data, :allow_mirror_update,
|
||||
:chat_data, :allow_mirror_update, :bridge,
|
||||
# These attributes are set by Chains during processing:
|
||||
:config_content, :config_processor, :stage_seeds
|
||||
) do
|
||||
|
|
|
@ -9,7 +9,7 @@ module Gitlab
|
|||
include Chain::Helpers
|
||||
|
||||
SOURCES = [
|
||||
Gitlab::Ci::Pipeline::Chain::Config::Content::Runtime,
|
||||
Gitlab::Ci::Pipeline::Chain::Config::Content::Bridge,
|
||||
Gitlab::Ci::Pipeline::Chain::Config::Content::Repository,
|
||||
Gitlab::Ci::Pipeline::Chain::Config::Content::ExternalProject,
|
||||
Gitlab::Ci::Pipeline::Chain::Config::Content::Remote,
|
||||
|
@ -17,7 +17,7 @@ module Gitlab
|
|||
].freeze
|
||||
|
||||
LEGACY_SOURCES = [
|
||||
Gitlab::Ci::Pipeline::Chain::Config::Content::Runtime,
|
||||
Gitlab::Ci::Pipeline::Chain::Config::Content::Bridge,
|
||||
Gitlab::Ci::Pipeline::Chain::Config::Content::LegacyRepository,
|
||||
Gitlab::Ci::Pipeline::Chain::Config::Content::LegacyAutoDevops
|
||||
].freeze
|
||||
|
|
|
@ -0,0 +1,25 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
module Gitlab
|
||||
module Ci
|
||||
module Pipeline
|
||||
module Chain
|
||||
module Config
|
||||
class Content
|
||||
class Bridge < Source
|
||||
def content
|
||||
return unless @command.bridge
|
||||
|
||||
@command.bridge.yaml_for_downstream
|
||||
end
|
||||
|
||||
def source
|
||||
:bridge_source
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
|
@ -1,30 +0,0 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
module Gitlab
|
||||
module Ci
|
||||
module Pipeline
|
||||
module Chain
|
||||
module Config
|
||||
class Content
|
||||
class Runtime < Source
|
||||
def content
|
||||
@command.config_content
|
||||
end
|
||||
|
||||
def source
|
||||
# The only case when this source is used is when the config content
|
||||
# is passed in as parameter to Ci::CreatePipelineService.
|
||||
# This would only occur with parent/child pipelines which is being
|
||||
# implemented.
|
||||
# TODO: change source to return :runtime_source
|
||||
# https://gitlab.com/gitlab-org/gitlab/merge_requests/21041
|
||||
|
||||
nil
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
|
@ -24,6 +24,8 @@ tree:
|
|||
- milestone:
|
||||
- events:
|
||||
- :push_event_payload
|
||||
- issue_milestones:
|
||||
- :milestone
|
||||
- resource_label_events:
|
||||
- label:
|
||||
- :priorities
|
||||
|
@ -57,6 +59,8 @@ tree:
|
|||
- milestone:
|
||||
- events:
|
||||
- :push_event_payload
|
||||
- merge_request_milestones:
|
||||
- :milestone
|
||||
- resource_label_events:
|
||||
- label:
|
||||
- :priorities
|
||||
|
@ -202,6 +206,12 @@ excluded_attributes:
|
|||
- :latest_merge_request_diff_id
|
||||
- :head_pipeline_id
|
||||
- :state_id
|
||||
issue_milestones:
|
||||
- :milestone_id
|
||||
- :issue_id
|
||||
merge_request_milestones:
|
||||
- :milestone_id
|
||||
- :merge_request_id
|
||||
award_emoji:
|
||||
- :awardable_id
|
||||
statuses:
|
||||
|
|
|
@ -2152,6 +2152,9 @@ msgstr ""
|
|||
msgid "Are you sure? Removing this GPG key does not affect already signed commits."
|
||||
msgstr ""
|
||||
|
||||
msgid "Are you sure? The device will be signed out of GitLab."
|
||||
msgstr ""
|
||||
|
||||
msgid "Are you sure? This will invalidate your registered applications and U2F devices."
|
||||
msgstr ""
|
||||
|
||||
|
@ -6816,6 +6819,9 @@ msgstr ""
|
|||
msgid "EnvironmentsDashboard|The environments dashboard provides a summary of each project's environments' status, including pipeline and alert statuses."
|
||||
msgstr ""
|
||||
|
||||
msgid "Environments|An error occurred while canceling the auto stop, please try again"
|
||||
msgstr ""
|
||||
|
||||
msgid "Environments|An error occurred while fetching the environments."
|
||||
msgstr ""
|
||||
|
||||
|
@ -6834,6 +6840,12 @@ msgstr ""
|
|||
msgid "Environments|Are you sure you want to stop this environment?"
|
||||
msgstr ""
|
||||
|
||||
msgid "Environments|Auto stop in"
|
||||
msgstr ""
|
||||
|
||||
msgid "Environments|Auto stops %{auto_stop_time}"
|
||||
msgstr ""
|
||||
|
||||
msgid "Environments|Commit"
|
||||
msgstr ""
|
||||
|
||||
|
@ -11121,6 +11133,9 @@ msgstr ""
|
|||
msgid "May"
|
||||
msgstr ""
|
||||
|
||||
msgid "Measured in bytes of code. Excludes generated and vendored code."
|
||||
msgstr ""
|
||||
|
||||
msgid "Median"
|
||||
msgstr ""
|
||||
|
||||
|
@ -13329,6 +13344,9 @@ msgstr ""
|
|||
msgid "Prevent approval of merge requests by merge request committers"
|
||||
msgstr ""
|
||||
|
||||
msgid "Prevent environment from auto-stopping"
|
||||
msgstr ""
|
||||
|
||||
msgid "Preview"
|
||||
msgstr ""
|
||||
|
||||
|
|
|
@ -72,3 +72,4 @@ Disallow: /*/*/protected_branches
|
|||
Disallow: /*/*/uploads/
|
||||
Disallow: /*/-/group_members
|
||||
Disallow: /*/project_members
|
||||
Disallow: /groups/*/-/analytics
|
||||
|
|
|
@ -84,4 +84,31 @@ describe 'Profile > Active Sessions', :clean_gitlab_redis_shared_state do
|
|||
expect(page).not_to have_content('Chrome on Windows')
|
||||
end
|
||||
end
|
||||
|
||||
it 'User can revoke a session', :js, :redis_session_store do
|
||||
Capybara::Session.new(:session1)
|
||||
Capybara::Session.new(:session2)
|
||||
|
||||
# set an additional session in another browser
|
||||
using_session :session2 do
|
||||
gitlab_sign_in(user)
|
||||
end
|
||||
|
||||
using_session :session1 do
|
||||
gitlab_sign_in(user)
|
||||
visit profile_active_sessions_path
|
||||
|
||||
expect(page).to have_link('Revoke', count: 1)
|
||||
|
||||
accept_confirm { click_on 'Revoke' }
|
||||
|
||||
expect(page).not_to have_link('Revoke')
|
||||
end
|
||||
|
||||
using_session :session2 do
|
||||
visit profile_active_sessions_path
|
||||
|
||||
expect(page).to have_content('You need to sign in or sign up before continuing.')
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
|
@ -12,6 +12,10 @@ describe 'Environment' do
|
|||
project.add_role(user, role)
|
||||
end
|
||||
|
||||
def auto_stop_button_selector
|
||||
%q{button[title="Prevent environment from auto-stopping"]}
|
||||
end
|
||||
|
||||
describe 'environment details page' do
|
||||
let!(:environment) { create(:environment, project: project) }
|
||||
let!(:permissions) { }
|
||||
|
@ -27,6 +31,40 @@ describe 'Environment' do
|
|||
expect(page).to have_content(environment.name)
|
||||
end
|
||||
|
||||
context 'without auto-stop' do
|
||||
it 'does not show auto-stop text' do
|
||||
expect(page).not_to have_content('Auto stops')
|
||||
end
|
||||
|
||||
it 'does not show auto-stop button' do
|
||||
expect(page).not_to have_selector(auto_stop_button_selector)
|
||||
end
|
||||
end
|
||||
|
||||
context 'with auto-stop' do
|
||||
let!(:environment) { create(:environment, :will_auto_stop, name: 'staging', project: project) }
|
||||
|
||||
before do
|
||||
visit_environment(environment)
|
||||
end
|
||||
|
||||
it 'shows auto stop info' do
|
||||
expect(page).to have_content('Auto stops')
|
||||
end
|
||||
|
||||
it 'shows auto stop button' do
|
||||
expect(page).to have_selector(auto_stop_button_selector)
|
||||
expect(page.find(auto_stop_button_selector).find(:xpath, '..')['action']).to have_content(cancel_auto_stop_project_environment_path(environment.project, environment))
|
||||
end
|
||||
|
||||
it 'allows user to cancel auto stop', :js do
|
||||
page.find(auto_stop_button_selector).click
|
||||
wait_for_all_requests
|
||||
expect(page).to have_content('Auto stop successfully canceled.')
|
||||
expect(page).not_to have_selector(auto_stop_button_selector)
|
||||
end
|
||||
end
|
||||
|
||||
context 'without deployments' do
|
||||
it 'does not show deployments' do
|
||||
expect(page).to have_content('You don\'t have any deployments right now.')
|
||||
|
|
|
@ -64,6 +64,19 @@ describe PipelinesFinder do
|
|||
end
|
||||
end
|
||||
|
||||
context 'when project has child pipelines' do
|
||||
let!(:parent_pipeline) { create(:ci_pipeline, project: project) }
|
||||
let!(:child_pipeline) { create(:ci_pipeline, project: project, source: :parent_pipeline) }
|
||||
|
||||
let!(:pipeline_source) do
|
||||
create(:ci_sources_pipeline, pipeline: child_pipeline, source_pipeline: parent_pipeline)
|
||||
end
|
||||
|
||||
it 'filters out child pipelines and show only the parents' do
|
||||
is_expected.to eq([parent_pipeline])
|
||||
end
|
||||
end
|
||||
|
||||
HasStatus::AVAILABLE_STATUSES.each do |target|
|
||||
context "when status is #{target}" do
|
||||
let(:params) { { status: target } }
|
||||
|
|
|
@ -1,6 +1,8 @@
|
|||
import { mount } from '@vue/test-utils';
|
||||
import { format } from 'timeago.js';
|
||||
import EnvironmentItem from '~/environments/components/environment_item.vue';
|
||||
import PinComponent from '~/environments/components/environment_pin.vue';
|
||||
|
||||
import { environment, folder, tableData } from './mock_data';
|
||||
|
||||
describe('Environment item', () => {
|
||||
|
@ -26,6 +28,8 @@ describe('Environment item', () => {
|
|||
});
|
||||
});
|
||||
|
||||
const findAutoStop = () => wrapper.find('.js-auto-stop');
|
||||
|
||||
afterEach(() => {
|
||||
wrapper.destroy();
|
||||
});
|
||||
|
@ -77,6 +81,79 @@ describe('Environment item', () => {
|
|||
expect(wrapper.find('.js-commit-component')).toBeDefined();
|
||||
});
|
||||
});
|
||||
|
||||
describe('Without auto-stop date', () => {
|
||||
beforeEach(() => {
|
||||
factory({
|
||||
propsData: {
|
||||
model: environment,
|
||||
canReadEnvironment: true,
|
||||
tableData,
|
||||
shouldShowAutoStopDate: true,
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
it('should not render a date', () => {
|
||||
expect(findAutoStop().exists()).toBe(false);
|
||||
});
|
||||
|
||||
it('should not render the suto-stop button', () => {
|
||||
expect(wrapper.find(PinComponent).exists()).toBe(false);
|
||||
});
|
||||
});
|
||||
|
||||
describe('With auto-stop date', () => {
|
||||
describe('in the future', () => {
|
||||
const futureDate = new Date(Date.now() + 100000);
|
||||
beforeEach(() => {
|
||||
factory({
|
||||
propsData: {
|
||||
model: {
|
||||
...environment,
|
||||
auto_stop_at: futureDate,
|
||||
},
|
||||
canReadEnvironment: true,
|
||||
tableData,
|
||||
shouldShowAutoStopDate: true,
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
it('renders the date', () => {
|
||||
expect(findAutoStop().text()).toContain(format(futureDate));
|
||||
});
|
||||
|
||||
it('should render the auto-stop button', () => {
|
||||
expect(wrapper.find(PinComponent).exists()).toBe(true);
|
||||
});
|
||||
});
|
||||
|
||||
describe('in the past', () => {
|
||||
const pastDate = new Date(Date.now() - 100000);
|
||||
beforeEach(() => {
|
||||
factory({
|
||||
propsData: {
|
||||
model: {
|
||||
...environment,
|
||||
auto_stop_at: pastDate,
|
||||
},
|
||||
canReadEnvironment: true,
|
||||
tableData,
|
||||
shouldShowAutoStopDate: true,
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
it('should not render a date', () => {
|
||||
expect(findAutoStop().exists()).toBe(false);
|
||||
});
|
||||
|
||||
it('should not render the suto-stop button', () => {
|
||||
expect(wrapper.find(PinComponent).exists()).toBe(false);
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('With manual actions', () => {
|
||||
|
|
|
@ -0,0 +1,46 @@
|
|||
import { shallowMount } from '@vue/test-utils';
|
||||
import { GlButton } from '@gitlab/ui';
|
||||
import Icon from '~/vue_shared/components/icon.vue';
|
||||
import eventHub from '~/environments/event_hub';
|
||||
import PinComponent from '~/environments/components/environment_pin.vue';
|
||||
|
||||
describe('Pin Component', () => {
|
||||
let wrapper;
|
||||
|
||||
const factory = (options = {}) => {
|
||||
// This destroys any wrappers created before a nested call to factory reassigns it
|
||||
if (wrapper && wrapper.destroy) {
|
||||
wrapper.destroy();
|
||||
}
|
||||
wrapper = shallowMount(PinComponent, {
|
||||
...options,
|
||||
});
|
||||
};
|
||||
|
||||
const autoStopUrl = '/root/auto-stop-env-test/-/environments/38/cancel_auto_stop';
|
||||
|
||||
beforeEach(() => {
|
||||
factory({
|
||||
propsData: {
|
||||
autoStopUrl,
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
wrapper.destroy();
|
||||
});
|
||||
|
||||
it('should render the component with thumbtack icon', () => {
|
||||
expect(wrapper.find(Icon).props('name')).toBe('thumbtack');
|
||||
});
|
||||
|
||||
it('should emit onPinClick when clicked', () => {
|
||||
const eventHubSpy = jest.spyOn(eventHub, '$emit');
|
||||
const button = wrapper.find(GlButton);
|
||||
|
||||
button.vm.$emit('click');
|
||||
|
||||
expect(eventHubSpy).toHaveBeenCalledWith('cancelAutoStop', autoStopUrl);
|
||||
});
|
||||
});
|
|
@ -63,6 +63,7 @@ const environment = {
|
|||
log_path: 'root/ci-folders/environments/31/logs',
|
||||
created_at: '2016-11-07T11:11:16.525Z',
|
||||
updated_at: '2016-11-10T15:55:58.778Z',
|
||||
auto_stop_at: null,
|
||||
};
|
||||
|
||||
const folder = {
|
||||
|
@ -98,6 +99,10 @@ const tableData = {
|
|||
title: 'Updated',
|
||||
spacing: 'section-10',
|
||||
},
|
||||
autoStop: {
|
||||
title: 'Auto stop in',
|
||||
spacing: 'section-5',
|
||||
},
|
||||
actions: {
|
||||
spacing: 'section-25',
|
||||
},
|
||||
|
|
|
@ -15,6 +15,42 @@ describe Gitlab::Ci::Pipeline::Chain::Config::Content do
|
|||
stub_feature_flags(ci_root_config_content: false)
|
||||
end
|
||||
|
||||
context 'when bridge job is passed in as parameter' do
|
||||
let(:ci_config_path) { nil }
|
||||
let(:bridge) { create(:ci_bridge) }
|
||||
|
||||
before do
|
||||
command.bridge = bridge
|
||||
end
|
||||
|
||||
context 'when bridge job has downstream yaml' do
|
||||
before do
|
||||
allow(bridge).to receive(:yaml_for_downstream).and_return('the-yaml')
|
||||
end
|
||||
|
||||
it 'returns the content already available in command' do
|
||||
subject.perform!
|
||||
|
||||
expect(pipeline.config_source).to eq 'bridge_source'
|
||||
expect(command.config_content).to eq 'the-yaml'
|
||||
end
|
||||
end
|
||||
|
||||
context 'when bridge job does not have downstream yaml' do
|
||||
before do
|
||||
allow(bridge).to receive(:yaml_for_downstream).and_return(nil)
|
||||
end
|
||||
|
||||
it 'returns the next available source' do
|
||||
subject.perform!
|
||||
|
||||
expect(pipeline.config_source).to eq 'auto_devops_source'
|
||||
template = Gitlab::Template::GitlabCiYmlTemplate.find('Beta/Auto-DevOps')
|
||||
expect(command.config_content).to eq(template.content)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
context 'when config is defined in a custom path in the repository' do
|
||||
let(:ci_config_path) { 'path/to/config.yml' }
|
||||
|
||||
|
@ -135,6 +171,23 @@ describe Gitlab::Ci::Pipeline::Chain::Config::Content do
|
|||
end
|
||||
end
|
||||
|
||||
context 'when bridge job is passed in as parameter' do
|
||||
let(:ci_config_path) { nil }
|
||||
let(:bridge) { create(:ci_bridge) }
|
||||
|
||||
before do
|
||||
command.bridge = bridge
|
||||
allow(bridge).to receive(:yaml_for_downstream).and_return('the-yaml')
|
||||
end
|
||||
|
||||
it 'returns the content already available in command' do
|
||||
subject.perform!
|
||||
|
||||
expect(pipeline.config_source).to eq 'bridge_source'
|
||||
expect(command.config_content).to eq 'the-yaml'
|
||||
end
|
||||
end
|
||||
|
||||
context 'when config is defined in a custom path in the repository' do
|
||||
let(:ci_config_path) { 'path/to/config.yml' }
|
||||
let(:config_content_result) do
|
||||
|
|
|
@ -6,6 +6,8 @@ issues:
|
|||
- assignees
|
||||
- updated_by
|
||||
- milestone
|
||||
- issue_milestones
|
||||
- milestones
|
||||
- notes
|
||||
- resource_label_events
|
||||
- resource_weight_events
|
||||
|
@ -78,6 +80,8 @@ milestone:
|
|||
- boards
|
||||
- milestone_releases
|
||||
- releases
|
||||
- issue_milestones
|
||||
- merge_request_milestones
|
||||
snippets:
|
||||
- author
|
||||
- project
|
||||
|
@ -106,6 +110,8 @@ merge_requests:
|
|||
- assignee
|
||||
- updated_by
|
||||
- milestone
|
||||
- merge_request_milestones
|
||||
- milestones
|
||||
- notes
|
||||
- resource_label_events
|
||||
- label_links
|
||||
|
@ -146,6 +152,12 @@ merge_requests:
|
|||
- deployment_merge_requests
|
||||
- deployments
|
||||
- user_mentions
|
||||
issue_milestones:
|
||||
- milestone
|
||||
- issue
|
||||
merge_request_milestones:
|
||||
- milestone
|
||||
- merge_request
|
||||
external_pull_requests:
|
||||
- project
|
||||
merge_request_diff:
|
||||
|
@ -189,6 +201,8 @@ ci_pipelines:
|
|||
- sourced_pipelines
|
||||
- triggered_by_pipeline
|
||||
- triggered_pipelines
|
||||
- child_pipelines
|
||||
- parent_pipeline
|
||||
- downstream_bridges
|
||||
- job_artifacts
|
||||
- vulnerabilities_occurrence_pipelines
|
||||
|
|
|
@ -44,6 +44,19 @@ RSpec.describe ActiveSession, :clean_gitlab_redis_shared_state do
|
|||
end
|
||||
end
|
||||
|
||||
describe '#public_id' do
|
||||
it 'returns an encrypted, url-encoded session id' do
|
||||
original_session_id = "!*'();:@&\n=+$,/?%abcd#123[4567]8"
|
||||
active_session = ActiveSession.new(session_id: original_session_id)
|
||||
encrypted_encoded_id = active_session.public_id
|
||||
|
||||
encrypted_id = CGI.unescape(encrypted_encoded_id)
|
||||
derived_session_id = Gitlab::CryptoHelper.aes256_gcm_decrypt(encrypted_id)
|
||||
|
||||
expect(original_session_id).to eq derived_session_id
|
||||
end
|
||||
end
|
||||
|
||||
describe '.list' do
|
||||
it 'returns all sessions by user' do
|
||||
Gitlab::Redis::SharedState.with do |redis|
|
||||
|
@ -173,8 +186,7 @@ RSpec.describe ActiveSession, :clean_gitlab_redis_shared_state do
|
|||
device_name: 'iPhone 6',
|
||||
device_type: 'smartphone',
|
||||
created_at: Time.zone.parse('2018-03-12 09:06'),
|
||||
updated_at: Time.zone.parse('2018-03-12 09:06'),
|
||||
session_id: '6919a6f1bb119dd7396fadc38fd18d0d'
|
||||
updated_at: Time.zone.parse('2018-03-12 09:06')
|
||||
)
|
||||
end
|
||||
end
|
||||
|
@ -244,6 +256,40 @@ RSpec.describe ActiveSession, :clean_gitlab_redis_shared_state do
|
|||
end
|
||||
end
|
||||
|
||||
describe '.destroy_with_public_id' do
|
||||
it 'receives a user and public id and destroys the associated session' do
|
||||
ActiveSession.set(user, request)
|
||||
session = ActiveSession.list(user).first
|
||||
|
||||
ActiveSession.destroy_with_public_id(user, session.public_id)
|
||||
|
||||
total_sessions = ActiveSession.list(user).count
|
||||
expect(total_sessions).to eq 0
|
||||
end
|
||||
|
||||
it 'handles invalid input for public id' do
|
||||
expect do
|
||||
ActiveSession.destroy_with_public_id(user, nil)
|
||||
end.not_to raise_error
|
||||
|
||||
expect do
|
||||
ActiveSession.destroy_with_public_id(user, "")
|
||||
end.not_to raise_error
|
||||
|
||||
expect do
|
||||
ActiveSession.destroy_with_public_id(user, "aaaaaaaa")
|
||||
end.not_to raise_error
|
||||
end
|
||||
|
||||
it 'does not attempt to destroy session when given invalid input for public id' do
|
||||
expect(ActiveSession).not_to receive(:destroy)
|
||||
|
||||
ActiveSession.destroy_with_public_id(user, nil)
|
||||
ActiveSession.destroy_with_public_id(user, "")
|
||||
ActiveSession.destroy_with_public_id(user, "aaaaaaaa")
|
||||
end
|
||||
end
|
||||
|
||||
describe '.cleanup' do
|
||||
before do
|
||||
stub_const("ActiveSession::ALLOWED_NUMBER_OF_ACTIVE_SESSIONS", 5)
|
||||
|
|
|
@ -2716,4 +2716,114 @@ describe Ci::Pipeline, :mailer do
|
|||
end
|
||||
end
|
||||
end
|
||||
|
||||
describe '#parent_pipeline' do
|
||||
let(:project) { create(:project) }
|
||||
let(:pipeline) { create(:ci_pipeline, project: project) }
|
||||
|
||||
context 'when pipeline is triggered by a pipeline from the same project' do
|
||||
let(:upstream_pipeline) { create(:ci_pipeline, project: pipeline.project) }
|
||||
|
||||
before do
|
||||
create(:ci_sources_pipeline,
|
||||
source_pipeline: upstream_pipeline,
|
||||
source_project: project,
|
||||
pipeline: pipeline,
|
||||
project: project)
|
||||
end
|
||||
|
||||
it 'returns the parent pipeline' do
|
||||
expect(pipeline.parent_pipeline).to eq(upstream_pipeline)
|
||||
end
|
||||
|
||||
it 'is child' do
|
||||
expect(pipeline).to be_child
|
||||
end
|
||||
end
|
||||
|
||||
context 'when pipeline is triggered by a pipeline from another project' do
|
||||
let(:upstream_pipeline) { create(:ci_pipeline) }
|
||||
|
||||
before do
|
||||
create(:ci_sources_pipeline,
|
||||
source_pipeline: upstream_pipeline,
|
||||
source_project: upstream_pipeline.project,
|
||||
pipeline: pipeline,
|
||||
project: project)
|
||||
end
|
||||
|
||||
it 'returns nil' do
|
||||
expect(pipeline.parent_pipeline).to be_nil
|
||||
end
|
||||
|
||||
it 'is not child' do
|
||||
expect(pipeline).not_to be_child
|
||||
end
|
||||
end
|
||||
|
||||
context 'when pipeline is not triggered by a pipeline' do
|
||||
it 'returns nil' do
|
||||
expect(pipeline.parent_pipeline).to be_nil
|
||||
end
|
||||
|
||||
it 'is not child' do
|
||||
expect(pipeline).not_to be_child
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
describe '#child_pipelines' do
|
||||
let(:project) { create(:project) }
|
||||
let(:pipeline) { create(:ci_pipeline, project: project) }
|
||||
|
||||
context 'when pipeline triggered other pipelines on same project' do
|
||||
let(:downstream_pipeline) { create(:ci_pipeline, project: pipeline.project) }
|
||||
|
||||
before do
|
||||
create(:ci_sources_pipeline,
|
||||
source_pipeline: pipeline,
|
||||
source_project: pipeline.project,
|
||||
pipeline: downstream_pipeline,
|
||||
project: pipeline.project)
|
||||
end
|
||||
|
||||
it 'returns the child pipelines' do
|
||||
expect(pipeline.child_pipelines).to eq [downstream_pipeline]
|
||||
end
|
||||
|
||||
it 'is parent' do
|
||||
expect(pipeline).to be_parent
|
||||
end
|
||||
end
|
||||
|
||||
context 'when pipeline triggered other pipelines on another project' do
|
||||
let(:downstream_pipeline) { create(:ci_pipeline) }
|
||||
|
||||
before do
|
||||
create(:ci_sources_pipeline,
|
||||
source_pipeline: pipeline,
|
||||
source_project: pipeline.project,
|
||||
pipeline: downstream_pipeline,
|
||||
project: downstream_pipeline.project)
|
||||
end
|
||||
|
||||
it 'returns empty array' do
|
||||
expect(pipeline.child_pipelines).to be_empty
|
||||
end
|
||||
|
||||
it 'is not parent' do
|
||||
expect(pipeline).not_to be_parent
|
||||
end
|
||||
end
|
||||
|
||||
context 'when pipeline did not trigger any pipelines' do
|
||||
it 'returns empty array' do
|
||||
expect(pipeline.child_pipelines).to be_empty
|
||||
end
|
||||
|
||||
it 'is not parent' do
|
||||
expect(pipeline).not_to be_parent
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
|
@ -53,43 +53,6 @@ describe Issuable do
|
|||
it_behaves_like 'validates description length with custom validation'
|
||||
it_behaves_like 'truncates the description to its allowed maximum length on import'
|
||||
end
|
||||
|
||||
describe 'milestone' do
|
||||
let(:project) { create(:project) }
|
||||
let(:milestone_id) { create(:milestone, project: project).id }
|
||||
let(:params) do
|
||||
{
|
||||
title: 'something',
|
||||
project: project,
|
||||
author: build(:user),
|
||||
milestone_id: milestone_id
|
||||
}
|
||||
end
|
||||
|
||||
subject { issuable_class.new(params) }
|
||||
|
||||
context 'with correct params' do
|
||||
it { is_expected.to be_valid }
|
||||
end
|
||||
|
||||
context 'with empty string milestone' do
|
||||
let(:milestone_id) { '' }
|
||||
|
||||
it { is_expected.to be_valid }
|
||||
end
|
||||
|
||||
context 'with nil milestone id' do
|
||||
let(:milestone_id) { nil }
|
||||
|
||||
it { is_expected.to be_valid }
|
||||
end
|
||||
|
||||
context 'with a milestone id from another project' do
|
||||
let(:milestone_id) { create(:milestone).id }
|
||||
|
||||
it { is_expected.to be_invalid }
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
describe "Scope" do
|
||||
|
@ -141,48 +104,6 @@ describe Issuable do
|
|||
end
|
||||
end
|
||||
|
||||
describe '#milestone_available?' do
|
||||
let(:group) { create(:group) }
|
||||
let(:project) { create(:project, group: group) }
|
||||
let(:issue) { create(:issue, project: project) }
|
||||
|
||||
def build_issuable(milestone_id)
|
||||
issuable_class.new(project: project, milestone_id: milestone_id)
|
||||
end
|
||||
|
||||
it 'returns true with a milestone from the issue project' do
|
||||
milestone = create(:milestone, project: project)
|
||||
|
||||
expect(build_issuable(milestone.id).milestone_available?).to be_truthy
|
||||
end
|
||||
|
||||
it 'returns true with a milestone from the issue project group' do
|
||||
milestone = create(:milestone, group: group)
|
||||
|
||||
expect(build_issuable(milestone.id).milestone_available?).to be_truthy
|
||||
end
|
||||
|
||||
it 'returns true with a milestone from the the parent of the issue project group' do
|
||||
parent = create(:group)
|
||||
group.update(parent: parent)
|
||||
milestone = create(:milestone, group: parent)
|
||||
|
||||
expect(build_issuable(milestone.id).milestone_available?).to be_truthy
|
||||
end
|
||||
|
||||
it 'returns false with a milestone from another project' do
|
||||
milestone = create(:milestone)
|
||||
|
||||
expect(build_issuable(milestone.id).milestone_available?).to be_falsey
|
||||
end
|
||||
|
||||
it 'returns false with a milestone from another group' do
|
||||
milestone = create(:milestone, group: create(:group))
|
||||
|
||||
expect(build_issuable(milestone.id).milestone_available?).to be_falsey
|
||||
end
|
||||
end
|
||||
|
||||
describe ".search" do
|
||||
let!(:searchable_issue) { create(:issue, title: "Searchable awesome issue") }
|
||||
let!(:searchable_issue2) { create(:issue, title: 'Aw') }
|
||||
|
@ -809,27 +730,6 @@ describe Issuable do
|
|||
end
|
||||
end
|
||||
|
||||
describe '#supports_milestone?' do
|
||||
let(:group) { create(:group) }
|
||||
let(:project) { create(:project, group: group) }
|
||||
|
||||
context "for issues" do
|
||||
let(:issue) { build(:issue, project: project) }
|
||||
|
||||
it 'returns true' do
|
||||
expect(issue.supports_milestone?).to be_truthy
|
||||
end
|
||||
end
|
||||
|
||||
context "for merge requests" do
|
||||
let(:merge_request) { build(:merge_request, target_project: project, source_project: project) }
|
||||
|
||||
it 'returns true' do
|
||||
expect(merge_request.supports_milestone?).to be_truthy
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
describe '#matches_cross_reference_regex?' do
|
||||
context "issue description with long path string" do
|
||||
let(:mentionable) { build(:issue, description: "/a" * 50000) }
|
||||
|
@ -854,91 +754,4 @@ describe Issuable do
|
|||
it_behaves_like 'matches_cross_reference_regex? fails fast'
|
||||
end
|
||||
end
|
||||
|
||||
describe 'release scopes' do
|
||||
let_it_be(:project) { create(:project) }
|
||||
let(:forked_project) { fork_project(project) }
|
||||
|
||||
let_it_be(:release_1) { create(:release, tag: 'v1.0', project: project) }
|
||||
let_it_be(:release_2) { create(:release, tag: 'v2.0', project: project) }
|
||||
let_it_be(:release_3) { create(:release, tag: 'v3.0', project: project) }
|
||||
let_it_be(:release_4) { create(:release, tag: 'v4.0', project: project) }
|
||||
|
||||
let_it_be(:milestone_1) { create(:milestone, releases: [release_1], title: 'm1', project: project) }
|
||||
let_it_be(:milestone_2) { create(:milestone, releases: [release_1, release_2], title: 'm2', project: project) }
|
||||
let_it_be(:milestone_3) { create(:milestone, releases: [release_2, release_4], title: 'm3', project: project) }
|
||||
let_it_be(:milestone_4) { create(:milestone, releases: [release_3], title: 'm4', project: project) }
|
||||
let_it_be(:milestone_5) { create(:milestone, releases: [release_3], title: 'm5', project: project) }
|
||||
let_it_be(:milestone_6) { create(:milestone, title: 'm6', project: project) }
|
||||
|
||||
let_it_be(:issue_1) { create(:issue, milestone: milestone_1, project: project) }
|
||||
let_it_be(:issue_2) { create(:issue, milestone: milestone_1, project: project) }
|
||||
let_it_be(:issue_3) { create(:issue, milestone: milestone_2, project: project) }
|
||||
let_it_be(:issue_4) { create(:issue, milestone: milestone_5, project: project) }
|
||||
let_it_be(:issue_5) { create(:issue, milestone: milestone_6, project: project) }
|
||||
let_it_be(:issue_6) { create(:issue, project: project) }
|
||||
|
||||
let(:mr_1) { create(:merge_request, milestone: milestone_1, target_project: project, source_project: project) }
|
||||
let(:mr_2) { create(:merge_request, milestone: milestone_3, target_project: project, source_project: forked_project) }
|
||||
let(:mr_3) { create(:merge_request, source_project: project) }
|
||||
|
||||
let_it_be(:issue_items) { Issue.all }
|
||||
let(:mr_items) { MergeRequest.all }
|
||||
|
||||
describe '#without_release' do
|
||||
it 'returns the issues or mrs not tied to any milestone and the ones tied to milestone with no release' do
|
||||
expect(issue_items.without_release).to contain_exactly(issue_5, issue_6)
|
||||
expect(mr_items.without_release).to contain_exactly(mr_3)
|
||||
end
|
||||
end
|
||||
|
||||
describe '#any_release' do
|
||||
it 'returns all issues or all mrs tied to a release' do
|
||||
expect(issue_items.any_release).to contain_exactly(issue_1, issue_2, issue_3, issue_4)
|
||||
expect(mr_items.any_release).to contain_exactly(mr_1, mr_2)
|
||||
end
|
||||
end
|
||||
|
||||
describe '#with_release' do
|
||||
it 'returns the issues tied to a specfic release' do
|
||||
expect(issue_items.with_release('v1.0', project.id)).to contain_exactly(issue_1, issue_2, issue_3)
|
||||
end
|
||||
|
||||
it 'returns the mrs tied to a specific release' do
|
||||
expect(mr_items.with_release('v1.0', project.id)).to contain_exactly(mr_1)
|
||||
end
|
||||
|
||||
context 'when a release has a milestone with one issue and another one with no issue' do
|
||||
it 'returns that one issue' do
|
||||
expect(issue_items.with_release('v2.0', project.id)).to contain_exactly(issue_3)
|
||||
end
|
||||
|
||||
context 'when the milestone with no issue is added as a filter' do
|
||||
it 'returns an empty list' do
|
||||
expect(issue_items.with_release('v2.0', project.id).with_milestone('m3')).to be_empty
|
||||
end
|
||||
end
|
||||
|
||||
context 'when the milestone with the issue is added as a filter' do
|
||||
it 'returns this issue' do
|
||||
expect(issue_items.with_release('v2.0', project.id).with_milestone('m2')).to contain_exactly(issue_3)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
context 'when there is no issue or mr under a specific release' do
|
||||
it 'returns no issue or no mr' do
|
||||
expect(issue_items.with_release('v4.0', project.id)).to be_empty
|
||||
expect(mr_items.with_release('v4.0', project.id)).to be_empty
|
||||
end
|
||||
end
|
||||
|
||||
context 'when a non-existent release tag is passed in' do
|
||||
it 'returns no issue or no mr' do
|
||||
expect(issue_items.with_release('v999.0', project.id)).to be_empty
|
||||
expect(mr_items.with_release('v999.0', project.id)).to be_empty
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
|
@ -0,0 +1,243 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
require 'spec_helper'
|
||||
|
||||
describe Milestoneable do
|
||||
let(:user) { create(:user) }
|
||||
let(:milestone) { create(:milestone, project: project) }
|
||||
|
||||
shared_examples_for 'an object that can be assigned a milestone' do
|
||||
describe 'Validation' do
|
||||
describe 'milestone' do
|
||||
let(:project) { create(:project, :repository) }
|
||||
let(:milestone_id) { milestone.id }
|
||||
|
||||
subject { milestoneable_class.new(params) }
|
||||
|
||||
context 'with correct params' do
|
||||
it { is_expected.to be_valid }
|
||||
end
|
||||
|
||||
context 'with empty string milestone' do
|
||||
let(:milestone_id) { '' }
|
||||
|
||||
it { is_expected.to be_valid }
|
||||
end
|
||||
|
||||
context 'with nil milestone id' do
|
||||
let(:milestone_id) { nil }
|
||||
|
||||
it { is_expected.to be_valid }
|
||||
end
|
||||
|
||||
context 'with a milestone id from another project' do
|
||||
let(:milestone_id) { create(:milestone).id }
|
||||
|
||||
it { is_expected.to be_invalid }
|
||||
end
|
||||
|
||||
context 'when valid and saving' do
|
||||
it 'copies the value to the new milestones relationship' do
|
||||
subject.save!
|
||||
|
||||
expect(subject.milestones).to match_array([milestone])
|
||||
end
|
||||
|
||||
context 'with old values in milestones relationship' do
|
||||
let(:old_milestone) { create(:milestone, project: project) }
|
||||
|
||||
before do
|
||||
subject.milestone = old_milestone
|
||||
subject.save!
|
||||
end
|
||||
|
||||
it 'replaces old values' do
|
||||
expect(subject.milestones).to match_array([old_milestone])
|
||||
|
||||
subject.milestone = milestone
|
||||
subject.save!
|
||||
|
||||
expect(subject.milestones).to match_array([milestone])
|
||||
end
|
||||
|
||||
it 'can nullify the milestone' do
|
||||
expect(subject.milestones).to match_array([old_milestone])
|
||||
|
||||
subject.milestone = nil
|
||||
subject.save!
|
||||
|
||||
expect(subject.milestones).to match_array([])
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
describe '#milestone_available?' do
|
||||
let(:group) { create(:group) }
|
||||
let(:project) { create(:project, group: group) }
|
||||
let(:issue) { create(:issue, project: project) }
|
||||
|
||||
def build_milestoneable(milestone_id)
|
||||
milestoneable_class.new(project: project, milestone_id: milestone_id)
|
||||
end
|
||||
|
||||
it 'returns true with a milestone from the issue project' do
|
||||
milestone = create(:milestone, project: project)
|
||||
|
||||
expect(build_milestoneable(milestone.id).milestone_available?).to be_truthy
|
||||
end
|
||||
|
||||
it 'returns true with a milestone from the issue project group' do
|
||||
milestone = create(:milestone, group: group)
|
||||
|
||||
expect(build_milestoneable(milestone.id).milestone_available?).to be_truthy
|
||||
end
|
||||
|
||||
it 'returns true with a milestone from the the parent of the issue project group' do
|
||||
parent = create(:group)
|
||||
group.update(parent: parent)
|
||||
milestone = create(:milestone, group: parent)
|
||||
|
||||
expect(build_milestoneable(milestone.id).milestone_available?).to be_truthy
|
||||
end
|
||||
|
||||
it 'returns false with a milestone from another project' do
|
||||
milestone = create(:milestone)
|
||||
|
||||
expect(build_milestoneable(milestone.id).milestone_available?).to be_falsey
|
||||
end
|
||||
|
||||
it 'returns false with a milestone from another group' do
|
||||
milestone = create(:milestone, group: create(:group))
|
||||
|
||||
expect(build_milestoneable(milestone.id).milestone_available?).to be_falsey
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
describe '#supports_milestone?' do
|
||||
let(:group) { create(:group) }
|
||||
let(:project) { create(:project, group: group) }
|
||||
|
||||
context "for issues" do
|
||||
let(:issue) { build(:issue, project: project) }
|
||||
|
||||
it 'returns true' do
|
||||
expect(issue.supports_milestone?).to be_truthy
|
||||
end
|
||||
end
|
||||
|
||||
context "for merge requests" do
|
||||
let(:merge_request) { build(:merge_request, target_project: project, source_project: project) }
|
||||
|
||||
it 'returns true' do
|
||||
expect(merge_request.supports_milestone?).to be_truthy
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
describe 'release scopes' do
|
||||
let_it_be(:project) { create(:project) }
|
||||
|
||||
let_it_be(:release_1) { create(:release, tag: 'v1.0', project: project) }
|
||||
let_it_be(:release_2) { create(:release, tag: 'v2.0', project: project) }
|
||||
let_it_be(:release_3) { create(:release, tag: 'v3.0', project: project) }
|
||||
let_it_be(:release_4) { create(:release, tag: 'v4.0', project: project) }
|
||||
|
||||
let_it_be(:milestone_1) { create(:milestone, releases: [release_1], title: 'm1', project: project) }
|
||||
let_it_be(:milestone_2) { create(:milestone, releases: [release_1, release_2], title: 'm2', project: project) }
|
||||
let_it_be(:milestone_3) { create(:milestone, releases: [release_2, release_4], title: 'm3', project: project) }
|
||||
let_it_be(:milestone_4) { create(:milestone, releases: [release_3], title: 'm4', project: project) }
|
||||
let_it_be(:milestone_5) { create(:milestone, releases: [release_3], title: 'm5', project: project) }
|
||||
let_it_be(:milestone_6) { create(:milestone, title: 'm6', project: project) }
|
||||
|
||||
let_it_be(:issue_1) { create(:issue, milestone: milestone_1, project: project) }
|
||||
let_it_be(:issue_2) { create(:issue, milestone: milestone_1, project: project) }
|
||||
let_it_be(:issue_3) { create(:issue, milestone: milestone_2, project: project) }
|
||||
let_it_be(:issue_4) { create(:issue, milestone: milestone_5, project: project) }
|
||||
let_it_be(:issue_5) { create(:issue, milestone: milestone_6, project: project) }
|
||||
let_it_be(:issue_6) { create(:issue, project: project) }
|
||||
|
||||
let_it_be(:items) { Issue.all }
|
||||
|
||||
describe '#without_release' do
|
||||
it 'returns the issues not tied to any milestone and the ones tied to milestone with no release' do
|
||||
expect(items.without_release).to contain_exactly(issue_5, issue_6)
|
||||
end
|
||||
end
|
||||
|
||||
describe '#any_release' do
|
||||
it 'returns all issues tied to a release' do
|
||||
expect(items.any_release).to contain_exactly(issue_1, issue_2, issue_3, issue_4)
|
||||
end
|
||||
end
|
||||
|
||||
describe '#with_release' do
|
||||
it 'returns the issues tied a specfic release' do
|
||||
expect(items.with_release('v1.0', project.id)).to contain_exactly(issue_1, issue_2, issue_3)
|
||||
end
|
||||
|
||||
context 'when a release has a milestone with one issue and another one with no issue' do
|
||||
it 'returns that one issue' do
|
||||
expect(items.with_release('v2.0', project.id)).to contain_exactly(issue_3)
|
||||
end
|
||||
|
||||
context 'when the milestone with no issue is added as a filter' do
|
||||
it 'returns an empty list' do
|
||||
expect(items.with_release('v2.0', project.id).with_milestone('m3')).to be_empty
|
||||
end
|
||||
end
|
||||
|
||||
context 'when the milestone with the issue is added as a filter' do
|
||||
it 'returns this issue' do
|
||||
expect(items.with_release('v2.0', project.id).with_milestone('m2')).to contain_exactly(issue_3)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
context 'when there is no issue under a specific release' do
|
||||
it 'returns no issue' do
|
||||
expect(items.with_release('v4.0', project.id)).to be_empty
|
||||
end
|
||||
end
|
||||
|
||||
context 'when a non-existent release tag is passed in' do
|
||||
it 'returns no issue' do
|
||||
expect(items.with_release('v999.0', project.id)).to be_empty
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
context 'Issues' do
|
||||
let(:milestoneable_class) { Issue }
|
||||
let(:params) do
|
||||
{
|
||||
title: 'something',
|
||||
project: project,
|
||||
author: user,
|
||||
milestone_id: milestone_id
|
||||
}
|
||||
end
|
||||
|
||||
it_behaves_like 'an object that can be assigned a milestone'
|
||||
end
|
||||
|
||||
context 'MergeRequests' do
|
||||
let(:milestoneable_class) { MergeRequest }
|
||||
let(:params) do
|
||||
{
|
||||
title: 'something',
|
||||
source_project: project,
|
||||
target_project: project,
|
||||
source_branch: 'feature',
|
||||
target_branch: 'master',
|
||||
author: user,
|
||||
milestone_id: milestone_id
|
||||
}
|
||||
end
|
||||
|
||||
it_behaves_like 'an object that can be assigned a milestone'
|
||||
end
|
||||
end
|
|
@ -5,6 +5,12 @@ require 'spec_helper'
|
|||
describe UserPreference do
|
||||
let(:user_preference) { create(:user_preference) }
|
||||
|
||||
describe 'notes filters global keys' do
|
||||
it 'contains expected values' do
|
||||
expect(UserPreference::NOTES_FILTERS.keys).to match_array([:all_notes, :only_comments, :only_activity])
|
||||
end
|
||||
end
|
||||
|
||||
describe '#set_notes_filter' do
|
||||
let(:issuable) { build_stubbed(:issue) }
|
||||
|
||||
|
|
|
@ -101,6 +101,75 @@ describe API::Notes do
|
|||
expect(json_response.first['body']).to eq(cross_reference_note.note)
|
||||
end
|
||||
end
|
||||
|
||||
context "activity filters" do
|
||||
let!(:user_reference_note) do
|
||||
create :note,
|
||||
noteable: ext_issue, project: ext_proj,
|
||||
note: "Hello there general!",
|
||||
system: false
|
||||
end
|
||||
|
||||
let(:test_url) {"/projects/#{ext_proj.id}/issues/#{ext_issue.iid}/notes"}
|
||||
|
||||
shared_examples 'a notes request' do
|
||||
it 'is a note array response' do
|
||||
expect(response).to have_gitlab_http_status(200)
|
||||
expect(response).to include_pagination_headers
|
||||
expect(json_response).to be_an Array
|
||||
end
|
||||
end
|
||||
|
||||
context "when not provided" do
|
||||
let(:count) { 2 }
|
||||
|
||||
before do
|
||||
get api(test_url, private_user)
|
||||
end
|
||||
|
||||
it_behaves_like 'a notes request'
|
||||
|
||||
it 'returns all the notes' do
|
||||
expect(json_response.count).to eq(count)
|
||||
end
|
||||
end
|
||||
|
||||
context "when all_notes provided" do
|
||||
let(:count) { 2 }
|
||||
|
||||
before do
|
||||
get api(test_url + "?activity_filter=all_notes", private_user)
|
||||
end
|
||||
|
||||
it_behaves_like 'a notes request'
|
||||
|
||||
it 'returns all the notes' do
|
||||
expect(json_response.count).to eq(count)
|
||||
end
|
||||
end
|
||||
|
||||
context "when provided" do
|
||||
using RSpec::Parameterized::TableSyntax
|
||||
|
||||
where(:filter, :count, :system_notable) do
|
||||
"only_comments" | 1 | false
|
||||
"only_activity" | 1 | true
|
||||
end
|
||||
|
||||
with_them do
|
||||
before do
|
||||
get api(test_url + "?activity_filter=#{filter}", private_user)
|
||||
end
|
||||
|
||||
it_behaves_like 'a notes request'
|
||||
|
||||
it "properly filters the returned notables" do
|
||||
expect(json_response.count).to eq(count)
|
||||
expect(json_response.first["system"]).to be system_notable
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
describe "GET /projects/:id/noteable/:noteable_id/notes/:note_id" do
|
||||
|
|
|
@ -0,0 +1,29 @@
|
|||
# frozen_string_literal: true
|
||||
require 'spec_helper'
|
||||
|
||||
describe Ci::CreatePipelineService do
|
||||
let_it_be(:project) { create(:project, :repository) }
|
||||
let_it_be(:user) { create(:admin) }
|
||||
let(:ref) { 'refs/heads/master' }
|
||||
let(:service) { described_class.new(project, user, { ref: ref }) }
|
||||
|
||||
context 'custom config content' do
|
||||
let(:bridge) do
|
||||
double(:bridge, yaml_for_downstream: <<~YML
|
||||
rspec:
|
||||
script: rspec
|
||||
custom:
|
||||
script: custom
|
||||
YML
|
||||
)
|
||||
end
|
||||
|
||||
subject { service.execute(:push, bridge: bridge) }
|
||||
|
||||
it 'creates a pipeline using the content passed in as param' do
|
||||
expect(subject).to be_persisted
|
||||
expect(subject.builds.map(&:name)).to eq %w[rspec custom]
|
||||
expect(subject.config_source).to eq 'bridge_source'
|
||||
end
|
||||
end
|
||||
end
|
|
@ -0,0 +1,116 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
# Expects `worker_class` to be defined
|
||||
shared_examples_for 'reenqueuer' do
|
||||
subject(:job) { worker_class.new }
|
||||
|
||||
before do
|
||||
allow(job).to receive(:sleep) # faster tests
|
||||
end
|
||||
|
||||
it 'implements lease_timeout' do
|
||||
expect(job.lease_timeout).to be_a(ActiveSupport::Duration)
|
||||
end
|
||||
|
||||
describe '#perform' do
|
||||
it 'tries to obtain a lease' do
|
||||
expect_to_obtain_exclusive_lease(job.lease_key)
|
||||
|
||||
job.perform
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
# Example usage:
|
||||
#
|
||||
# it_behaves_like 'it is rate limited to 1 call per', 5.seconds do
|
||||
# subject { described_class.new }
|
||||
# let(:rate_limited_method) { subject.perform }
|
||||
# end
|
||||
#
|
||||
shared_examples_for 'it is rate limited to 1 call per' do |minimum_duration|
|
||||
before do
|
||||
# Allow Timecop freeze and travel without the block form
|
||||
Timecop.safe_mode = false
|
||||
Timecop.freeze
|
||||
|
||||
time_travel_during_rate_limited_method(actual_duration)
|
||||
end
|
||||
|
||||
after do
|
||||
Timecop.return
|
||||
Timecop.safe_mode = true
|
||||
end
|
||||
|
||||
context 'when the work finishes in 0 seconds' do
|
||||
let(:actual_duration) { 0 }
|
||||
|
||||
it 'sleeps exactly the minimum duration' do
|
||||
expect(subject).to receive(:sleep).with(a_value_within(0.01).of(minimum_duration))
|
||||
|
||||
rate_limited_method
|
||||
end
|
||||
end
|
||||
|
||||
context 'when the work finishes in 10% of minimum duration' do
|
||||
let(:actual_duration) { 0.1 * minimum_duration }
|
||||
|
||||
it 'sleeps 90% of minimum duration' do
|
||||
expect(subject).to receive(:sleep).with(a_value_within(0.01).of(0.9 * minimum_duration))
|
||||
|
||||
rate_limited_method
|
||||
end
|
||||
end
|
||||
|
||||
context 'when the work finishes in 90% of minimum duration' do
|
||||
let(:actual_duration) { 0.9 * minimum_duration }
|
||||
|
||||
it 'sleeps 10% of minimum duration' do
|
||||
expect(subject).to receive(:sleep).with(a_value_within(0.01).of(0.1 * minimum_duration))
|
||||
|
||||
rate_limited_method
|
||||
end
|
||||
end
|
||||
|
||||
context 'when the work finishes exactly at minimum duration' do
|
||||
let(:actual_duration) { minimum_duration }
|
||||
|
||||
it 'does not sleep' do
|
||||
expect(subject).not_to receive(:sleep)
|
||||
|
||||
rate_limited_method
|
||||
end
|
||||
end
|
||||
|
||||
context 'when the work takes 10% longer than minimum duration' do
|
||||
let(:actual_duration) { 1.1 * minimum_duration }
|
||||
|
||||
it 'does not sleep' do
|
||||
expect(subject).not_to receive(:sleep)
|
||||
|
||||
rate_limited_method
|
||||
end
|
||||
end
|
||||
|
||||
context 'when the work takes twice as long as minimum duration' do
|
||||
let(:actual_duration) { 2 * minimum_duration }
|
||||
|
||||
it 'does not sleep' do
|
||||
expect(subject).not_to receive(:sleep)
|
||||
|
||||
rate_limited_method
|
||||
end
|
||||
end
|
||||
|
||||
def time_travel_during_rate_limited_method(actual_duration)
|
||||
# Save the original implementation of ensure_minimum_duration
|
||||
original_ensure_minimum_duration = subject.method(:ensure_minimum_duration)
|
||||
|
||||
allow(subject).to receive(:ensure_minimum_duration) do |minimum_duration, &block|
|
||||
original_ensure_minimum_duration.call(minimum_duration) do
|
||||
# Time travel inside the block inside ensure_minimum_duration
|
||||
Timecop.travel(actual_duration) if actual_duration && actual_duration > 0
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
|
@ -0,0 +1,179 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
require 'spec_helper'
|
||||
|
||||
describe Reenqueuer do
|
||||
include ExclusiveLeaseHelpers
|
||||
|
||||
let_it_be(:worker_class) do
|
||||
Class.new do
|
||||
def self.name
|
||||
'Gitlab::Foo::Bar::DummyWorker'
|
||||
end
|
||||
|
||||
include ApplicationWorker
|
||||
prepend Reenqueuer
|
||||
|
||||
attr_reader :performed_args
|
||||
|
||||
def perform(*args)
|
||||
@performed_args = args
|
||||
|
||||
success? # for stubbing
|
||||
end
|
||||
|
||||
def success?
|
||||
false
|
||||
end
|
||||
|
||||
def lease_timeout
|
||||
30.seconds
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
subject(:job) { worker_class.new }
|
||||
|
||||
before do
|
||||
allow(job).to receive(:sleep) # faster tests
|
||||
end
|
||||
|
||||
it_behaves_like 'reenqueuer'
|
||||
|
||||
it_behaves_like 'it is rate limited to 1 call per', 5.seconds do
|
||||
let(:rate_limited_method) { subject.perform }
|
||||
end
|
||||
|
||||
it 'disables Sidekiq retries' do
|
||||
expect(job.sidekiq_options_hash).to include('retry' => false)
|
||||
end
|
||||
|
||||
describe '#perform', :clean_gitlab_redis_shared_state do
|
||||
let(:arbitrary_args) { [:foo, 'bar', { a: 1 }] }
|
||||
|
||||
context 'when the lease is available' do
|
||||
it 'does perform' do
|
||||
job.perform(*arbitrary_args)
|
||||
|
||||
expect(job.performed_args).to eq(arbitrary_args)
|
||||
end
|
||||
end
|
||||
|
||||
context 'when the lease is taken' do
|
||||
before do
|
||||
stub_exclusive_lease_taken(job.lease_key)
|
||||
end
|
||||
|
||||
it 'does not perform' do
|
||||
job.perform(*arbitrary_args)
|
||||
|
||||
expect(job.performed_args).to be_nil
|
||||
end
|
||||
end
|
||||
|
||||
context 'when #perform returns truthy' do
|
||||
before do
|
||||
allow(job).to receive(:success?).and_return(true)
|
||||
end
|
||||
|
||||
it 'reenqueues the worker' do
|
||||
expect(worker_class).to receive(:perform_async)
|
||||
|
||||
job.perform
|
||||
end
|
||||
end
|
||||
|
||||
context 'when #perform returns falsey' do
|
||||
it 'does not reenqueue the worker' do
|
||||
expect(worker_class).not_to receive(:perform_async)
|
||||
|
||||
job.perform
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
describe Reenqueuer::ReenqueuerSleeper do
|
||||
let_it_be(:dummy_class) do
|
||||
Class.new do
|
||||
include Reenqueuer::ReenqueuerSleeper
|
||||
|
||||
def rate_limited_method
|
||||
ensure_minimum_duration(11.seconds) do
|
||||
# do work
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
subject(:dummy) { dummy_class.new }
|
||||
|
||||
# Test that rate_limited_method is rate limited by ensure_minimum_duration
|
||||
it_behaves_like 'it is rate limited to 1 call per', 11.seconds do
|
||||
let(:rate_limited_method) { dummy.rate_limited_method }
|
||||
end
|
||||
|
||||
# Test ensure_minimum_duration more directly
|
||||
describe '#ensure_minimum_duration' do
|
||||
around do |example|
|
||||
# Allow Timecop.travel without the block form
|
||||
Timecop.safe_mode = false
|
||||
|
||||
Timecop.freeze do
|
||||
example.run
|
||||
end
|
||||
|
||||
Timecop.safe_mode = true
|
||||
end
|
||||
|
||||
let(:minimum_duration) { 4.seconds }
|
||||
|
||||
context 'when the block completes well before the minimum duration' do
|
||||
let(:time_left) { 3.seconds }
|
||||
|
||||
it 'sleeps until the minimum duration' do
|
||||
expect(dummy).to receive(:sleep).with(a_value_within(0.01).of(time_left))
|
||||
|
||||
dummy.ensure_minimum_duration(minimum_duration) do
|
||||
Timecop.travel(minimum_duration - time_left)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
context 'when the block completes just before the minimum duration' do
|
||||
let(:time_left) { 0.1.seconds }
|
||||
|
||||
it 'sleeps until the minimum duration' do
|
||||
expect(dummy).to receive(:sleep).with(a_value_within(0.01).of(time_left))
|
||||
|
||||
dummy.ensure_minimum_duration(minimum_duration) do
|
||||
Timecop.travel(minimum_duration - time_left)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
context 'when the block completes just after the minimum duration' do
|
||||
let(:time_over) { 0.1.seconds }
|
||||
|
||||
it 'does not sleep' do
|
||||
expect(dummy).not_to receive(:sleep)
|
||||
|
||||
dummy.ensure_minimum_duration(minimum_duration) do
|
||||
Timecop.travel(minimum_duration + time_over)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
context 'when the block completes well after the minimum duration' do
|
||||
let(:time_over) { 10.seconds }
|
||||
|
||||
it 'does not sleep' do
|
||||
expect(dummy).not_to receive(:sleep)
|
||||
|
||||
dummy.ensure_minimum_duration(minimum_duration) do
|
||||
Timecop.travel(minimum_duration + time_over)
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
Loading…
Reference in New Issue