diff options
author | GitLab Bot <gitlab-bot@gitlab.com> | 2020-03-09 12:07:45 +0300 |
---|---|---|
committer | GitLab Bot <gitlab-bot@gitlab.com> | 2020-03-09 12:07:45 +0300 |
commit | f4186a753b86625a83e8499af14b5badd63a2ac2 (patch) | |
tree | b960dd9f4255e9eee9f87d28e853f163836aa4c5 /app | |
parent | 0221116862ee66024a03492b4fbbe4e069d84303 (diff) |
Add latest changes from gitlab-org/gitlab@master
Diffstat (limited to 'app')
20 files changed, 279 insertions, 314 deletions
diff --git a/app/assets/javascripts/blob/components/blob_edit_content.vue b/app/assets/javascripts/blob/components/blob_edit_content.vue new file mode 100644 index 00000000000..83303a373f3 --- /dev/null +++ b/app/assets/javascripts/blob/components/blob_edit_content.vue @@ -0,0 +1,49 @@ +<script> +import { initEditorLite } from '~/blob/utils'; + +export default { + props: { + value: { + type: String, + required: true, + }, + fileName: { + type: String, + required: false, + default: '', + }, + }, + data() { + return { + content: this.value, + editor: null, + }; + }, + watch: { + fileName(newVal) { + this.editor.updateModelLanguage(newVal); + }, + }, + mounted() { + this.editor = initEditorLite({ + el: this.$refs.editor, + blobPath: this.fileName, + blobContent: this.content, + }); + }, + methods: { + triggerFileChange() { + const val = this.editor.getValue(); + this.content = val; + this.$emit('input', val); + }, + }, +}; +</script> +<template> + <div class="file-content code"> + <pre id="editor" ref="editor" data-editor-loading @focusout="triggerFileChange">{{ + content + }}</pre> + </div> +</template> diff --git a/app/assets/javascripts/blob/utils.js b/app/assets/javascripts/blob/utils.js new file mode 100644 index 00000000000..dc2ec642e59 --- /dev/null +++ b/app/assets/javascripts/blob/utils.js @@ -0,0 +1,24 @@ +/* global ace */ +import Editor from '~/editor/editor_lite'; + +export function initEditorLite({ el, blobPath, blobContent }) { + if (!el) { + throw new Error(`"el" parameter is required to initialize Editor`); + } + let editor; + + if (window?.gon?.features?.monacoSnippets) { + editor = new Editor(); + editor.createInstance({ + el, + blobPath, + blobContent, + }); + } else { + editor = ace.edit(el); + } + + return editor; +} + +export default () => ({}); diff --git a/app/assets/javascripts/boards/components/issue_card_inner.vue b/app/assets/javascripts/boards/components/issue_card_inner.vue index 274565adab0..00da039c235 100644 --- a/app/assets/javascripts/boards/components/issue_card_inner.vue +++ b/app/assets/javascripts/boards/components/issue_card_inner.vue @@ -1,7 +1,7 @@ <script> import _ from 'underscore'; import { mapState } from 'vuex'; -import { GlTooltipDirective } from '@gitlab/ui'; +import { GlLabel, GlTooltipDirective } from '@gitlab/ui'; import issueCardInner from 'ee_else_ce/boards/mixins/issue_card_inner'; import { sprintf, __ } from '~/locale'; import Icon from '~/vue_shared/components/icon.vue'; @@ -10,18 +10,17 @@ import UserAvatarLink from '../../vue_shared/components/user_avatar/user_avatar_ import IssueDueDate from './issue_due_date.vue'; import IssueTimeEstimate from './issue_time_estimate.vue'; import boardsStore from '../stores/boards_store'; -import IssueCardInnerScopedLabel from './issue_card_inner_scoped_label.vue'; import { isScopedLabel } from '~/lib/utils/common_utils'; export default { components: { + GlLabel, Icon, UserAvatarLink, TooltipOnTruncate, IssueDueDate, IssueTimeEstimate, IssueCardWeight: () => import('ee_component/boards/components/issue_card_weight.vue'), - IssueCardInnerScopedLabel, }, directives: { GlTooltip: GlTooltipDirective, @@ -145,12 +144,6 @@ export default { boardsStore.toggleFilter(filter); }, - labelStyle(label) { - return { - backgroundColor: label.color, - color: label.textColor, - }; - }, showScopedLabel(label) { return boardsStore.scopedLabels.enabled && isScopedLabel(label); }, @@ -184,27 +177,16 @@ export default { </div> <div v-if="showLabelFooter" class="board-card-labels prepend-top-4 d-flex flex-wrap"> <template v-for="label in orderedLabels"> - <issue-card-inner-scoped-label - v-if="showScopedLabel(label)" + <gl-label :key="label.id" - :label="label" - :label-style="labelStyle(label)" + :background-color="label.color" + :title="label.title" + :description="label.description" + size="sm" + :scoped="showScopedLabel(label)" :scoped-labels-documentation-link="helpLink" - @scoped-label-click="filterByLabel($event)" - /> - - <button - v-else - :key="label.id" - v-gl-tooltip - :style="labelStyle(label)" - :title="label.description" - class="badge color-label append-right-4 prepend-top-4" - type="button" @click="filterByLabel(label)" - > - {{ label.title }} - </button> + /> </template> </div> <div class="board-card-footer d-flex justify-content-between align-items-end"> diff --git a/app/assets/javascripts/boards/components/issue_card_inner_scoped_label.vue b/app/assets/javascripts/boards/components/issue_card_inner_scoped_label.vue deleted file mode 100644 index fa4c68964cb..00000000000 --- a/app/assets/javascripts/boards/components/issue_card_inner_scoped_label.vue +++ /dev/null @@ -1,45 +0,0 @@ -<script> -import { GlLink, GlTooltip } from '@gitlab/ui'; - -export default { - components: { - GlTooltip, - GlLink, - }, - props: { - label: { - type: Object, - required: true, - }, - labelStyle: { - type: Object, - required: true, - }, - scopedLabelsDocumentationLink: { - type: String, - required: true, - }, - }, -}; -</script> - -<template> - <span - class="d-inline-block position-relative scoped-label-wrapper append-right-4 prepend-top-4 board-label" - > - <a @click="$emit('scoped-label-click', label)"> - <span :ref="'labelTitleRef'" :style="labelStyle" class="badge label color-label"> - {{ label.title }} - </span> - <gl-tooltip :target="() => $refs.labelTitleRef" placement="top" boundary="viewport"> - <span class="font-weight-bold scoped-label-tooltip-title">{{ __('Scoped label') }}</span - ><br /> - {{ label.description }} - </gl-tooltip> - </a> - - <gl-link :href="scopedLabelsDocumentationLink" target="_blank" class="label scoped-label" - ><i class="fa fa-question-circle" :style="labelStyle"></i - ></gl-link> - </span> -</template> diff --git a/app/assets/javascripts/clusters_list/components/clusters.vue b/app/assets/javascripts/clusters_list/components/clusters.vue index 9322423370b..a1b581dc627 100644 --- a/app/assets/javascripts/clusters_list/components/clusters.vue +++ b/app/assets/javascripts/clusters_list/components/clusters.vue @@ -1,8 +1,9 @@ <script> import { mapState, mapActions } from 'vuex'; import { GlTable, GlLoadingIcon, GlBadge } from '@gitlab/ui'; -import { CLUSTER_TYPES } from '../constants'; -import { __ } from '~/locale'; +import tooltip from '~/vue_shared/directives/tooltip'; +import { CLUSTER_TYPES, STATUSES } from '../constants'; +import { __, sprintf } from '~/locale'; export default { components: { @@ -10,6 +11,9 @@ export default { GlLoadingIcon, GlBadge, }, + directives: { + tooltip, + }, fields: [ { key: 'name', @@ -38,6 +42,13 @@ export default { }, methods: { ...mapActions(['fetchClusters']), + statusClass(status) { + return STATUSES[status].className; + }, + statusTitle(status) { + const { title } = STATUSES[status]; + return sprintf(__('Status: %{title}'), { title }, false); + }, }, }; </script> @@ -52,6 +63,25 @@ export default { variant="light" class="qa-clusters-table" > + <template #cell(name)="{ item }"> + <div class="d-flex flex-row-reverse flex-md-row js-status"> + {{ item.name }} + <gl-loading-icon + v-if="item.status === 'deleting'" + v-tooltip + :title="statusTitle(item.status)" + size="sm" + class="mr-2 ml-md-2" + /> + <div + v-else + v-tooltip + class="cluster-status-indicator rounded-circle align-self-center gl-w-8 gl-h-8 mr-2 ml-md-2" + :class="statusClass(item.status)" + :title="statusTitle(item.status)" + ></div> + </div> + </template> <template #cell(clusterType)="{value}"> <gl-badge variant="light"> {{ value }} diff --git a/app/assets/javascripts/clusters_list/constants.js b/app/assets/javascripts/clusters_list/constants.js index 4125288b5a5..9428f08176c 100644 --- a/app/assets/javascripts/clusters_list/constants.js +++ b/app/assets/javascripts/clusters_list/constants.js @@ -6,6 +6,10 @@ export const CLUSTER_TYPES = { instance_type: __('Instance'), }; -export default { - CLUSTER_TYPES, +export const STATUSES = { + disabled: { className: 'disabled', title: __('Disabled') }, + connected: { className: 'bg-success', title: __('Connected') }, + unreachable: { className: 'bg-danger', title: __('Unreachable') }, + authentication_failure: { className: 'bg-warning', title: __('Authentication Failure') }, + deleting: { title: __('Deleting') }, }; diff --git a/app/assets/javascripts/clusters_list/store/state.js b/app/assets/javascripts/clusters_list/store/state.js index e6cdf9d67db..ed032ed8435 100644 --- a/app/assets/javascripts/clusters_list/store/state.js +++ b/app/assets/javascripts/clusters_list/store/state.js @@ -1,19 +1,5 @@ export default (initialState = {}) => ({ endpoint: initialState.endpoint, loading: false, // TODO - set this to true once integrated with BE - clusters: [ - // TODO - remove mock data once integrated with BE - // { - // name: 'My Cluster', - // environmentScope: '*', - // size: '3', - // clusterType: 'group_type', - // }, - // { - // name: 'My other cluster', - // environmentScope: 'production', - // size: '12', - // clusterType: 'project_type', - // }, - ], + clusters: [], }); diff --git a/app/assets/javascripts/snippet/snippet_bundle.js b/app/assets/javascripts/snippet/snippet_bundle.js index 42da65a941d..a3ed8d9c632 100644 --- a/app/assets/javascripts/snippet/snippet_bundle.js +++ b/app/assets/javascripts/snippet/snippet_bundle.js @@ -1,14 +1,15 @@ -/* global ace */ -import Editor from '~/editor/editor_lite'; +import { initEditorLite } from '~/blob/utils'; import setupCollapsibleInputs from './collapsible_input'; let editor; const initAce = () => { - editor = ace.edit('editor'); - + const editorEl = document.getElementById('editor'); const form = document.querySelector('.snippet-form-holder form'); const content = document.querySelector('.snippet-file-content'); + + editor = initEditorLite({ el: editorEl }); + form.addEventListener('submit', () => { content.value = editor.getValue(); }); @@ -20,8 +21,7 @@ const initMonaco = () => { const fileNameEl = document.querySelector('.js-snippet-file-name'); const form = document.querySelector('.snippet-form-holder form'); - editor = new Editor(); - editor.createInstance({ + editor = initEditorLite({ el: editorEl, blobPath: fileNameEl.value, blobContent: contentEl.value, diff --git a/app/assets/javascripts/snippets/components/snippet_blob_edit.vue b/app/assets/javascripts/snippets/components/snippet_blob_edit.vue new file mode 100644 index 00000000000..af1574f98d9 --- /dev/null +++ b/app/assets/javascripts/snippets/components/snippet_blob_edit.vue @@ -0,0 +1,36 @@ +<script> +import BlobHeaderEdit from '~/blob/components/blob_edit_header.vue'; +import BlobContentEdit from '~/blob/components/blob_edit_content.vue'; + +export default { + components: { + BlobHeaderEdit, + BlobContentEdit, + }, + props: { + content: { + type: String, + required: true, + }, + fileName: { + type: String, + required: true, + }, + }, + data() { + return { + name: this.fileName, + blobContent: this.content, + }; + }, +}; +</script> +<template> + <div class="form-group file-editor"> + <label>{{ s__('Snippets|File') }}</label> + <div class="file-holder snippet"> + <blob-header-edit v-model="name" /> + <blob-content-edit v-model="blobContent" :file-name="name" /> + </div> + </div> +</template> diff --git a/app/assets/javascripts/tracking.js b/app/assets/javascripts/tracking.js index a17b8a047c0..ab5acd83b01 100644 --- a/app/assets/javascripts/tracking.js +++ b/app/assets/javascripts/tracking.js @@ -111,4 +111,6 @@ export function initUserTracking() { if (opts.linkClickTracking) window.snowplow('enableLinkClickTracking'); Tracking.bindDocument(); + + document.dispatchEvent(new Event('SnowplowInitialized')); } diff --git a/app/assets/javascripts/vue_shared/components/sidebar/labels_select/dropdown_value.vue b/app/assets/javascripts/vue_shared/components/sidebar/labels_select/dropdown_value.vue index 4abf7c478ee..fe43f77b1ee 100644 --- a/app/assets/javascripts/vue_shared/components/sidebar/labels_select/dropdown_value.vue +++ b/app/assets/javascripts/vue_shared/components/sidebar/labels_select/dropdown_value.vue @@ -1,12 +1,10 @@ <script> -import DropdownValueScopedLabel from './dropdown_value_scoped_label.vue'; -import DropdownValueRegularLabel from './dropdown_value_regular_label.vue'; +import { GlLabel } from '@gitlab/ui'; import { isScopedLabel } from '~/lib/utils/common_utils'; export default { components: { - DropdownValueScopedLabel, - DropdownValueRegularLabel, + GlLabel, }, props: { labels: { @@ -37,12 +35,6 @@ export default { labelFilterUrl(label) { return `${this.labelFilterBasePath}?label_name[]=${encodeURIComponent(label.title)}`; }, - labelStyle(label) { - return { - color: label.textColor, - backgroundColor: label.color, - }; - }, scopedLabelsDescription({ description = '' }) { return `<span class="font-weight-bold scoped-label-tooltip-title">Scoped label</span><br />${description}`; }, @@ -65,22 +57,15 @@ export default { </span> <template v-for="label in labels" v-else> - <dropdown-value-scoped-label - v-if="showScopedLabels(label)" + <gl-label :key="label.id" - :label="label" - :label-filter-url="labelFilterUrl(label)" - :label-style="labelStyle(label)" + :target="labelFilterUrl(label)" + :background-color="label.color" + :title="label.title" + :description="label.description" + :scoped="showScopedLabels(label)" :scoped-labels-documentation-link="scopedLabelsDocumentationLink" /> - - <dropdown-value-regular-label - v-else - :key="label.id" - :label="label" - :label-filter-url="labelFilterUrl(label)" - :label-style="labelStyle(label)" - /> </template> </div> </template> diff --git a/app/assets/javascripts/vue_shared/components/sidebar/labels_select/dropdown_value_regular_label.vue b/app/assets/javascripts/vue_shared/components/sidebar/labels_select/dropdown_value_regular_label.vue deleted file mode 100644 index 839117becd9..00000000000 --- a/app/assets/javascripts/vue_shared/components/sidebar/labels_select/dropdown_value_regular_label.vue +++ /dev/null @@ -1,39 +0,0 @@ -<script> -import { GlTooltip } from '@gitlab/ui'; - -export default { - components: { - GlTooltip, - }, - props: { - label: { - type: Object, - required: true, - }, - labelStyle: { - type: Object, - required: true, - }, - labelFilterUrl: { - type: String, - required: true, - }, - }, -}; -</script> - -<template> - <a ref="regularLabelRef" :href="labelFilterUrl"> - <span :style="labelStyle" class="badge color-label"> - {{ label.title }} - </span> - <gl-tooltip - v-if="label.description" - :target="() => $refs.regularLabelRef" - placement="top" - boundary="viewport" - > - {{ label.description }} - </gl-tooltip> - </a> -</template> diff --git a/app/assets/javascripts/vue_shared/components/sidebar/labels_select/dropdown_value_scoped_label.vue b/app/assets/javascripts/vue_shared/components/sidebar/labels_select/dropdown_value_scoped_label.vue deleted file mode 100644 index 94587e1cbab..00000000000 --- a/app/assets/javascripts/vue_shared/components/sidebar/labels_select/dropdown_value_scoped_label.vue +++ /dev/null @@ -1,52 +0,0 @@ -<script> -import { GlLink, GlTooltip } from '@gitlab/ui'; - -export default { - components: { - GlTooltip, - GlLink, - }, - props: { - label: { - type: Object, - required: true, - }, - labelStyle: { - type: Object, - required: true, - }, - scopedLabelsDocumentationLink: { - type: String, - required: true, - }, - labelFilterUrl: { - type: String, - required: true, - }, - }, -}; -</script> - -<template> - <span class="d-inline-block position-relative scoped-label-wrapper"> - <a :href="labelFilterUrl"> - <span :ref="`labelTitleRef`" :style="labelStyle" class="badge color-label label"> - {{ label.title }} - </span> - <gl-tooltip - v-if="label.description" - :target="() => $refs.labelTitleRef" - placement="top" - boundary="viewport" - > - <span class="font-weight-bold scoped-label-tooltip-title">{{ __('Scoped label') }}</span - ><br /> - {{ label.description }} - </gl-tooltip> - </a> - - <gl-link :href="scopedLabelsDocumentationLink" target="_blank" class="label scoped-label" - ><i class="fa fa-question-circle" :style="labelStyle"></i - ></gl-link> - </span> -</template> diff --git a/app/assets/stylesheets/pages/boards.scss b/app/assets/stylesheets/pages/boards.scss index 42d7b0d08f7..a9079f036ab 100644 --- a/app/assets/stylesheets/pages/boards.scss +++ b/app/assets/stylesheets/pages/boards.scss @@ -266,20 +266,9 @@ background-color: $blue-50; } - .badge { - border: 0; - outline: 0; - - &:hover { - text-decoration: underline; - } - - @include media-breakpoint-down(lg) { - font-size: $gl-font-size-xs; - padding-left: $gl-padding-4; - padding-right: $gl-padding-4; - font-weight: $gl-font-weight-bold; - } + .gl-label { + margin-top: 4px; + margin-right: 4px; } .confidential-icon { diff --git a/app/assets/stylesheets/pages/clusters.scss b/app/assets/stylesheets/pages/clusters.scss index 88d6b0d3746..b9d415ae237 100644 --- a/app/assets/stylesheets/pages/clusters.scss +++ b/app/assets/stylesheets/pages/clusters.scss @@ -163,3 +163,9 @@ color: $black; font-weight: $gl-font-weight-bold; } + +.cluster-status-indicator { + &.disabled { + background-color: $gray-600; + } +} diff --git a/app/assets/stylesheets/pages/issuable.scss b/app/assets/stylesheets/pages/issuable.scss index fd56f655c0a..85a8113ff1b 100644 --- a/app/assets/stylesheets/pages/issuable.scss +++ b/app/assets/stylesheets/pages/issuable.scss @@ -158,6 +158,10 @@ a:not(.btn) { color: inherit; + .gl-label-text:hover { + color: inherit; + } + &:hover { color: $blue-800; diff --git a/app/assets/stylesheets/utilities.scss b/app/assets/stylesheets/utilities.scss index 882ce29c671..1eff21401a2 100644 --- a/app/assets/stylesheets/utilities.scss +++ b/app/assets/stylesheets/utilities.scss @@ -54,8 +54,10 @@ .mh-50vh { max-height: 50vh; } .font-size-inherit { font-size: inherit; } +.gl-w-8 { width: px-to-rem($grid-size); } .gl-w-16 { width: px-to-rem($grid-size * 2); } .gl-w-64 { width: px-to-rem($grid-size * 8); } +.gl-h-8 { height: px-to-rem($grid-size); } .gl-h-32 { height: px-to-rem($grid-size * 4); } .gl-h-64 { height: px-to-rem($grid-size * 8); } diff --git a/app/finders/ci/pipelines_for_merge_request_finder.rb b/app/finders/ci/pipelines_for_merge_request_finder.rb new file mode 100644 index 00000000000..c01a68d6749 --- /dev/null +++ b/app/finders/ci/pipelines_for_merge_request_finder.rb @@ -0,0 +1,90 @@ +# frozen_string_literal: true + +module Ci + # A state object to centralize logic related to merge request pipelines + class PipelinesForMergeRequestFinder + include Gitlab::Utils::StrongMemoize + + EVENT = 'merge_request_event' + + def initialize(merge_request) + @merge_request = merge_request + end + + attr_reader :merge_request + + delegate :commit_shas, :source_project, :source_branch, to: :merge_request + + def all + strong_memoize(:all_pipelines) do + next Ci::Pipeline.none unless source_project + + pipelines = + if merge_request.persisted? + pipelines_using_cte + else + triggered_for_branch.for_sha(commit_shas) + end + + sort(pipelines) + end + end + + private + + def pipelines_using_cte + cte = Gitlab::SQL::CTE.new(:shas, merge_request.all_commits.select(:sha)) + + source_pipelines_join = cte.table[:sha].eq(Ci::Pipeline.arel_table[:source_sha]) + source_pipelines = filter_by(triggered_by_merge_request, cte, source_pipelines_join) + detached_pipelines = filter_by_sha(triggered_by_merge_request, cte) + pipelines_for_branch = filter_by_sha(triggered_for_branch, cte) + + Ci::Pipeline.with(cte.to_arel) # rubocop: disable CodeReuse/ActiveRecord + .from_union([source_pipelines, detached_pipelines, pipelines_for_branch]) + end + + def filter_by_sha(pipelines, cte) + hex = Arel::Nodes::SqlLiteral.new("'hex'") + string_sha = Arel::Nodes::NamedFunction.new('encode', [cte.table[:sha], hex]) + join_condition = string_sha.eq(Ci::Pipeline.arel_table[:sha]) + + filter_by(pipelines, cte, join_condition) + end + + def filter_by(pipelines, cte, join_condition) + shas_table = + Ci::Pipeline.arel_table + .join(cte.table, Arel::Nodes::InnerJoin) + .on(join_condition) + .join_sources + + pipelines.joins(shas_table) # rubocop: disable CodeReuse/ActiveRecord + end + + # NOTE: this method returns only parent merge request pipelines. + # Child merge request pipelines have a different source. + def triggered_by_merge_request + source_project.ci_pipelines + .where(source: :merge_request_event, merge_request: merge_request) # rubocop: disable CodeReuse/ActiveRecord + end + + def triggered_for_branch + source_project.ci_pipelines + .where(source: branch_pipeline_sources, ref: source_branch, tag: false) # rubocop: disable CodeReuse/ActiveRecord + end + + def branch_pipeline_sources + strong_memoize(:branch_pipeline_sources) do + Ci::Pipeline.sources.reject { |source| source == EVENT }.values + end + end + + def sort(pipelines) + sql = 'CASE ci_pipelines.source WHEN (?) THEN 0 ELSE 1 END, ci_pipelines.id DESC' + query = ApplicationRecord.send(:sanitize_sql_array, [sql, Ci::Pipeline.sources[:merge_request_event]]) # rubocop:disable GitlabSecurity/PublicSend + + pipelines.order(Arel.sql(query)) # rubocop: disable CodeReuse/ActiveRecord + end + end +end diff --git a/app/models/merge_request.rb b/app/models/merge_request.rb index 0bc4e550678..26e3c8f38f6 100644 --- a/app/models/merge_request.rb +++ b/app/models/merge_request.rb @@ -1251,7 +1251,7 @@ class MergeRequest < ApplicationRecord def all_pipelines strong_memoize(:all_pipelines) do - MergeRequest::Pipelines.new(self).all + Ci::PipelinesForMergeRequestFinder.new(self).all end end diff --git a/app/models/merge_request/pipelines.rb b/app/models/merge_request/pipelines.rb deleted file mode 100644 index 72756e8e9d0..00000000000 --- a/app/models/merge_request/pipelines.rb +++ /dev/null @@ -1,88 +0,0 @@ -# frozen_string_literal: true - -# A state object to centralize logic related to merge request pipelines -class MergeRequest::Pipelines - include Gitlab::Utils::StrongMemoize - - EVENT = 'merge_request_event' - - def initialize(merge_request) - @merge_request = merge_request - end - - attr_reader :merge_request - - delegate :commit_shas, :source_project, :source_branch, to: :merge_request - - def all - strong_memoize(:all_pipelines) do - next Ci::Pipeline.none unless source_project - - pipelines = - if merge_request.persisted? - pipelines_using_cte - else - triggered_for_branch.for_sha(commit_shas) - end - - sort(pipelines) - end - end - - private - - def pipelines_using_cte - cte = Gitlab::SQL::CTE.new(:shas, merge_request.all_commits.select(:sha)) - - source_pipelines_join = cte.table[:sha].eq(Ci::Pipeline.arel_table[:source_sha]) - source_pipelines = filter_by(triggered_by_merge_request, cte, source_pipelines_join) - detached_pipelines = filter_by_sha(triggered_by_merge_request, cte) - pipelines_for_branch = filter_by_sha(triggered_for_branch, cte) - - Ci::Pipeline.with(cte.to_arel) - .from_union([source_pipelines, detached_pipelines, pipelines_for_branch]) - end - - def filter_by_sha(pipelines, cte) - hex = Arel::Nodes::SqlLiteral.new("'hex'") - string_sha = Arel::Nodes::NamedFunction.new('encode', [cte.table[:sha], hex]) - join_condition = string_sha.eq(Ci::Pipeline.arel_table[:sha]) - - filter_by(pipelines, cte, join_condition) - end - - def filter_by(pipelines, cte, join_condition) - shas_table = - Ci::Pipeline.arel_table - .join(cte.table, Arel::Nodes::InnerJoin) - .on(join_condition) - .join_sources - - pipelines.joins(shas_table) - end - - # NOTE: this method returns only parent merge request pipelines. - # Child merge request pipelines have a different source. - def triggered_by_merge_request - source_project.ci_pipelines - .where(source: :merge_request_event, merge_request: merge_request) - end - - def triggered_for_branch - source_project.ci_pipelines - .where(source: branch_pipeline_sources, ref: source_branch, tag: false) - end - - def branch_pipeline_sources - strong_memoize(:branch_pipeline_sources) do - Ci::Pipeline.sources.reject { |source| source == EVENT }.values - end - end - - def sort(pipelines) - sql = 'CASE ci_pipelines.source WHEN (?) THEN 0 ELSE 1 END, ci_pipelines.id DESC' - query = ApplicationRecord.send(:sanitize_sql_array, [sql, Ci::Pipeline.sources[:merge_request_event]]) # rubocop:disable GitlabSecurity/PublicSend - - pipelines.order(Arel.sql(query)) - end -end |