summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--Gemfile2
-rw-r--r--app/assets/javascripts/repository/index.js20
-rw-r--r--app/assets/javascripts/repository/mixins/preload.js4
-rw-r--r--app/assets/javascripts/repository/router.js4
-rw-r--r--app/assets/javascripts/vue_shared/components/bar_chart.vue351
-rw-r--r--app/assets/javascripts/vue_shared/components/bar_chart_constants.js4
-rw-r--r--app/models/sentry_issue.rb11
-rw-r--r--changelogs/unreleased/37174-move-contribution-analytics-chart-to-echarts.yml5
-rw-r--r--changelogs/unreleased/geo-spike-self-service-framework.yml5
-rw-r--r--changelogs/unreleased/mc-bug-fail-upstream-on-invalid-yaml.yml5
-rw-r--r--changelogs/unreleased/refactoring-entities-file-11.yml5
-rw-r--r--config/application.rb2
-rw-r--r--config/initializers/0_inflections.rb1
-rw-r--r--db/migrate/20200121192942_create_geo_events.rb14
-rw-r--r--db/migrate/20200121194000_add_geo_event_id_to_geo_event_log.rb9
-rw-r--r--db/migrate/20200121194048_add_geo_event_id_index_to_geo_event_log.rb20
-rw-r--r--db/migrate/20200121194154_add_geo_events_foreign_key.rb20
-rw-r--r--db/schema.rb10
-rw-r--r--doc/administration/monitoring/gitlab_self_monitoring_project/index.md13
-rw-r--r--doc/api/api_resources.md1
-rw-r--r--doc/api/protected_environments.md136
-rw-r--r--doc/user/application_security/container_scanning/index.md2
-rw-r--r--doc/user/application_security/dast/index.md14
-rw-r--r--doc/user/application_security/dependency_scanning/index.md8
-rw-r--r--doc/user/application_security/license_compliance/index.md14
-rw-r--r--doc/user/application_security/sast/analyzers.md8
-rw-r--r--doc/user/application_security/sast/index.md2
-rw-r--r--lib/api/entities.rb172
-rw-r--r--lib/api/entities/merge_request.rb53
-rw-r--r--lib/api/entities/merge_request_basic.rb94
-rw-r--r--lib/api/entities/merge_request_changes.rb11
-rw-r--r--lib/api/entities/merge_request_diff.rb10
-rw-r--r--lib/api/entities/merge_request_diff_full.rb13
-rw-r--r--lib/api/entities/merge_request_simple.rb12
-rw-r--r--lib/api/entities/pipeline_basic.rb14
-rw-r--r--lib/quality/test_level.rb1
-rw-r--r--locale/gitlab.pot3
-rw-r--r--qa/qa/specs/features/browser_ui/2_plan/email/trigger_email_notification_spec.rb2
-rw-r--r--qa/qa/specs/features/browser_ui/2_plan/issue/check_mentions_for_xss_spec.rb2
-rw-r--r--qa/qa/specs/features/browser_ui/2_plan/issue/close_issue_spec.rb2
-rw-r--r--qa/qa/specs/features/browser_ui/2_plan/issue/collapse_comments_in_discussions_spec.rb2
-rw-r--r--qa/qa/specs/features/browser_ui/2_plan/issue/comment_issue_spec.rb2
-rw-r--r--qa/qa/specs/features/browser_ui/2_plan/issue/create_issue_spec.rb2
-rw-r--r--qa/qa/specs/features/browser_ui/2_plan/issue/filter_issue_comments_spec.rb2
-rw-r--r--qa/qa/specs/features/browser_ui/2_plan/issue/issue_suggestions_spec.rb2
-rw-r--r--qa/qa/specs/features/browser_ui/2_plan/issue/mentions_spec.rb2
-rw-r--r--spec/frontend/environments/environment_actions_spec.js124
-rw-r--r--spec/frontend/repository/router_spec.js1
-rw-r--r--spec/javascripts/environments/environment_actions_spec.js117
-rw-r--r--spec/javascripts/vue_shared/components/bar_chart_spec.js79
-rw-r--r--spec/lib/quality/test_level_spec.rb4
-rw-r--r--spec/models/sentry_issue_spec.rb29
52 files changed, 582 insertions, 863 deletions
diff --git a/Gemfile b/Gemfile
index ac1d3d8e9c6..c9319a34594 100644
--- a/Gemfile
+++ b/Gemfile
@@ -84,7 +84,7 @@ gem 'net-ldap'
# API
gem 'grape', '~> 1.1.0'
gem 'grape-entity', '~> 0.7.1'
-gem 'rack-cors', '~> 1.0.0', require: 'rack/cors'
+gem 'rack-cors', '~> 1.0.6', require: 'rack/cors'
# GraphQL API
gem 'graphql', '~> 1.9.11'
diff --git a/app/assets/javascripts/repository/index.js b/app/assets/javascripts/repository/index.js
index a26acbbe301..aefbef5467d 100644
--- a/app/assets/javascripts/repository/index.js
+++ b/app/assets/javascripts/repository/index.js
@@ -28,8 +28,8 @@ export default function setupVueRepositoryList() {
},
});
- router.afterEach(({ params: { pathMatch } }) => {
- setTitle(pathMatch, ref, fullName);
+ router.afterEach(({ params: { path } }) => {
+ setTitle(path, ref, fullName);
});
const breadcrumbEl = document.getElementById('js-repo-breadcrumb');
@@ -48,9 +48,9 @@ export default function setupVueRepositoryList() {
newDirPath,
} = breadcrumbEl.dataset;
- router.afterEach(({ params: { pathMatch = '/' } }) => {
- updateFormAction('.js-upload-blob-form', uploadPath, pathMatch);
- updateFormAction('.js-create-dir-form', newDirPath, pathMatch);
+ router.afterEach(({ params: { path = '/' } }) => {
+ updateFormAction('.js-upload-blob-form', uploadPath, path);
+ updateFormAction('.js-create-dir-form', newDirPath, path);
});
// eslint-disable-next-line no-new
@@ -61,7 +61,7 @@ export default function setupVueRepositoryList() {
render(h) {
return h(Breadcrumbs, {
props: {
- currentPath: this.$route.params.pathMatch,
+ currentPath: this.$route.params.path,
canCollaborate: parseBoolean(canCollaborate),
canEditTree: parseBoolean(canEditTree),
newBranchPath,
@@ -84,7 +84,7 @@ export default function setupVueRepositoryList() {
render(h) {
return h(LastCommit, {
props: {
- currentPath: this.$route.params.pathMatch,
+ currentPath: this.$route.params.path,
},
});
},
@@ -100,7 +100,7 @@ export default function setupVueRepositoryList() {
render(h) {
return h(TreeActionLink, {
props: {
- path: historyLink + (this.$route.params.pathMatch || '/'),
+ path: `${historyLink}/${this.$route.params.path || ''}`,
text: __('History'),
},
});
@@ -117,7 +117,7 @@ export default function setupVueRepositoryList() {
render(h) {
return h(TreeActionLink, {
props: {
- path: webIDEUrl(`/${projectPath}/edit/${ref}/-${this.$route.params.pathMatch || '/'}`),
+ path: webIDEUrl(`/${projectPath}/edit/${ref}/-/${this.$route.params.path || ''}`),
text: __('Web IDE'),
cssClass: 'qa-web-ide-button',
},
@@ -134,7 +134,7 @@ export default function setupVueRepositoryList() {
el: directoryDownloadLinks,
router,
render(h) {
- const currentPath = this.$route.params.pathMatch || '/';
+ const currentPath = this.$route.params.path || '/';
if (currentPath !== '/') {
return h(DirectoryDownloadLinks, {
diff --git a/app/assets/javascripts/repository/mixins/preload.js b/app/assets/javascripts/repository/mixins/preload.js
index e68996245a8..cb6c2294679 100644
--- a/app/assets/javascripts/repository/mixins/preload.js
+++ b/app/assets/javascripts/repository/mixins/preload.js
@@ -13,10 +13,10 @@ export default {
return { projectPath: '', loadingPath: null };
},
beforeRouteUpdate(to, from, next) {
- this.preload(to.params.pathMatch, next);
+ this.preload(to.params.path, next);
},
methods: {
- preload(path, next) {
+ preload(path = '/', next) {
this.loadingPath = path.replace(/^\//, '');
return this.$apollo
diff --git a/app/assets/javascripts/repository/router.js b/app/assets/javascripts/repository/router.js
index 6c31c9bae82..2386773699c 100644
--- a/app/assets/javascripts/repository/router.js
+++ b/app/assets/javascripts/repository/router.js
@@ -12,11 +12,11 @@ export default function createRouter(base, baseRef) {
base: joinPaths(gon.relative_url_root || '', base),
routes: [
{
- path: `/-/tree/${escape(baseRef)}(/.*)?`,
+ path: `(/-)?/tree/${escape(baseRef)}/:path*`,
name: 'treePath',
component: TreePage,
props: route => ({
- path: route.params.pathMatch && (route.params.pathMatch.replace(/^\//, '') || '/'),
+ path: route.params.path?.replace(/^\//, '') || '/',
}),
},
{
diff --git a/app/assets/javascripts/vue_shared/components/bar_chart.vue b/app/assets/javascripts/vue_shared/components/bar_chart.vue
deleted file mode 100644
index 25d7bfe515c..00000000000
--- a/app/assets/javascripts/vue_shared/components/bar_chart.vue
+++ /dev/null
@@ -1,351 +0,0 @@
-<script>
-import * as d3 from 'd3';
-import tooltip from '../directives/tooltip';
-import Icon from './icon.vue';
-import SvgGradient from './svg_gradient.vue';
-import {
- GRADIENT_COLORS,
- GRADIENT_OPACITY,
- INVERSE_GRADIENT_COLORS,
- INVERSE_GRADIENT_OPACITY,
-} from './bar_chart_constants';
-
-/**
- * Renders a bar chart that can be dragged(scrolled) when the number
- * of elements to renders surpasses that of the available viewport space
- * while keeping even padding and a width of 24px (customizable)
- *
- * It can render data with the following format:
- * graphData: [{
- * name: 'element' // x domain data
- * value: 1 // y domain data
- * }]
- *
- * Used in:
- * - Contribution analytics - all of the rows describing pushes, merge requests and issues
- */
-
-export default {
- directives: {
- tooltip,
- },
- components: {
- Icon,
- SvgGradient,
- },
- props: {
- graphData: {
- type: Array,
- required: true,
- },
- barWidth: {
- type: Number,
- required: false,
- default: 24,
- },
- yAxisLabel: {
- type: String,
- required: true,
- },
- },
- data() {
- return {
- minX: -40,
- minY: 0,
- vbWidth: 0,
- vbHeight: 0,
- vpWidth: 0,
- vpHeight: 200,
- preserveAspectRatioType: 'xMidYMin meet',
- containerMargin: {
- leftRight: 30,
- },
- viewBoxMargin: {
- topBottom: 100,
- },
- panX: 0,
- xScale: {},
- yScale: {},
- zoom: {},
- bars: {},
- xGraphRange: 0,
- isLoading: true,
- paddingThreshold: 50,
- showScrollIndicator: false,
- showLeftScrollIndicator: false,
- isGrabbed: false,
- isPanAvailable: false,
- gradientColors: GRADIENT_COLORS,
- gradientOpacity: GRADIENT_OPACITY,
- inverseGradientColors: INVERSE_GRADIENT_COLORS,
- inverseGradientOpacity: INVERSE_GRADIENT_OPACITY,
- maxTextWidth: 72,
- rectYAxisLabelDims: {},
- xAxisTextElements: {},
- yAxisRectTransformPadding: 20,
- yAxisTextTransformPadding: 10,
- yAxisTextRotation: 90,
- };
- },
- computed: {
- svgViewBox() {
- return `${this.minX} ${this.minY} ${this.vbWidth} ${this.vbHeight}`;
- },
- xAxisLocation() {
- return `translate(${this.panX}, ${this.vbHeight})`;
- },
- barTranslationTransform() {
- return `translate(${this.panX}, 0)`;
- },
- scrollIndicatorTransform() {
- return `translate(${this.vbWidth - 80}, 0)`;
- },
- activateGrabCursor() {
- return {
- 'svg-graph-container-with-grab': this.isPanAvailable,
- 'svg-graph-container-grabbed': this.isPanAvailable && this.isGrabbed,
- };
- },
- yAxisLabelRectTransform() {
- const rectWidth =
- this.rectYAxisLabelDims.height != null ? this.rectYAxisLabelDims.height / 2 : 0;
- const yCoord = this.vbHeight / 2 - rectWidth;
-
- return `translate(${this.minX - this.yAxisRectTransformPadding}, ${yCoord})`;
- },
- yAxisLabelTextTransform() {
- const rectWidth =
- this.rectYAxisLabelDims.height != null ? this.rectYAxisLabelDims.height / 2 : 0;
- const yCoord = this.vbHeight / 2 + rectWidth - 5;
-
- return `translate(${this.minX + this.yAxisTextTransformPadding}, ${yCoord}) rotate(-${
- this.yAxisTextRotation
- })`;
- },
- },
- mounted() {
- if (!this.allValuesEmpty) {
- this.draw();
- }
- },
- methods: {
- draw() {
- // update viewport
- this.vpWidth = this.$refs.svgContainer.clientWidth - this.containerMargin.leftRight;
- // update viewbox
- this.vbWidth = this.vpWidth;
- this.vbHeight = this.vpHeight - this.viewBoxMargin.topBottom;
- let padding = 0;
- if (this.graphData.length * this.barWidth > this.vbWidth) {
- this.xGraphRange = this.graphData.length * this.barWidth;
- padding = this.calculatePadding(this.barWidth);
- this.showScrollIndicator = true;
- this.isPanAvailable = true;
- } else {
- this.xGraphRange = this.vbWidth - Math.abs(this.minX);
- }
-
- this.xScale = d3
- .scaleBand()
- .range([0, this.xGraphRange])
- .round(true)
- .paddingInner(padding);
- this.yScale = d3.scaleLinear().rangeRound([this.vbHeight, 0]);
-
- this.xScale.domain(this.graphData.map(d => d.name));
- this.yScale.domain([0, d3.max(this.graphData.map(d => d.value))]);
-
- // Zoom/Panning Function
- this.zoom = d3
- .zoom()
- .translateExtent([[0, 0], [this.xGraphRange, this.vbHeight]])
- .on('zoom', this.panGraph)
- .on('end', this.removeGrabStyling);
-
- const xAxis = d3.axisBottom().scale(this.xScale);
-
- const yAxis = d3
- .axisLeft()
- .scale(this.yScale)
- .ticks(4);
-
- const renderedXAxis = d3
- .select(this.$refs.baseSvg)
- .select('.x-axis')
- .call(xAxis);
-
- this.xAxisTextElements = this.$refs.xAxis.querySelectorAll('text');
-
- renderedXAxis.select('.domain').remove();
-
- renderedXAxis
- .selectAll('text')
- .style('text-anchor', 'end')
- .attr('dx', '-.3em')
- .attr('dy', '-.95em')
- .attr('class', 'tick-text')
- .attr('transform', 'rotate(-90)');
-
- renderedXAxis.selectAll('line').remove();
-
- const { maxTextWidth } = this;
- renderedXAxis.selectAll('text').each(function formatText() {
- const axisText = d3.select(this);
- let textLength = axisText.node().getComputedTextLength();
- let textContent = axisText.text();
- while (textLength > maxTextWidth && textContent.length > 0) {
- textContent = textContent.slice(0, -1);
- axisText.text(`${textContent}...`);
- textLength = axisText.node().getComputedTextLength();
- }
- });
-
- const width = this.vbWidth;
-
- const renderedYAxis = d3
- .select(this.$refs.baseSvg)
- .select('.y-axis')
- .call(yAxis);
-
- renderedYAxis.selectAll('.tick').each(function createTickLines(d, i) {
- if (i > 0) {
- d3.select(this)
- .select('line')
- .attr('x2', width)
- .attr('class', 'axis-tick');
- }
- });
-
- // Add the panning capabilities
- if (this.isPanAvailable) {
- d3.select(this.$refs.baseSvg)
- .call(this.zoom)
- .on('wheel.zoom', null); // This disables the pan of the graph with the scroll of the mouse wheel
- }
-
- this.isLoading = false;
- // Update the yAxisLabel coordinates
- const labelDims = this.$refs.yAxisLabel.getBBox();
- this.rectYAxisLabelDims = {
- height: labelDims.width + 10,
- };
- },
- panGraph() {
- const allowedRightScroll = this.xGraphRange - this.vbWidth - this.paddingThreshold;
- const graphMaxPan = Math.abs(d3.event.transform.x) < allowedRightScroll;
- this.isGrabbed = true;
- this.panX = d3.event.transform.x;
-
- if (d3.event.transform.x === 0) {
- this.showLeftScrollIndicator = false;
- } else {
- this.showLeftScrollIndicator = true;
- this.showScrollIndicator = true;
- }
-
- if (!graphMaxPan) {
- this.panX = -1 * (this.xGraphRange - this.vbWidth + this.paddingThreshold);
- this.showScrollIndicator = false;
- }
- },
- setTooltipTitle(data) {
- return data !== null ? `${data.name}: ${data.value}` : '';
- },
- calculatePadding(desiredBarWidth) {
- const widthWithMargin = this.vbWidth - Math.abs(this.minX);
- const dividend = widthWithMargin - this.graphData.length * desiredBarWidth;
- const divisor = widthWithMargin - desiredBarWidth;
-
- return dividend / divisor;
- },
- removeGrabStyling() {
- this.isGrabbed = false;
- },
- barHoveredIn(index) {
- this.xAxisTextElements[index].classList.add('x-axis-text');
- },
- barHoveredOut(index) {
- this.xAxisTextElements[index].classList.remove('x-axis-text');
- },
- },
-};
-</script>
-<template>
- <div ref="svgContainer" :class="activateGrabCursor" class="svg-graph-container">
- <svg
- ref="baseSvg"
- class="svg-graph overflow-visible pt-5"
- :width="vpWidth"
- :height="vpHeight"
- :viewBox="svgViewBox"
- :preserveAspectRatio="preserveAspectRatioType"
- >
- <g ref="xAxis" :transform="xAxisLocation" class="x-axis" />
- <g v-if="!isLoading">
- <template v-for="(data, index) in graphData">
- <rect
- :key="index"
- v-tooltip
- :width="xScale.bandwidth()"
- :x="xScale(data.name)"
- :y="yScale(data.value)"
- :height="vbHeight - yScale(data.value)"
- :transform="barTranslationTransform"
- :title="setTooltipTitle(data)"
- class="bar-rect"
- data-placement="top"
- @mouseover="barHoveredIn(index)"
- @mouseout="barHoveredOut(index)"
- />
- </template>
- </g>
- <rect :height="vbHeight + 100" transform="translate(-100, -5)" width="100" fill="#fff" />
- <g class="y-axis-label">
- <line :x1="0" :x2="0" :y1="0" :y2="vbHeight" transform="translate(-35, 0)" stroke="black" />
- <!-- Get text length and change the height of this rect accordingly -->
- <rect
- :height="rectYAxisLabelDims.height"
- :transform="yAxisLabelRectTransform"
- :width="30"
- fill="#fff"
- />
- <text ref="yAxisLabel" :transform="yAxisLabelTextTransform">{{ yAxisLabel }}</text>
- </g>
- <g class="y-axis" />
- <g v-if="showScrollIndicator">
- <rect
- :height="vbHeight + 100"
- :transform="`translate(${vpWidth - 60}, -5)`"
- width="40"
- fill="#fff"
- />
- <icon
- :x="vpWidth - 50"
- :y="vbHeight / 2"
- :width="14"
- :height="14"
- name="chevron-right"
- class="animate-flicker"
- />
- </g>
- <!-- The line that shows up when the data elements surpass the available width -->
- <g v-if="showScrollIndicator" :transform="scrollIndicatorTransform">
- <rect :height="vbHeight" x="0" y="0" width="20" fill="url(#shadow-gradient)" />
- </g>
- <!-- Left scroll indicator -->
- <g v-if="showLeftScrollIndicator" transform="translate(0, 0)">
- <rect :height="vbHeight" x="0" y="0" width="20" fill="url(#left-shadow-gradient)" />
- </g>
- <svg-gradient
- :colors="gradientColors"
- :opacity="gradientOpacity"
- identifier-name="shadow-gradient"
- />
- <svg-gradient
- :colors="inverseGradientColors"
- :opacity="inverseGradientOpacity"
- identifier-name="left-shadow-gradient"
- />
- </svg>
- </div>
-</template>
diff --git a/app/assets/javascripts/vue_shared/components/bar_chart_constants.js b/app/assets/javascripts/vue_shared/components/bar_chart_constants.js
deleted file mode 100644
index 6957b112da6..00000000000
--- a/app/assets/javascripts/vue_shared/components/bar_chart_constants.js
+++ /dev/null
@@ -1,4 +0,0 @@
-export const GRADIENT_COLORS = ['#000', '#a7a7a7'];
-export const GRADIENT_OPACITY = ['0', '0.4'];
-export const INVERSE_GRADIENT_COLORS = ['#a7a7a7', '#000'];
-export const INVERSE_GRADIENT_OPACITY = ['0.4', '0'];
diff --git a/app/models/sentry_issue.rb b/app/models/sentry_issue.rb
index 1325bce6c43..30f4026e633 100644
--- a/app/models/sentry_issue.rb
+++ b/app/models/sentry_issue.rb
@@ -5,13 +5,22 @@ class SentryIssue < ApplicationRecord
validates :issue, uniqueness: true, presence: true
validates :sentry_issue_identifier, presence: true
+ validate :ensure_sentry_issue_identifier_is_unique_per_project
after_create_commit :enqueue_sentry_sync_job
def self.for_project_and_identifier(project, identifier)
joins(:issue)
.where(issues: { project_id: project.id })
- .find_by_sentry_issue_identifier(identifier)
+ .where(sentry_issue_identifier: identifier)
+ .order('issues.created_at').last
+ end
+
+ def ensure_sentry_issue_identifier_is_unique_per_project
+ if issue && self.class.for_project_and_identifier(issue.project, sentry_issue_identifier).present?
+ # Custom message because field is hidden
+ errors.add(:_, _('is already associated to a GitLab Issue. New issue will not be associated.'))
+ end
end
def enqueue_sentry_sync_job
diff --git a/changelogs/unreleased/37174-move-contribution-analytics-chart-to-echarts.yml b/changelogs/unreleased/37174-move-contribution-analytics-chart-to-echarts.yml
new file mode 100644
index 00000000000..fc7e2cf588d
--- /dev/null
+++ b/changelogs/unreleased/37174-move-contribution-analytics-chart-to-echarts.yml
@@ -0,0 +1,5 @@
+---
+title: Move contribution analytics chart to echarts
+merge_request: 24272
+author:
+type: other
diff --git a/changelogs/unreleased/geo-spike-self-service-framework.yml b/changelogs/unreleased/geo-spike-self-service-framework.yml
deleted file mode 100644
index 409bc8a9298..00000000000
--- a/changelogs/unreleased/geo-spike-self-service-framework.yml
+++ /dev/null
@@ -1,5 +0,0 @@
----
-title: 'Geo: Add tables to prepare to replicate package files'
-merge_request: 23447
-author:
-type: added
diff --git a/changelogs/unreleased/mc-bug-fail-upstream-on-invalid-yaml.yml b/changelogs/unreleased/mc-bug-fail-upstream-on-invalid-yaml.yml
new file mode 100644
index 00000000000..910f2851400
--- /dev/null
+++ b/changelogs/unreleased/mc-bug-fail-upstream-on-invalid-yaml.yml
@@ -0,0 +1,5 @@
+---
+title: Fail upstream bridge on downstream pipeline creation failure.
+merge_request: 24092
+author:
+type: fixed
diff --git a/changelogs/unreleased/refactoring-entities-file-11.yml b/changelogs/unreleased/refactoring-entities-file-11.yml
new file mode 100644
index 00000000000..f6002642df5
--- /dev/null
+++ b/changelogs/unreleased/refactoring-entities-file-11.yml
@@ -0,0 +1,5 @@
+---
+title: Separate merge request entities into own class files
+merge_request: 24373
+author: Rajendra Kadam
+type: added
diff --git a/config/application.rb b/config/application.rb
index 5241a195e53..e8cc35aed2a 100644
--- a/config/application.rb
+++ b/config/application.rb
@@ -55,8 +55,6 @@ module Gitlab
memo << ee_path.to_s
end
- ee_paths << "ee/app/replicators"
-
# Eager load should load CE first
config.eager_load_paths.push(*ee_paths)
config.helpers_paths.push "#{config.root}/ee/app/helpers"
diff --git a/config/initializers/0_inflections.rb b/config/initializers/0_inflections.rb
index 5c38859a667..7690eafdc6b 100644
--- a/config/initializers/0_inflections.rb
+++ b/config/initializers/0_inflections.rb
@@ -19,7 +19,6 @@ ActiveSupport::Inflector.inflections do |inflect|
group_view
job_artifact_registry
lfs_object_registry
- package_file_registry
project_auto_devops
project_registry
project_statistics
diff --git a/db/migrate/20200121192942_create_geo_events.rb b/db/migrate/20200121192942_create_geo_events.rb
deleted file mode 100644
index 6dbe131051f..00000000000
--- a/db/migrate/20200121192942_create_geo_events.rb
+++ /dev/null
@@ -1,14 +0,0 @@
-# frozen_string_literal: true
-
-class CreateGeoEvents < ActiveRecord::Migration[5.2]
- DOWNTIME = false
-
- def change
- create_table :geo_events do |t|
- t.string :replicable_name, limit: 255, null: false
- t.string :event_name, limit: 255, null: false
- t.jsonb :payload, default: {}, null: false
- t.datetime_with_timezone :created_at, null: false
- end
- end
-end
diff --git a/db/migrate/20200121194000_add_geo_event_id_to_geo_event_log.rb b/db/migrate/20200121194000_add_geo_event_id_to_geo_event_log.rb
deleted file mode 100644
index 720995164b2..00000000000
--- a/db/migrate/20200121194000_add_geo_event_id_to_geo_event_log.rb
+++ /dev/null
@@ -1,9 +0,0 @@
-# frozen_string_literal: true
-
-class AddGeoEventIdToGeoEventLog < ActiveRecord::Migration[5.2]
- DOWNTIME = false
-
- def change
- add_column :geo_event_log, :geo_event_id, :integer
- end
-end
diff --git a/db/migrate/20200121194048_add_geo_event_id_index_to_geo_event_log.rb b/db/migrate/20200121194048_add_geo_event_id_index_to_geo_event_log.rb
deleted file mode 100644
index 9b0ec12c959..00000000000
--- a/db/migrate/20200121194048_add_geo_event_id_index_to_geo_event_log.rb
+++ /dev/null
@@ -1,20 +0,0 @@
-# frozen_string_literal: true
-
-class AddGeoEventIdIndexToGeoEventLog < ActiveRecord::Migration[5.2]
- include Gitlab::Database::MigrationHelpers
-
- DOWNTIME = false
-
- disable_ddl_transaction!
-
- def up
- add_concurrent_index :geo_event_log, :geo_event_id,
- where: "(geo_event_id IS NOT NULL)",
- using: :btree,
- name: 'index_geo_event_log_on_geo_event_id'
- end
-
- def down
- remove_concurrent_index :geo_event_log, :geo_event_id, name: 'index_geo_event_log_on_geo_event_id'
- end
-end
diff --git a/db/migrate/20200121194154_add_geo_events_foreign_key.rb b/db/migrate/20200121194154_add_geo_events_foreign_key.rb
deleted file mode 100644
index b5e16e22989..00000000000
--- a/db/migrate/20200121194154_add_geo_events_foreign_key.rb
+++ /dev/null
@@ -1,20 +0,0 @@
-# frozen_string_literal: true
-
-class AddGeoEventsForeignKey < ActiveRecord::Migration[5.2]
- include Gitlab::Database::MigrationHelpers
-
- DOWNTIME = false
-
- disable_ddl_transaction!
-
- def up
- add_concurrent_foreign_key :geo_event_log, :geo_events,
- column: :geo_event_id,
- name: 'fk_geo_event_log_on_geo_event_id',
- on_delete: :cascade
- end
-
- def down
- remove_foreign_key_without_error :geo_event_log, column: :geo_event_id, name: 'fk_geo_event_log_on_geo_event_id'
- end
-end
diff --git a/db/schema.rb b/db/schema.rb
index 9617c595a65..258530ac3b3 100644
--- a/db/schema.rb
+++ b/db/schema.rb
@@ -1665,10 +1665,8 @@ ActiveRecord::Schema.define(version: 2020_02_04_131054) do
t.bigint "reset_checksum_event_id"
t.bigint "cache_invalidation_event_id"
t.bigint "container_repository_updated_event_id"
- t.integer "geo_event_id"
t.index ["cache_invalidation_event_id"], name: "index_geo_event_log_on_cache_invalidation_event_id", where: "(cache_invalidation_event_id IS NOT NULL)"
t.index ["container_repository_updated_event_id"], name: "index_geo_event_log_on_container_repository_updated_event_id"
- t.index ["geo_event_id"], name: "index_geo_event_log_on_geo_event_id", where: "(geo_event_id IS NOT NULL)"
t.index ["hashed_storage_attachments_event_id"], name: "index_geo_event_log_on_hashed_storage_attachments_event_id", where: "(hashed_storage_attachments_event_id IS NOT NULL)"
t.index ["hashed_storage_migrated_event_id"], name: "index_geo_event_log_on_hashed_storage_migrated_event_id", where: "(hashed_storage_migrated_event_id IS NOT NULL)"
t.index ["job_artifact_deleted_event_id"], name: "index_geo_event_log_on_job_artifact_deleted_event_id", where: "(job_artifact_deleted_event_id IS NOT NULL)"
@@ -1682,13 +1680,6 @@ ActiveRecord::Schema.define(version: 2020_02_04_131054) do
t.index ["upload_deleted_event_id"], name: "index_geo_event_log_on_upload_deleted_event_id", where: "(upload_deleted_event_id IS NOT NULL)"
end
- create_table "geo_events", force: :cascade do |t|
- t.string "replicable_name", limit: 255, null: false
- t.string "event_name", limit: 255, null: false
- t.jsonb "payload", default: {}, null: false
- t.datetime_with_timezone "created_at", null: false
- end
-
create_table "geo_hashed_storage_attachments_events", force: :cascade do |t|
t.integer "project_id", null: false
t.text "old_attachments_path", null: false
@@ -4649,7 +4640,6 @@ ActiveRecord::Schema.define(version: 2020_02_04_131054) do
add_foreign_key "geo_container_repository_updated_events", "container_repositories", name: "fk_212c89c706", on_delete: :cascade
add_foreign_key "geo_event_log", "geo_cache_invalidation_events", column: "cache_invalidation_event_id", name: "fk_42c3b54bed", on_delete: :cascade
add_foreign_key "geo_event_log", "geo_container_repository_updated_events", column: "container_repository_updated_event_id", name: "fk_6ada82d42a", on_delete: :cascade
- add_foreign_key "geo_event_log", "geo_events", name: "fk_geo_event_log_on_geo_event_id", on_delete: :cascade
add_foreign_key "geo_event_log", "geo_hashed_storage_migrated_events", column: "hashed_storage_migrated_event_id", name: "fk_27548c6db3", on_delete: :cascade
add_foreign_key "geo_event_log", "geo_job_artifact_deleted_events", column: "job_artifact_deleted_event_id", name: "fk_176d3fbb5d", on_delete: :cascade
add_foreign_key "geo_event_log", "geo_lfs_object_deleted_events", column: "lfs_object_deleted_event_id", name: "fk_d5af95fcd9", on_delete: :cascade
diff --git a/doc/administration/monitoring/gitlab_self_monitoring_project/index.md b/doc/administration/monitoring/gitlab_self_monitoring_project/index.md
index 82944d10d52..df109b94d2d 100644
--- a/doc/administration/monitoring/gitlab_self_monitoring_project/index.md
+++ b/doc/administration/monitoring/gitlab_self_monitoring_project/index.md
@@ -17,7 +17,18 @@ All administrators at the time of creation of the project and group will be adde
as maintainers of the group and project, and as an admin, you'll be able to add new
members to the group in order to give them maintainer access to the project.
-This project will be used for self-monitoring your GitLab instance.
+This project will be used for self monitoring your GitLab instance.
+
+## Activating or deactivating the self monitoring project
+
+1. Navigate to **Admin Area > Settings > Metrics and profiling** and expand the **Self monitoring** section.
+1. Toggle on or off the **Create Project** button to create or remove the "GitLab self monitoring" project.
+1. Click **Save changes** for the changes to take effect.
+
+If you activated the monitoring project, it should now be visible in **Projects > Your projects**.
+
+CAUTION: **Warning:**
+If you deactivate the self monitoring project, it will be permanently deleted.
## Connection to Prometheus
diff --git a/doc/api/api_resources.md b/doc/api/api_resources.md
index 6eba9bf23bf..2adc0087474 100644
--- a/doc/api/api_resources.md
+++ b/doc/api/api_resources.md
@@ -56,6 +56,7 @@ The following API resources are available in the project context:
| [Project milestones](milestones.md) | `/projects/:id/milestones` |
| [Project snippets](project_snippets.md) | `/projects/:id/snippets` |
| [Project templates](project_templates.md) | `/projects/:id/templates` |
+| [Protected_environments](protected_environments.md) | `/projects/:id/protected_environments` |
| [Protected branches](protected_branches.md) | `/projects/:id/protected_branches` |
| [Protected tags](protected_tags.md) | `/projects/:id/protected_tags` |
| [Releases](releases/index.md) | `/projects/:id/releases` |
diff --git a/doc/api/protected_environments.md b/doc/api/protected_environments.md
new file mode 100644
index 00000000000..7d4e62a8ff5
--- /dev/null
+++ b/doc/api/protected_environments.md
@@ -0,0 +1,136 @@
+# Protected environments API **(PREMIUM)**
+
+> [Introduced](https://gitlab.com/gitlab-org/gitlab/issues/30595) in [GitLab Premium](https://about.gitlab.com/pricing/) 12.8.
+
+## Valid access levels
+
+The access levels are defined in the `ProtectedEnvironment::DeployAccessLevel::ALLOWED_ACCESS_LEVELS` method.
+Currently, these levels are recognized:
+
+```
+30 => Developer access
+40 => Maintainer access
+60 => Admin access
+```
+
+## List protected environments
+
+Gets a list of protected environments from a project:
+
+```bash
+GET /projects/:id/protected_environments
+```
+
+| Attribute | Type | Required | Description |
+| --------- | ---- | -------- | ----------- |
+| `id` | integer/string | yes | The ID or [URL-encoded path of the project](README.md#namespaced-path-encoding) owned by the authenticated user. |
+
+```bash
+curl --header "PRIVATE-TOKEN: <your_access_token>" 'https://gitlab.example.com/api/v4/projects/5/protected_environments/'
+```
+
+Example response:
+
+```json
+[
+ {
+ "name":"production",
+ "deploy_access_levels":[
+ {
+ "access_level":40,
+ "access_level_description":"Maintainers",
+ "user_id":null,
+ "group_id":null
+ }
+ ]
+ }
+]
+```
+
+## Get a single protected environment
+
+Gets a single protected environment:
+
+```bash
+GET /projects/:id/protected_environments/:name
+```
+
+| Attribute | Type | Required | Description |
+| --------- | ---- | -------- | ----------- |
+| `id` | integer/string | yes | The ID or [URL-encoded path of the project](README.md#namespaced-path-encoding) owned by the authenticated user |
+| `name` | string | yes | The name of the protected environment |
+
+```bash
+curl --header "PRIVATE-TOKEN: <your_access_token>" 'https://gitlab.example.com/api/v4/projects/5/protected_environments/production'
+```
+
+Example response:
+
+```json
+{
+ "name":"production",
+ "deploy_access_levels":[
+ {
+ "access_level":40,
+ "access_level_description":"Maintainers",
+ "user_id":null,
+ "group_id":null
+ }
+ ]
+}
+```
+
+## Protect repository environments
+
+Protects a single environment:
+
+```bash
+POST /projects/:id/protected_environments
+```
+
+```bash
+curl --request POST --header "PRIVATE-TOKEN: <your_access_token>" 'https://gitlab.example.com/api/v4/projects/5/protected_environments?name=staging&deploy_access_levels%5B%5D%5Buser_id%5D=1'
+```
+
+| Attribute | Type | Required | Description |
+| --------- | ---- | -------- | ----------- |
+| `id` | integer/string | yes | The ID or [URL-encoded path of the project](README.md#namespaced-path-encoding) owned by the authenticated user. |
+| `name` | string | yes | The name of the environment. |
+| `deploy_access_levels` | array | yes | Array of access levels allowed to deploy, with each described by a hash. |
+
+Elements in the `deploy_access_levels` array should take the
+form `{user_id: integer}`, `{group_id: integer}` or `{access_level: integer}`.
+Each user must have access to the project and each group must [have this project shared](../user/project/members/share_project_with_groups.md).
+
+Example response:
+
+```json
+{
+ "name":"staging",
+ "deploy_access_levels":[
+ {
+ "access_level":null,
+ "access_level_description":"Administrator",
+ "user_id":1,
+ "group_id":null
+ }
+ ]
+}
+```
+
+## Unprotect environment
+
+Unprotects the given protected environment:
+
+```bash
+DELETE /projects/:id/protected_environments/:name
+```
+
+```bash
+curl --request DELETE --header "PRIVATE-TOKEN: <your_access_token>" 'https://gitlab.example.com/api/v4/projects/5/protected_environments/staging'
+```
+
+| Attribute | Type | Required | Description |
+| --------- | ---- | -------- | ----------- |
+| `id` | integer/string | yes | The ID or [URL-encoded path of the project](README.md#namespaced-path-encoding) owned by the authenticated user. |
+| `name` | string | yes | The name of the protected environment. |
diff --git a/doc/user/application_security/container_scanning/index.md b/doc/user/application_security/container_scanning/index.md
index 25a6c21ac39..b5d11f36080 100644
--- a/doc/user/application_security/container_scanning/index.md
+++ b/doc/user/application_security/container_scanning/index.md
@@ -85,7 +85,7 @@ Add the following to your `.gitlab-ci.yml` file:
```yaml
include:
- template: Container-Scanning.gitlab-ci.yml
+ - template: Container-Scanning.gitlab-ci.yml
```
The included template will:
diff --git a/doc/user/application_security/dast/index.md b/doc/user/application_security/dast/index.md
index 7e318a47f4d..a96a17aa45a 100644
--- a/doc/user/application_security/dast/index.md
+++ b/doc/user/application_security/dast/index.md
@@ -71,7 +71,7 @@ Add the following to your `.gitlab-ci.yml` file:
```yaml
include:
- template: DAST.gitlab-ci.yml
+ - template: DAST.gitlab-ci.yml
variables:
DAST_WEBSITE: https://example.com
@@ -111,7 +111,7 @@ It's also possible to authenticate the user before performing the DAST checks:
```yaml
include:
- template: DAST.gitlab-ci.yml
+ - template: DAST.gitlab-ci.yml
variables:
DAST_WEBSITE: https://example.com
@@ -135,7 +135,7 @@ includes both passive and active scanning against the same target website:
```yaml
include:
- template: DAST.gitlab-ci.yml
+ - template: DAST.gitlab-ci.yml
variables:
DAST_FULL_SCAN_ENABLED: "true"
@@ -151,7 +151,7 @@ Domain validation is not required by default. It can be required by setting the
```yaml
include:
- template: DAST.gitlab-ci.yml
+ - template: DAST.gitlab-ci.yml
variables:
DAST_FULL_SCAN_ENABLED: "true"
@@ -260,7 +260,7 @@ For example:
```yaml
include:
- template: DAST.gitlab-ci.yml
+ - template: DAST.gitlab-ci.yml
variables:
DAST_WEBSITE: https://example.com
@@ -278,7 +278,7 @@ template inclusion and specify any additional keys under it. For example:
```yaml
include:
- template: DAST.gitlab-ci.yml
+ - template: DAST.gitlab-ci.yml
dast:
stage: dast # IMPORTANT: don't forget to add this
@@ -447,7 +447,7 @@ for DAST by overwriting the `script` key in the DAST template:
```yaml
include:
- template: DAST.gitlab-ci.yml
+ - template: DAST.gitlab-ci.yml
dast:
script:
diff --git a/doc/user/application_security/dependency_scanning/index.md b/doc/user/application_security/dependency_scanning/index.md
index 3f725089932..24cf7906848 100644
--- a/doc/user/application_security/dependency_scanning/index.md
+++ b/doc/user/application_security/dependency_scanning/index.md
@@ -79,7 +79,7 @@ Add the following to your `.gitlab-ci.yml` file:
```yaml
include:
- template: Dependency-Scanning.gitlab-ci.yml
+ - template: Dependency-Scanning.gitlab-ci.yml
```
The included template will create a `dependency_scanning` job in your CI/CD
@@ -99,7 +99,7 @@ For example:
```yaml
include:
- template: Dependency-Scanning.gitlab-ci.yml
+ - template: Dependency-Scanning.gitlab-ci.yml
variables:
DS_PYTHON_VERSION: 2
@@ -116,7 +116,7 @@ after the template inclusion and specify any additional keys under it. For examp
```yaml
include:
- template: Dependency-Scanning.gitlab-ci.yml
+ - template: Dependency-Scanning.gitlab-ci.yml
dependency_scanning:
variables:
@@ -187,7 +187,7 @@ This does not require running the executor in privileged mode. For example:
```yaml
include:
- template: Dependency-Scanning.gitlab-ci.yml
+ - template: Dependency-Scanning.gitlab-ci.yml
variables:
DS_DISABLE_DIND: "true"
diff --git a/doc/user/application_security/license_compliance/index.md b/doc/user/application_security/license_compliance/index.md
index 95eec0db7fa..f70c654413e 100644
--- a/doc/user/application_security/license_compliance/index.md
+++ b/doc/user/application_security/license_compliance/index.md
@@ -88,7 +88,7 @@ Add the following to your `.gitlab-ci.yml` file:
```yaml
include:
- template: License-Scanning.gitlab-ci.yml
+ - template: License-Scanning.gitlab-ci.yml
```
The included template will create a `license_scanning` job in your CI/CD pipeline
@@ -141,7 +141,7 @@ For example:
```yaml
include:
- template: License-Scanning.gitlab-ci.yml
+ - template: License-Scanning.gitlab-ci.yml
variables:
LICENSE_MANAGEMENT_SETUP_CMD: sh my-custom-install-script.sh
@@ -158,7 +158,7 @@ after the template inclusion and specify any additional keys under it. For examp
```yaml
include:
- template: License-Scanning.gitlab-ci.yml
+ - template: License-Scanning.gitlab-ci.yml
license_scanning:
variables:
@@ -173,7 +173,7 @@ Feel free to use it for the customization of Maven execution. For example:
```yaml
include:
- template: License-Scanning.gitlab-ci.yml
+ - template: License-Scanning.gitlab-ci.yml
license_scanning:
variables:
@@ -201,7 +201,7 @@ by setting the `LM_PYTHON_VERSION` environment variable to `2`.
```yaml
include:
- template: License-Scanning.gitlab-ci.yml
+ - template: License-Scanning.gitlab-ci.yml
license_scanning:
variables:
@@ -223,7 +223,7 @@ For example, the following `.gitlab-ci.yml`:
```yaml
include:
- template: License-Management.gitlab-ci.yml
+ - template: License-Management.gitlab-ci.yml
license_management:
artifacts:
@@ -235,7 +235,7 @@ Should be changed to:
```yaml
include:
- template: License-Scanning.gitlab-ci.yml
+ - template: License-Scanning.gitlab-ci.yml
license_scanning:
artifacts:
diff --git a/doc/user/application_security/sast/analyzers.md b/doc/user/application_security/sast/analyzers.md
index a42cf7f09ff..c27c61a52e2 100644
--- a/doc/user/application_security/sast/analyzers.md
+++ b/doc/user/application_security/sast/analyzers.md
@@ -49,7 +49,7 @@ In `.gitlab-ci.yml` define:
```yaml
include:
- template: SAST.gitlab-ci.yml
+ - template: SAST.gitlab-ci.yml
variables:
SAST_ANALYZER_IMAGE_PREFIX: my-docker-registry/gl-images
@@ -66,7 +66,7 @@ In `.gitlab-ci.yml` define:
```yaml
include:
- template: SAST.gitlab-ci.yml
+ - template: SAST.gitlab-ci.yml
variables:
SAST_DEFAULT_ANALYZERS: "bandit,flawfinder"
@@ -82,7 +82,7 @@ default analyzers. In `.gitlab-ci.yml` define:
```yaml
include:
- template: SAST.gitlab-ci.yml
+ - template: SAST.gitlab-ci.yml
variables:
SAST_DEFAULT_ANALYZERS: ""
@@ -98,7 +98,7 @@ In `.gitlab-ci.yml` define:
```yaml
include:
- template: SAST.gitlab-ci.yml
+ - template: SAST.gitlab-ci.yml
variables:
SAST_ANALYZER_IMAGES: "my-docker-registry/analyzers/csharp,amy-docker-registry/analyzers/perl"
diff --git a/doc/user/application_security/sast/index.md b/doc/user/application_security/sast/index.md
index 24f28e547b3..ea9c0b85bea 100644
--- a/doc/user/application_security/sast/index.md
+++ b/doc/user/application_security/sast/index.md
@@ -225,7 +225,7 @@ stages:
- test
include:
- template: SAST.gitlab-ci.yml
+ - template: SAST.gitlab-ci.yml
variables:
SAST_DISABLE_DIND: "true"
diff --git a/lib/api/entities.rb b/lib/api/entities.rb
index 2768dc103c4..98ae4af2a19 100644
--- a/lib/api/entities.rb
+++ b/lib/api/entities.rb
@@ -129,178 +129,6 @@ module API
end
end
- class PipelineBasic < Grape::Entity
- expose :id, :sha, :ref, :status
- expose :created_at, :updated_at
-
- expose :web_url do |pipeline, _options|
- Gitlab::Routing.url_helpers.project_pipeline_url(pipeline.project, pipeline)
- end
- end
-
- class MergeRequestSimple < IssuableEntity
- expose :title
- expose :web_url do |merge_request, options|
- Gitlab::UrlBuilder.build(merge_request)
- end
- end
-
- class MergeRequestBasic < IssuableEntity
- expose :merged_by, using: Entities::UserBasic do |merge_request, _options|
- merge_request.metrics&.merged_by
- end
-
- expose :merged_at do |merge_request, _options|
- merge_request.metrics&.merged_at
- end
-
- expose :closed_by, using: Entities::UserBasic do |merge_request, _options|
- merge_request.metrics&.latest_closed_by
- end
-
- expose :closed_at do |merge_request, _options|
- merge_request.metrics&.latest_closed_at
- end
-
- expose :title_html, if: -> (_, options) { options[:render_html] } do |entity|
- MarkupHelper.markdown_field(entity, :title)
- end
- expose :description_html, if: -> (_, options) { options[:render_html] } do |entity|
- MarkupHelper.markdown_field(entity, :description)
- end
- expose :target_branch, :source_branch
- expose(:user_notes_count) { |merge_request, options| issuable_metadata(merge_request, options, :user_notes_count) }
- expose(:upvotes) { |merge_request, options| issuable_metadata(merge_request, options, :upvotes) }
- expose(:downvotes) { |merge_request, options| issuable_metadata(merge_request, options, :downvotes) }
- expose :assignee, using: ::API::Entities::UserBasic do |merge_request|
- merge_request.assignee
- end
- expose :author, :assignees, using: Entities::UserBasic
-
- expose :source_project_id, :target_project_id
- expose :labels do |merge_request, options|
- if options[:with_labels_details]
- ::API::Entities::LabelBasic.represent(merge_request.labels.sort_by(&:title))
- else
- merge_request.labels.map(&:title).sort
- end
- end
- expose :work_in_progress?, as: :work_in_progress
- expose :milestone, using: Entities::Milestone
- expose :merge_when_pipeline_succeeds
-
- # Ideally we should deprecate `MergeRequest#merge_status` exposure and
- # use `MergeRequest#mergeable?` instead (boolean).
- # See https://gitlab.com/gitlab-org/gitlab-foss/issues/42344 for more
- # information.
- expose :merge_status do |merge_request|
- merge_request.check_mergeability(async: true)
- merge_request.merge_status
- end
- expose :diff_head_sha, as: :sha
- expose :merge_commit_sha
- expose :squash_commit_sha
- expose :discussion_locked
- expose :should_remove_source_branch?, as: :should_remove_source_branch
- expose :force_remove_source_branch?, as: :force_remove_source_branch
- expose :allow_collaboration, if: -> (merge_request, _) { merge_request.for_fork? }
- # Deprecated
- expose :allow_collaboration, as: :allow_maintainer_to_push, if: -> (merge_request, _) { merge_request.for_fork? }
-
- # reference is deprecated in favour of references
- # Introduced [Gitlab 12.6](https://gitlab.com/gitlab-org/gitlab/merge_requests/20354)
- expose :reference do |merge_request, options|
- merge_request.to_reference(options[:project])
- end
-
- expose :references, with: IssuableReferences do |merge_request|
- merge_request
- end
-
- expose :web_url do |merge_request|
- Gitlab::UrlBuilder.build(merge_request)
- end
-
- expose :time_stats, using: 'API::Entities::IssuableTimeStats' do |merge_request|
- merge_request
- end
-
- expose :squash
-
- expose :task_completion_status
-
- expose :cannot_be_merged?, as: :has_conflicts
-
- expose :mergeable_discussions_state?, as: :blocking_discussions_resolved
- end
-
- class MergeRequest < MergeRequestBasic
- expose :subscribed, if: -> (_, options) { options.fetch(:include_subscribed, true) } do |merge_request, options|
- merge_request.subscribed?(options[:current_user], options[:project])
- end
-
- expose :changes_count do |merge_request, _options|
- merge_request.merge_request_diff.real_size
- end
-
- expose :latest_build_started_at, if: -> (_, options) { build_available?(options) } do |merge_request, _options|
- merge_request.metrics&.latest_build_started_at
- end
-
- expose :latest_build_finished_at, if: -> (_, options) { build_available?(options) } do |merge_request, _options|
- merge_request.metrics&.latest_build_finished_at
- end
-
- expose :first_deployed_to_production_at, if: -> (_, options) { build_available?(options) } do |merge_request, _options|
- merge_request.metrics&.first_deployed_to_production_at
- end
-
- expose :pipeline, using: Entities::PipelineBasic, if: -> (_, options) { build_available?(options) } do |merge_request, _options|
- merge_request.metrics&.pipeline
- end
-
- expose :head_pipeline, using: 'API::Entities::Pipeline', if: -> (_, options) do
- Ability.allowed?(options[:current_user], :read_pipeline, options[:project])
- end
-
- expose :diff_refs, using: Entities::DiffRefs
-
- # Allow the status of a rebase to be determined
- expose :merge_error
- expose :rebase_in_progress?, as: :rebase_in_progress, if: -> (_, options) { options[:include_rebase_in_progress] }
-
- expose :diverged_commits_count, as: :diverged_commits_count, if: -> (_, options) { options[:include_diverged_commits_count] }
-
- def build_available?(options)
- options[:project]&.feature_available?(:builds, options[:current_user])
- end
-
- expose :user do
- expose :can_merge do |merge_request, options|
- merge_request.can_be_merged_by?(options[:current_user])
- end
- end
- end
-
- class MergeRequestChanges < MergeRequest
- expose :diffs, as: :changes, using: Entities::Diff do |compare, _|
- compare.raw_diffs(limits: false).to_a
- end
- end
-
- class MergeRequestDiff < Grape::Entity
- expose :id, :head_commit_sha, :base_commit_sha, :start_commit_sha,
- :created_at, :merge_request_id, :state, :real_size
- end
-
- class MergeRequestDiffFull < MergeRequestDiff
- expose :commits, using: Entities::Commit
-
- expose :diffs, using: Entities::Diff do |compare, _|
- compare.raw_diffs(limits: false).to_a
- end
- end
-
class SSHKey < Grape::Entity
expose :id, :title, :key, :created_at
end
diff --git a/lib/api/entities/merge_request.rb b/lib/api/entities/merge_request.rb
new file mode 100644
index 00000000000..9ff8e20ced1
--- /dev/null
+++ b/lib/api/entities/merge_request.rb
@@ -0,0 +1,53 @@
+# frozen_string_literal: true
+
+module API
+ module Entities
+ class MergeRequest < MergeRequestBasic
+ expose :subscribed, if: -> (_, options) { options.fetch(:include_subscribed, true) } do |merge_request, options|
+ merge_request.subscribed?(options[:current_user], options[:project])
+ end
+
+ expose :changes_count do |merge_request, _options|
+ merge_request.merge_request_diff.real_size
+ end
+
+ expose :latest_build_started_at, if: -> (_, options) { build_available?(options) } do |merge_request, _options|
+ merge_request.metrics&.latest_build_started_at
+ end
+
+ expose :latest_build_finished_at, if: -> (_, options) { build_available?(options) } do |merge_request, _options|
+ merge_request.metrics&.latest_build_finished_at
+ end
+
+ expose :first_deployed_to_production_at, if: -> (_, options) { build_available?(options) } do |merge_request, _options|
+ merge_request.metrics&.first_deployed_to_production_at
+ end
+
+ expose :pipeline, using: Entities::PipelineBasic, if: -> (_, options) { build_available?(options) } do |merge_request, _options|
+ merge_request.metrics&.pipeline
+ end
+
+ expose :head_pipeline, using: 'API::Entities::Pipeline', if: -> (_, options) do
+ Ability.allowed?(options[:current_user], :read_pipeline, options[:project])
+ end
+
+ expose :diff_refs, using: Entities::DiffRefs
+
+ # Allow the status of a rebase to be determined
+ expose :merge_error
+ expose :rebase_in_progress?, as: :rebase_in_progress, if: -> (_, options) { options[:include_rebase_in_progress] }
+
+ expose :diverged_commits_count, as: :diverged_commits_count, if: -> (_, options) { options[:include_diverged_commits_count] }
+
+ def build_available?(options)
+ options[:project]&.feature_available?(:builds, options[:current_user])
+ end
+
+ expose :user do
+ expose :can_merge do |merge_request, options|
+ merge_request.can_be_merged_by?(options[:current_user])
+ end
+ end
+ end
+ end
+end
diff --git a/lib/api/entities/merge_request_basic.rb b/lib/api/entities/merge_request_basic.rb
new file mode 100644
index 00000000000..ce8bfa9e670
--- /dev/null
+++ b/lib/api/entities/merge_request_basic.rb
@@ -0,0 +1,94 @@
+# frozen_string_literal: true
+
+module API
+ module Entities
+ class MergeRequestBasic < IssuableEntity
+ expose :merged_by, using: Entities::UserBasic do |merge_request, _options|
+ merge_request.metrics&.merged_by
+ end
+
+ expose :merged_at do |merge_request, _options|
+ merge_request.metrics&.merged_at
+ end
+
+ expose :closed_by, using: Entities::UserBasic do |merge_request, _options|
+ merge_request.metrics&.latest_closed_by
+ end
+
+ expose :closed_at do |merge_request, _options|
+ merge_request.metrics&.latest_closed_at
+ end
+
+ expose :title_html, if: -> (_, options) { options[:render_html] } do |entity|
+ MarkupHelper.markdown_field(entity, :title)
+ end
+ expose :description_html, if: -> (_, options) { options[:render_html] } do |entity|
+ MarkupHelper.markdown_field(entity, :description)
+ end
+ expose :target_branch, :source_branch
+ expose(:user_notes_count) { |merge_request, options| issuable_metadata(merge_request, options, :user_notes_count) }
+ expose(:upvotes) { |merge_request, options| issuable_metadata(merge_request, options, :upvotes) }
+ expose(:downvotes) { |merge_request, options| issuable_metadata(merge_request, options, :downvotes) }
+ expose :assignee, using: ::API::Entities::UserBasic do |merge_request|
+ merge_request.assignee
+ end
+ expose :author, :assignees, using: Entities::UserBasic
+
+ expose :source_project_id, :target_project_id
+ expose :labels do |merge_request, options|
+ if options[:with_labels_details]
+ ::API::Entities::LabelBasic.represent(merge_request.labels.sort_by(&:title))
+ else
+ merge_request.labels.map(&:title).sort
+ end
+ end
+ expose :work_in_progress?, as: :work_in_progress
+ expose :milestone, using: Entities::Milestone
+ expose :merge_when_pipeline_succeeds
+
+ # Ideally we should deprecate `MergeRequest#merge_status` exposure and
+ # use `MergeRequest#mergeable?` instead (boolean).
+ # See https://gitlab.com/gitlab-org/gitlab-foss/issues/42344 for more
+ # information.
+ expose :merge_status do |merge_request|
+ merge_request.check_mergeability(async: true)
+ merge_request.merge_status
+ end
+ expose :diff_head_sha, as: :sha
+ expose :merge_commit_sha
+ expose :squash_commit_sha
+ expose :discussion_locked
+ expose :should_remove_source_branch?, as: :should_remove_source_branch
+ expose :force_remove_source_branch?, as: :force_remove_source_branch
+ expose :allow_collaboration, if: -> (merge_request, _) { merge_request.for_fork? }
+ # Deprecated
+ expose :allow_collaboration, as: :allow_maintainer_to_push, if: -> (merge_request, _) { merge_request.for_fork? }
+
+ # reference is deprecated in favour of references
+ # Introduced [Gitlab 12.6](https://gitlab.com/gitlab-org/gitlab/merge_requests/20354)
+ expose :reference do |merge_request, options|
+ merge_request.to_reference(options[:project])
+ end
+
+ expose :references, with: IssuableReferences do |merge_request|
+ merge_request
+ end
+
+ expose :web_url do |merge_request|
+ Gitlab::UrlBuilder.build(merge_request)
+ end
+
+ expose :time_stats, using: 'API::Entities::IssuableTimeStats' do |merge_request|
+ merge_request
+ end
+
+ expose :squash
+
+ expose :task_completion_status
+
+ expose :cannot_be_merged?, as: :has_conflicts
+
+ expose :mergeable_discussions_state?, as: :blocking_discussions_resolved
+ end
+ end
+end
diff --git a/lib/api/entities/merge_request_changes.rb b/lib/api/entities/merge_request_changes.rb
new file mode 100644
index 00000000000..a835d119736
--- /dev/null
+++ b/lib/api/entities/merge_request_changes.rb
@@ -0,0 +1,11 @@
+# frozen_string_literal: true
+
+module API
+ module Entities
+ class MergeRequestChanges < MergeRequest
+ expose :diffs, as: :changes, using: Entities::Diff do |compare, _|
+ compare.raw_diffs(limits: false).to_a
+ end
+ end
+ end
+end
diff --git a/lib/api/entities/merge_request_diff.rb b/lib/api/entities/merge_request_diff.rb
new file mode 100644
index 00000000000..3eda1400855
--- /dev/null
+++ b/lib/api/entities/merge_request_diff.rb
@@ -0,0 +1,10 @@
+# frozen_string_literal: true
+
+module API
+ module Entities
+ class MergeRequestDiff < Grape::Entity
+ expose :id, :head_commit_sha, :base_commit_sha, :start_commit_sha,
+ :created_at, :merge_request_id, :state, :real_size
+ end
+ end
+end
diff --git a/lib/api/entities/merge_request_diff_full.rb b/lib/api/entities/merge_request_diff_full.rb
new file mode 100644
index 00000000000..772b9b6822c
--- /dev/null
+++ b/lib/api/entities/merge_request_diff_full.rb
@@ -0,0 +1,13 @@
+# frozen_string_literal: true
+
+module API
+ module Entities
+ class MergeRequestDiffFull < MergeRequestDiff
+ expose :commits, using: Entities::Commit
+
+ expose :diffs, using: Entities::Diff do |compare, _|
+ compare.raw_diffs(limits: false).to_a
+ end
+ end
+ end
+end
diff --git a/lib/api/entities/merge_request_simple.rb b/lib/api/entities/merge_request_simple.rb
new file mode 100644
index 00000000000..f3ff4cc18a8
--- /dev/null
+++ b/lib/api/entities/merge_request_simple.rb
@@ -0,0 +1,12 @@
+# frozen_string_literal: true
+
+module API
+ module Entities
+ class MergeRequestSimple < IssuableEntity
+ expose :title
+ expose :web_url do |merge_request, options|
+ Gitlab::UrlBuilder.build(merge_request)
+ end
+ end
+ end
+end
diff --git a/lib/api/entities/pipeline_basic.rb b/lib/api/entities/pipeline_basic.rb
new file mode 100644
index 00000000000..359f6a447ab
--- /dev/null
+++ b/lib/api/entities/pipeline_basic.rb
@@ -0,0 +1,14 @@
+# frozen_string_literal: true
+
+module API
+ module Entities
+ class PipelineBasic < Grape::Entity
+ expose :id, :sha, :ref, :status
+ expose :created_at, :updated_at
+
+ expose :web_url do |pipeline, _options|
+ Gitlab::Routing.url_helpers.project_pipeline_url(pipeline.project, pipeline)
+ end
+ end
+ end
+end
diff --git a/lib/quality/test_level.rb b/lib/quality/test_level.rb
index 022b42e489d..84470a73b1b 100644
--- a/lib/quality/test_level.rb
+++ b/lib/quality/test_level.rb
@@ -27,7 +27,6 @@ module Quality
policies
presenters
rack_servers
- replicators
routing
rubocop
serializers
diff --git a/locale/gitlab.pot b/locale/gitlab.pot
index 773b4ff88a6..4dd70043fd0 100644
--- a/locale/gitlab.pot
+++ b/locale/gitlab.pot
@@ -22692,6 +22692,9 @@ msgstr ""
msgid "is"
msgstr ""
+msgid "is already associated to a GitLab Issue. New issue will not be associated."
+msgstr ""
+
msgid "is an invalid IP address range"
msgstr ""
diff --git a/qa/qa/specs/features/browser_ui/2_plan/email/trigger_email_notification_spec.rb b/qa/qa/specs/features/browser_ui/2_plan/email/trigger_email_notification_spec.rb
index eecf485a518..bbe0770444a 100644
--- a/qa/qa/specs/features/browser_ui/2_plan/email/trigger_email_notification_spec.rb
+++ b/qa/qa/specs/features/browser_ui/2_plan/email/trigger_email_notification_spec.rb
@@ -1,7 +1,7 @@
# frozen_string_literal: true
module QA
- context 'Plan', :orchestrated, :smtp do
+ context 'Plan', :orchestrated, :smtp, :reliable do
describe 'Email Notification' do
let(:user) do
Resource::User.fabricate_or_use(Runtime::Env.gitlab_qa_username_1, Runtime::Env.gitlab_qa_password_1)
diff --git a/qa/qa/specs/features/browser_ui/2_plan/issue/check_mentions_for_xss_spec.rb b/qa/qa/specs/features/browser_ui/2_plan/issue/check_mentions_for_xss_spec.rb
index aa88937504e..381606e1648 100644
--- a/qa/qa/specs/features/browser_ui/2_plan/issue/check_mentions_for_xss_spec.rb
+++ b/qa/qa/specs/features/browser_ui/2_plan/issue/check_mentions_for_xss_spec.rb
@@ -1,7 +1,7 @@
# frozen_string_literal: true
module QA
- context 'Plan' do
+ context 'Plan', :reliable do
describe 'check xss occurence in @mentions in issues', :requires_admin do
it 'user mentions a user in comment' do
QA::Runtime::Env.personal_access_token = QA::Runtime::Env.admin_personal_access_token
diff --git a/qa/qa/specs/features/browser_ui/2_plan/issue/close_issue_spec.rb b/qa/qa/specs/features/browser_ui/2_plan/issue/close_issue_spec.rb
index 2543c0091fb..fd00e010b85 100644
--- a/qa/qa/specs/features/browser_ui/2_plan/issue/close_issue_spec.rb
+++ b/qa/qa/specs/features/browser_ui/2_plan/issue/close_issue_spec.rb
@@ -1,7 +1,7 @@
# frozen_string_literal: true
module QA
- context 'Plan' do
+ context 'Plan', :reliable do
describe 'Close issue' do
let(:issue) do
Resource::Issue.fabricate_via_api!
diff --git a/qa/qa/specs/features/browser_ui/2_plan/issue/collapse_comments_in_discussions_spec.rb b/qa/qa/specs/features/browser_ui/2_plan/issue/collapse_comments_in_discussions_spec.rb
index e505c0991a6..a2b7904a84d 100644
--- a/qa/qa/specs/features/browser_ui/2_plan/issue/collapse_comments_in_discussions_spec.rb
+++ b/qa/qa/specs/features/browser_ui/2_plan/issue/collapse_comments_in_discussions_spec.rb
@@ -1,7 +1,7 @@
# frozen_string_literal: true
module QA
- context 'Plan' do
+ context 'Plan', :reliable do
describe 'collapse comments in issue discussions' do
let(:my_first_reply) { 'My first reply' }
diff --git a/qa/qa/specs/features/browser_ui/2_plan/issue/comment_issue_spec.rb b/qa/qa/specs/features/browser_ui/2_plan/issue/comment_issue_spec.rb
index 6c37e3ecbb9..c4a8d2c4e20 100644
--- a/qa/qa/specs/features/browser_ui/2_plan/issue/comment_issue_spec.rb
+++ b/qa/qa/specs/features/browser_ui/2_plan/issue/comment_issue_spec.rb
@@ -1,7 +1,7 @@
# frozen_string_literal: true
module QA
- context 'Plan' do
+ context 'Plan', :reliable do
describe 'Issue comments' do
before do
Flow::Login.sign_in
diff --git a/qa/qa/specs/features/browser_ui/2_plan/issue/create_issue_spec.rb b/qa/qa/specs/features/browser_ui/2_plan/issue/create_issue_spec.rb
index 3b231b9930e..0ea81483a66 100644
--- a/qa/qa/specs/features/browser_ui/2_plan/issue/create_issue_spec.rb
+++ b/qa/qa/specs/features/browser_ui/2_plan/issue/create_issue_spec.rb
@@ -1,7 +1,7 @@
# frozen_string_literal: true
module QA
- context 'Plan', :smoke do
+ context 'Plan', :smoke, :reliable do
describe 'Issue creation' do
before do
Flow::Login.sign_in
diff --git a/qa/qa/specs/features/browser_ui/2_plan/issue/filter_issue_comments_spec.rb b/qa/qa/specs/features/browser_ui/2_plan/issue/filter_issue_comments_spec.rb
index 4156ba54785..f1013d38a1f 100644
--- a/qa/qa/specs/features/browser_ui/2_plan/issue/filter_issue_comments_spec.rb
+++ b/qa/qa/specs/features/browser_ui/2_plan/issue/filter_issue_comments_spec.rb
@@ -1,7 +1,7 @@
# frozen_string_literal: true
module QA
- context 'Plan' do
+ context 'Plan', :reliable do
describe 'filter issue comments activities' do
before do
Flow::Login.sign_in
diff --git a/qa/qa/specs/features/browser_ui/2_plan/issue/issue_suggestions_spec.rb b/qa/qa/specs/features/browser_ui/2_plan/issue/issue_suggestions_spec.rb
index 9a1d13cf677..6c851fd8141 100644
--- a/qa/qa/specs/features/browser_ui/2_plan/issue/issue_suggestions_spec.rb
+++ b/qa/qa/specs/features/browser_ui/2_plan/issue/issue_suggestions_spec.rb
@@ -1,7 +1,7 @@
# frozen_string_literal: true
module QA
- context 'Plan' do
+ context 'Plan', :reliable do
describe 'issue suggestions' do
let(:issue_title) { 'Issue Lists are awesome' }
diff --git a/qa/qa/specs/features/browser_ui/2_plan/issue/mentions_spec.rb b/qa/qa/specs/features/browser_ui/2_plan/issue/mentions_spec.rb
index a0647df4097..8760f5cac04 100644
--- a/qa/qa/specs/features/browser_ui/2_plan/issue/mentions_spec.rb
+++ b/qa/qa/specs/features/browser_ui/2_plan/issue/mentions_spec.rb
@@ -1,7 +1,7 @@
# frozen_string_literal: true
module QA
- context 'Plan', :smoke do
+ context 'Plan', :smoke, :reliable do
describe 'mention' do
before do
Flow::Login.sign_in
diff --git a/spec/frontend/environments/environment_actions_spec.js b/spec/frontend/environments/environment_actions_spec.js
new file mode 100644
index 00000000000..4c06e19cec0
--- /dev/null
+++ b/spec/frontend/environments/environment_actions_spec.js
@@ -0,0 +1,124 @@
+import { shallowMount } from '@vue/test-utils';
+import { TEST_HOST } from 'helpers/test_constants';
+import eventHub from '~/environments/event_hub';
+import EnvironmentActions from '~/environments/components/environment_actions.vue';
+import Icon from '~/vue_shared/components/icon.vue';
+import { GlLoadingIcon } from '@gitlab/ui';
+
+describe('EnvironmentActions Component', () => {
+ let vm;
+
+ beforeEach(() => {
+ vm = shallowMount(EnvironmentActions, { propsData: { actions: [] } });
+ });
+
+ afterEach(() => {
+ vm.destroy();
+ });
+
+ it('should render a dropdown button with 2 icons', () => {
+ expect(vm.find('.dropdown-new').findAll(Icon).length).toBe(2);
+ });
+
+ it('should render a dropdown button with aria-label description', () => {
+ expect(vm.find('.dropdown-new').attributes('aria-label')).toEqual('Deploy to...');
+ });
+
+ describe('is loading', () => {
+ beforeEach(() => {
+ vm.setData({ isLoading: true });
+ });
+
+ it('should render a dropdown button with a loading icon', () => {
+ expect(vm.findAll(GlLoadingIcon).length).toBe(1);
+ });
+ });
+
+ describe('manual actions', () => {
+ const actions = [
+ {
+ name: 'bar',
+ play_path: 'https://gitlab.com/play',
+ },
+ {
+ name: 'foo',
+ play_path: '#',
+ },
+ {
+ name: 'foo bar',
+ play_path: 'url',
+ playable: false,
+ },
+ ];
+
+ beforeEach(() => {
+ vm.setProps({ actions });
+ });
+
+ it('should render a dropdown with the provided list of actions', () => {
+ expect(vm.findAll('.dropdown-menu li').length).toEqual(actions.length);
+ });
+
+ it("should render a disabled action when it's not playable", () => {
+ expect(vm.find('.dropdown-menu li:last-child button').attributes('disabled')).toEqual(
+ 'disabled',
+ );
+
+ expect(vm.find('.dropdown-menu li:last-child button').classes('disabled')).toBe(true);
+ });
+ });
+
+ describe('scheduled jobs', () => {
+ const scheduledJobAction = {
+ name: 'scheduled action',
+ playPath: `${TEST_HOST}/scheduled/job/action`,
+ playable: true,
+ scheduledAt: '2063-04-05T00:42:00Z',
+ };
+ const expiredJobAction = {
+ name: 'expired action',
+ playPath: `${TEST_HOST}/expired/job/action`,
+ playable: true,
+ scheduledAt: '2018-10-05T08:23:00Z',
+ };
+ const findDropdownItem = action => {
+ const buttons = vm.findAll('.dropdown-menu li button');
+ return buttons.filter(button => button.text().startsWith(action.name)).at(0);
+ };
+
+ beforeEach(() => {
+ jest.spyOn(Date, 'now').mockImplementation(() => new Date('2063-04-04T00:42:00Z').getTime());
+ vm.setProps({ actions: [scheduledJobAction, expiredJobAction] });
+ });
+
+ it('emits postAction event after confirming', () => {
+ const emitSpy = jest.fn();
+ eventHub.$on('postAction', emitSpy);
+ jest.spyOn(window, 'confirm').mockImplementation(() => true);
+
+ findDropdownItem(scheduledJobAction).trigger('click');
+
+ expect(window.confirm).toHaveBeenCalled();
+ expect(emitSpy).toHaveBeenCalledWith({ endpoint: scheduledJobAction.playPath });
+ });
+
+ it('does not emit postAction event if confirmation is cancelled', () => {
+ const emitSpy = jest.fn();
+ eventHub.$on('postAction', emitSpy);
+ jest.spyOn(window, 'confirm').mockImplementation(() => false);
+
+ findDropdownItem(scheduledJobAction).trigger('click');
+
+ expect(window.confirm).toHaveBeenCalled();
+ expect(emitSpy).not.toHaveBeenCalled();
+ });
+
+ it('displays the remaining time in the dropdown', () => {
+ expect(findDropdownItem(scheduledJobAction).text()).toContain('24:00:00');
+ });
+
+ it('displays 00:00:00 for expired jobs in the dropdown', () => {
+ expect(findDropdownItem(expiredJobAction).text()).toContain('00:00:00');
+ });
+ });
+});
diff --git a/spec/frontend/repository/router_spec.js b/spec/frontend/repository/router_spec.js
index 1efd74a30c2..8f3ac53c37a 100644
--- a/spec/frontend/repository/router_spec.js
+++ b/spec/frontend/repository/router_spec.js
@@ -6,6 +6,7 @@ describe('Repository router spec', () => {
it.each`
path | component | componentName
${'/'} | ${IndexPage} | ${'IndexPage'}
+ ${'/tree/master'} | ${TreePage} | ${'TreePage'}
${'/-/tree/master'} | ${TreePage} | ${'TreePage'}
${'/-/tree/master/app/assets'} | ${TreePage} | ${'TreePage'}
${'/-/tree/123/app/assets'} | ${null} | ${'null'}
diff --git a/spec/javascripts/environments/environment_actions_spec.js b/spec/javascripts/environments/environment_actions_spec.js
deleted file mode 100644
index a844660f7bf..00000000000
--- a/spec/javascripts/environments/environment_actions_spec.js
+++ /dev/null
@@ -1,117 +0,0 @@
-import Vue from 'vue';
-import mountComponent from 'spec/helpers/vue_mount_component_helper';
-import { TEST_HOST } from 'spec/test_constants';
-import eventHub from '~/environments/event_hub';
-import EnvironmentActions from '~/environments/components/environment_actions.vue';
-
-describe('EnvironmentActions Component', () => {
- const Component = Vue.extend(EnvironmentActions);
- let vm;
-
- afterEach(() => {
- vm.$destroy();
- });
-
- describe('manual actions', () => {
- const actions = [
- {
- name: 'bar',
- play_path: 'https://gitlab.com/play',
- },
- {
- name: 'foo',
- play_path: '#',
- },
- {
- name: 'foo bar',
- play_path: 'url',
- playable: false,
- },
- ];
-
- beforeEach(() => {
- vm = mountComponent(Component, { actions });
- });
-
- it('should render a dropdown button with icon and title attribute', () => {
- expect(vm.$el.querySelector('.fa-caret-down')).toBeDefined();
- expect(vm.$el.querySelector('.dropdown-new').getAttribute('data-original-title')).toEqual(
- 'Deploy to...',
- );
-
- expect(vm.$el.querySelector('.dropdown-new').getAttribute('aria-label')).toEqual(
- 'Deploy to...',
- );
- });
-
- it('should render a dropdown with the provided list of actions', () => {
- expect(vm.$el.querySelectorAll('.dropdown-menu li').length).toEqual(actions.length);
- });
-
- it("should render a disabled action when it's not playable", () => {
- expect(
- vm.$el.querySelector('.dropdown-menu li:last-child button').getAttribute('disabled'),
- ).toEqual('disabled');
-
- expect(
- vm.$el.querySelector('.dropdown-menu li:last-child button').classList.contains('disabled'),
- ).toEqual(true);
- });
- });
-
- describe('scheduled jobs', () => {
- const scheduledJobAction = {
- name: 'scheduled action',
- playPath: `${TEST_HOST}/scheduled/job/action`,
- playable: true,
- scheduledAt: '2063-04-05T00:42:00Z',
- };
- const expiredJobAction = {
- name: 'expired action',
- playPath: `${TEST_HOST}/expired/job/action`,
- playable: true,
- scheduledAt: '2018-10-05T08:23:00Z',
- };
- const findDropdownItem = action => {
- const buttons = vm.$el.querySelectorAll('.dropdown-menu li button');
- return Array.prototype.find.call(buttons, element =>
- element.innerText.trim().startsWith(action.name),
- );
- };
-
- beforeEach(() => {
- spyOn(Date, 'now').and.callFake(() => new Date('2063-04-04T00:42:00Z').getTime());
- vm = mountComponent(Component, { actions: [scheduledJobAction, expiredJobAction] });
- });
-
- it('emits postAction event after confirming', () => {
- const emitSpy = jasmine.createSpy('emit');
- eventHub.$on('postAction', emitSpy);
- spyOn(window, 'confirm').and.callFake(() => true);
-
- findDropdownItem(scheduledJobAction).click();
-
- expect(window.confirm).toHaveBeenCalled();
- expect(emitSpy).toHaveBeenCalledWith({ endpoint: scheduledJobAction.playPath });
- });
-
- it('does not emit postAction event if confirmation is cancelled', () => {
- const emitSpy = jasmine.createSpy('emit');
- eventHub.$on('postAction', emitSpy);
- spyOn(window, 'confirm').and.callFake(() => false);
-
- findDropdownItem(scheduledJobAction).click();
-
- expect(window.confirm).toHaveBeenCalled();
- expect(emitSpy).not.toHaveBeenCalled();
- });
-
- it('displays the remaining time in the dropdown', () => {
- expect(findDropdownItem(scheduledJobAction)).toContainText('24:00:00');
- });
-
- it('displays 00:00:00 for expired jobs in the dropdown', () => {
- expect(findDropdownItem(expiredJobAction)).toContainText('00:00:00');
- });
- });
-});
diff --git a/spec/javascripts/vue_shared/components/bar_chart_spec.js b/spec/javascripts/vue_shared/components/bar_chart_spec.js
deleted file mode 100644
index 8f673c146ec..00000000000
--- a/spec/javascripts/vue_shared/components/bar_chart_spec.js
+++ /dev/null
@@ -1,79 +0,0 @@
-import Vue from 'vue';
-import mountComponent from 'spec/helpers/vue_mount_component_helper';
-import BarChart from '~/vue_shared/components/bar_chart.vue';
-
-function getRandomArbitrary(min, max) {
- return Math.random() * (max - min) + min;
-}
-
-function generateRandomData(dataNumber) {
- const randomGraphData = [];
-
- for (let i = 1; i <= dataNumber; i += 1) {
- randomGraphData.push({
- name: `random ${i}`,
- value: parseInt(getRandomArbitrary(1, 8), 10),
- });
- }
-
- return randomGraphData;
-}
-
-describe('Bar chart component', () => {
- let barChart;
- const graphData = generateRandomData(10);
-
- beforeEach(() => {
- const BarChartComponent = Vue.extend(BarChart);
-
- barChart = mountComponent(BarChartComponent, {
- graphData,
- yAxisLabel: 'data',
- });
- });
-
- afterEach(() => {
- barChart.$destroy();
- });
-
- it('calculates the padding for even distribution across bars', () => {
- barChart.vbWidth = 1000;
- const result = barChart.calculatePadding(30);
-
- // since padding can't be higher than 1 and lower than 0
- // for more info: https://github.com/d3/d3-scale#band-scales
- expect(result).not.toBeLessThan(0);
- expect(result).not.toBeGreaterThan(1);
- });
-
- it('formats the tooltip title', () => {
- const tooltipTitle = barChart.setTooltipTitle(barChart.graphData[0]);
-
- expect(tooltipTitle).toContain('random 1:');
- });
-
- it('has a translates the bar graphs on across the X axis', () => {
- barChart.panX = 100;
-
- expect(barChart.barTranslationTransform).toEqual('translate(100, 0)');
- });
-
- it('translates the scroll indicator to the far right side', () => {
- barChart.vbWidth = 500;
-
- expect(barChart.scrollIndicatorTransform).toEqual('translate(420, 0)');
- });
-
- it('translates the x-axis to the bottom of the viewbox and pan coordinates', () => {
- barChart.panX = 100;
- barChart.vbHeight = 250;
-
- expect(barChart.xAxisLocation).toEqual('translate(100, 250)');
- });
-
- it('rotates the x axis labels a total of 90 degress (CCW)', () => {
- const xAxisLabel = barChart.$el.querySelector('.x-axis').querySelectorAll('text')[0];
-
- expect(xAxisLabel.getAttribute('transform')).toEqual('rotate(-90)');
- });
-});
diff --git a/spec/lib/quality/test_level_spec.rb b/spec/lib/quality/test_level_spec.rb
index 621a426a18d..13817bdcc72 100644
--- a/spec/lib/quality/test_level_spec.rb
+++ b/spec/lib/quality/test_level_spec.rb
@@ -21,7 +21,7 @@ RSpec.describe Quality::TestLevel do
context 'when level is unit' do
it 'returns a pattern' do
expect(subject.pattern(:unit))
- .to eq("spec/{bin,config,db,dependencies,factories,finders,frontend,graphql,haml_lint,helpers,initializers,javascripts,lib,models,policies,presenters,rack_servers,replicators,routing,rubocop,serializers,services,sidekiq,tasks,uploaders,validators,views,workers,elastic_integration}{,/**/}*_spec.rb")
+ .to eq("spec/{bin,config,db,dependencies,factories,finders,frontend,graphql,haml_lint,helpers,initializers,javascripts,lib,models,policies,presenters,rack_servers,routing,rubocop,serializers,services,sidekiq,tasks,uploaders,validators,views,workers,elastic_integration}{,/**/}*_spec.rb")
end
end
@@ -82,7 +82,7 @@ RSpec.describe Quality::TestLevel do
context 'when level is unit' do
it 'returns a regexp' do
expect(subject.regexp(:unit))
- .to eq(%r{spec/(bin|config|db|dependencies|factories|finders|frontend|graphql|haml_lint|helpers|initializers|javascripts|lib|models|policies|presenters|rack_servers|replicators|routing|rubocop|serializers|services|sidekiq|tasks|uploaders|validators|views|workers|elastic_integration)})
+ .to eq(%r{spec/(bin|config|db|dependencies|factories|finders|frontend|graphql|haml_lint|helpers|initializers|javascripts|lib|models|policies|presenters|rack_servers|routing|rubocop|serializers|services|sidekiq|tasks|uploaders|validators|views|workers|elastic_integration)})
end
end
diff --git a/spec/models/sentry_issue_spec.rb b/spec/models/sentry_issue_spec.rb
index 022cfd4734e..b4c1cf57761 100644
--- a/spec/models/sentry_issue_spec.rb
+++ b/spec/models/sentry_issue_spec.rb
@@ -13,6 +13,20 @@ describe SentryIssue do
it { is_expected.to validate_presence_of(:issue) }
it { is_expected.to validate_uniqueness_of(:issue) }
it { is_expected.to validate_presence_of(:sentry_issue_identifier) }
+
+ it 'allows duplicated sentry_issue_identifier' do
+ duplicate_sentry_issue = build(:sentry_issue, sentry_issue_identifier: sentry_issue.sentry_issue_identifier)
+
+ expect(duplicate_sentry_issue).to be_valid
+ end
+
+ it 'validates uniqueness of sentry_issue_identifier per project' do
+ second_issue = create(:issue, project: sentry_issue.issue.project)
+ duplicate_sentry_issue = build(:sentry_issue, issue: second_issue, sentry_issue_identifier: sentry_issue.sentry_issue_identifier)
+
+ expect(duplicate_sentry_issue).to be_invalid
+ expect(duplicate_sentry_issue.errors.full_messages.first).to include('is already associated')
+ end
end
describe 'callbacks' do
@@ -28,13 +42,16 @@ describe SentryIssue do
end
describe '.for_project_and_identifier' do
- let!(:sentry_issue) { create(:sentry_issue) }
- let(:project) { sentry_issue.issue.project }
- let(:identifier) { sentry_issue.sentry_issue_identifier }
- let!(:second_sentry_issue) { create(:sentry_issue) }
+ it 'finds the most recent per project and sentry_issue_identifier' do
+ sentry_issue = create(:sentry_issue)
+ create(:sentry_issue)
+ project = sentry_issue.issue.project
+ sentry_issue_3 = build(:sentry_issue, issue: create(:issue, project: project), sentry_issue_identifier: sentry_issue.sentry_issue_identifier)
+ sentry_issue_3.save(validate: false)
- subject { described_class.for_project_and_identifier(project, identifier) }
+ result = described_class.for_project_and_identifier(project, sentry_issue.sentry_issue_identifier)
- it { is_expected.to eq(sentry_issue) }
+ expect(result).to eq(sentry_issue_3)
+ end
end
end