Skip to content
Snippets Groups Projects
Commit c0d8f9f3 authored by GitLab Bot's avatar GitLab Bot
Browse files

Add latest changes from gitlab-org/gitlab@master

parent 2cfa1fc7
No related branches found
No related tags found
No related merge requests found
Showing
with 365 additions and 111 deletions
<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 MonitoringButtonComponent from './environment_monitoring.vue';
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';
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';
 
/**
* Environment Item Component
Loading
Loading
@@ -26,21 +27,22 @@ import { convertObjectPropsToCamelCase } from '~/lib/utils/common_utils';
 
export default {
components: {
CommitComponent,
Icon,
ActionsComponent,
CommitComponent,
ExternalUrlComponent,
StopComponent,
Icon,
MonitoringButtonComponent,
PinComponent,
RollbackComponent,
StopComponent,
TerminalButtonComponent,
MonitoringButtonComponent,
TooltipOnTruncate,
UserAvatarLink,
},
directives: {
GlTooltip: GlTooltipDirective,
},
mixins: [environmentItemMixin],
mixins: [environmentItemMixin, timeagoMixin],
 
props: {
canReadEnvironment: {
Loading
Loading
@@ -52,7 +54,12 @@ export default {
model: {
type: Object,
required: true,
default: () => ({}),
},
shouldShowAutoStopDate: {
type: Boolean,
required: false,
default: false,
},
 
tableData: {
Loading
Loading
@@ -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)
Loading
Loading
@@ -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() {
Loading
Loading
@@ -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
Loading
Loading
@@ -353,7 +419,7 @@ export default {
*/
shouldRenderBuildName() {
return (
!this.model.isFolder &&
!this.isFolder &&
!_.isEmpty(this.model.last_deployment) &&
!_.isEmpty(this.model.last_deployment.deployable)
);
Loading
Loading
@@ -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 || '';
},
 
/**
Loading
Loading
@@ -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() {
Loading
Loading
@@ -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"
Loading
Loading
@@ -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>
 
Loading
Loading
@@ -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"
Loading
Loading
@@ -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"
Loading
Loading
@@ -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
Loading
Loading
@@ -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"
Loading
Loading
@@ -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="!isFolder" class="table-section" :class="tableData.date.spacing" role="gridcell">
<div role="rowheader" class="table-mobile-header">{{ tableData.date.title }}</div>
<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"
v-if="!isFolder && shouldShowAutoStopDate"
class="table-section"
:class="tableData.date.spacing"
:class="tableData.autoStop.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 }}
<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="!model.isFolder && displayEnvironmentActions"
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"
Loading
Loading
<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>
Loading
Loading
@@ -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 {
Loading
Loading
@@ -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,
Loading
Loading
@@ -42,6 +43,9 @@ export default {
: env,
);
},
shouldShowAutoStopDate() {
return this.glFeatures.autoStopEnvironments;
},
tableData() {
return {
// percent spacing for cols, should add up to 100
Loading
Loading
@@ -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',
},
};
},
Loading
Loading
@@ -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
Loading
Loading
@@ -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"
/>
 
Loading
Loading
Loading
Loading
@@ -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);
});
}
},
Loading
Loading
@@ -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: {
Loading
Loading
@@ -199,6 +209,8 @@ export default {
 
eventHub.$on('requestRollbackEnvironment', this.updateRollbackModal);
eventHub.$on('rollbackEnvironment', this.rollbackEnvironment);
eventHub.$on('cancelAutoStop', this.cancelAutoStop);
},
 
beforeDestroy() {
Loading
Loading
@@ -208,5 +220,7 @@ export default {
 
eventHub.$off('requestRollbackEnvironment', this.updateRollbackModal);
eventHub.$off('rollbackEnvironment', this.rollbackEnvironment);
eventHub.$off('cancelAutoStop', this.cancelAutoStop);
},
};
Loading
Loading
@@ -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
Loading
Loading
@@ -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
Loading
Loading
Loading
Loading
@@ -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)
Loading
Loading
Loading
Loading
@@ -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)
Loading
Loading
@@ -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
Loading
Loading
@@ -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}" }
Loading
Loading
@@ -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)
Loading
Loading
@@ -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
Loading
Loading
@@ -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
 
Loading
Loading
Loading
Loading
@@ -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
Loading
Loading
@@ -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) }
Loading
Loading
@@ -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?
 
Loading
Loading
@@ -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)
Loading
Loading
Loading
Loading
@@ -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
 
Loading
Loading
@@ -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
 
Loading
Loading
Loading
Loading
@@ -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
Loading
Loading
Loading
Loading
@@ -13,6 +13,7 @@ module Issuable
include CacheMarkdownField
include Participable
include Mentionable
include Milestoneable
include Subscribable
include StripAttribute
include Awardable
Loading
Loading
@@ -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?
Loading
Loading
@@ -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) }
Loading
Loading
@@ -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) }
Loading
Loading
@@ -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)
Loading
Loading
@@ -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
Loading
Loading
@@ -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
Loading
Loading
# 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')
Loading
Loading
@@ -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,
Loading
Loading
# frozen_string_literal: true
class IssueMilestone < ApplicationRecord
belongs_to :milestone
belongs_to :issue
end
Loading
Loading
@@ -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
 
Loading
Loading
# frozen_string_literal: true
class MergeRequestMilestone < ApplicationRecord
belongs_to :milestone
belongs_to :merge_request
end
Loading
Loading
@@ -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) }
Loading
Loading
# frozen_string_literal: true
 
class PipelineDetailsEntity < PipelineEntity
expose :project, using: ProjectEntity
expose :flags do
expose :latest?, as: :latest
end
Loading
Loading
0% Loading or .
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment