diff options
111 files changed, 2394 insertions, 1130 deletions
diff --git a/.gitlab/merge_request_templates/Documentation.md b/.gitlab/merge_request_templates/Documentation.md index 102eb7e7953..da38a703c3c 100644 --- a/.gitlab/merge_request_templates/Documentation.md +++ b/.gitlab/merge_request_templates/Documentation.md @@ -1,16 +1,29 @@ -See the general Documentation guidelines http://docs.gitlab.com/ce/development/doc_styleguide.html +<!--See the general Documentation guidelines https://docs.gitlab.com/ce/development/writing_documentation.html --> ## What does this MR do? -(briefly describe what this MR is about) +<!-- Briefly describe what this MR is about --> + +## Related issues + +<!-- Mention the issue(s) this MR closes or is related to --> + +Closes ## Moving docs to a new location? -See the guidelines: http://docs.gitlab.com/ce/development/doc_styleguide.html#changing-document-location +Read the guidelines: +https://docs.gitlab.com/ce/development/writing_documentation.html#changing-document-location -- [ ] Make sure the old link is not removed and has its contents replaced with a link to the new location. +- [ ] Make sure the old link is not removed and has its contents replaced with + a link to the new location. - [ ] Make sure internal links pointing to the document in question are not broken. -- [ ] Search and replace any links referring to old docs in GitLab Rails app, specifically under the `app/views/` directory. -- [ ] Make sure to add [`redirect_from`](https://docs.gitlab.com/ee/development/doc_styleguide.html#redirections-for-pages-with-disqus-comments) to the new document if there are any Disqus comments on the old document thread. -- [ ] If working on CE, submit an MR to EE with the changes as well. +- [ ] Search and replace any links referring to old docs in GitLab Rails app, + specifically under the `app/views/` and `ee/app/views` (for GitLab EE) directories. +- [ ] Make sure to add [`redirect_from`](https://docs.gitlab.com/ce/development/writing_documentation.html#redirections-for-pages-with-disqus-comments) + to the new document if there are any Disqus comments on the old document thread. +- [ ] If working on CE and the `ee-compat-check` jobs fails, submit an MR to EE + with the changes as well (https://docs.gitlab.com/ce/development/writing_documentation.html#cherry-picking-from-ce-to-ee). - [ ] Ping one of the technical writers for review. + +/label ~Documentation diff --git a/CHANGELOG.md b/CHANGELOG.md index 4426cd20732..adb0ec9f5b1 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,26 @@ documentation](doc/development/changelog.md) for instructions on adding your own entry. +## 10.6.1 (2018-03-27) + +### Security (1 change) + +- Bump rails-html-sanitizer to 1.0.4. + +### Fixed (2 changes) + +- Prevent auto-retry AccessDenied error from stopping transition to failed. !17862 +- Fix 500 error when trying to resolve non-ASCII conflicts in the editor. !17962 + +### Performance (1 change) + +- Add indexes for user activity queries. !17890 + +### Other (1 change) + +- Add documentation for runner IP address (#44232). !17837 + + ## 10.6.0 (2018-03-22) ### Security (4 changes) @@ -118,9 +118,9 @@ gem 'carrierwave', '~> 1.2' gem 'dropzonejs-rails', '~> 0.7.1' # for backups -gem 'fog-aws', '~> 2.0' +gem 'fog-aws', '~> 2.0.1' gem 'fog-core', '~> 1.44' -gem 'fog-google', '~> 0.5' +gem 'fog-google', '~> 1.3.3' gem 'fog-local', '~> 0.3' gem 'fog-openstack', '~> 0.1' gem 'fog-rackspace', '~> 0.1.1' @@ -146,8 +146,8 @@ gem 'rdoc', '~> 4.2' gem 'org-ruby', '~> 0.9.12' gem 'creole', '~> 0.5.0' gem 'wikicloth', '0.8.1' -gem 'asciidoctor', '~> 1.5.2' -gem 'asciidoctor-plantuml', '0.0.7' +gem 'asciidoctor', '~> 1.5.6' +gem 'asciidoctor-plantuml', '0.0.8' gem 'rouge', '~> 2.0' gem 'truncato', '~> 0.7.9' gem 'bootstrap_form', '~> 2.7.0' diff --git a/Gemfile.lock b/Gemfile.lock index fcffe45acbf..7d8b22359b2 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -56,8 +56,8 @@ GEM faraday_middleware (~> 0.9) faraday_middleware-multi_json (~> 0.0) oauth2 (~> 1.0) - asciidoctor (1.5.3) - asciidoctor-plantuml (0.0.7) + asciidoctor (1.5.6.2) + asciidoctor-plantuml (0.0.8) asciidoctor (~> 1.5) asset_sync (2.2.0) activemodel (>= 4.1.0) @@ -244,10 +244,11 @@ GEM builder excon (~> 0.58) formatador (~> 0.2) - fog-google (0.5.3) + fog-google (1.3.3) fog-core fog-json fog-xml + google-api-client (~> 0.19.1) fog-json (1.0.2) fog-core (~> 1.0) multi_json (~> 1.10) @@ -998,8 +999,8 @@ DEPENDENCIES akismet (~> 2.0) allocations (~> 1.0) asana (~> 0.6.0) - asciidoctor (~> 1.5.2) - asciidoctor-plantuml (= 0.0.7) + asciidoctor (~> 1.5.6) + asciidoctor-plantuml (= 0.0.8) asset_sync (~> 2.2.0) attr_encrypted (~> 3.0.0) awesome_print (~> 1.2.0) @@ -1047,9 +1048,9 @@ DEPENDENCIES flipper-active_record (~> 0.13.0) flipper-active_support_cache_store (~> 0.13.0) fog-aliyun (~> 0.2.0) - fog-aws (~> 2.0) + fog-aws (~> 2.0.1) fog-core (~> 1.44) - fog-google (~> 0.5) + fog-google (~> 1.3.3) fog-local (~> 0.3) fog-openstack (~> 0.1) fog-rackspace (~> 0.1.1) diff --git a/app/assets/javascripts/gl_dropdown.js b/app/assets/javascripts/gl_dropdown.js index 86b34a6e360..fa48d7d1915 100644 --- a/app/assets/javascripts/gl_dropdown.js +++ b/app/assets/javascripts/gl_dropdown.js @@ -753,7 +753,7 @@ GitLabDropdown = (function() { } if (this.options.isSelectable && !this.options.isSelectable(selectedObject, el)) { - return; + return [selectedObject]; } if (el.hasClass(ACTIVE_CLASS) && value !== 0) { diff --git a/app/assets/javascripts/monitoring/components/dashboard.vue b/app/assets/javascripts/monitoring/components/dashboard.vue index 10b3a4d2fee..f5572be5fbf 100644 --- a/app/assets/javascripts/monitoring/components/dashboard.vue +++ b/app/assets/javascripts/monitoring/components/dashboard.vue @@ -1,162 +1,155 @@ <script> - import _ from 'underscore'; - import Flash from '../../flash'; - import MonitoringService from '../services/monitoring_service'; - import GraphGroup from './graph_group.vue'; - import Graph from './graph.vue'; - import EmptyState from './empty_state.vue'; - import MonitoringStore from '../stores/monitoring_store'; - import eventHub from '../event_hub'; +import _ from 'underscore'; +import Flash from '../../flash'; +import MonitoringService from '../services/monitoring_service'; +import GraphGroup from './graph_group.vue'; +import Graph from './graph.vue'; +import EmptyState from './empty_state.vue'; +import MonitoringStore from '../stores/monitoring_store'; +import eventHub from '../event_hub'; - export default { - components: { - Graph, - GraphGroup, - EmptyState, +export default { + components: { + Graph, + GraphGroup, + EmptyState, + }, + props: { + hasMetrics: { + type: Boolean, + required: false, + default: true, }, - - props: { - hasMetrics: { - type: Boolean, - required: false, - default: true, - }, - showLegend: { - type: Boolean, - required: false, - default: true, - }, - showPanels: { - type: Boolean, - required: false, - default: true, - }, - forceSmallGraph: { - type: Boolean, - required: false, - default: false, - }, - documentationPath: { - type: String, - required: true, - }, - settingsPath: { - type: String, - required: true, - }, - clustersPath: { - type: String, - required: true, - }, - tagsPath: { - type: String, - required: true, - }, - projectPath: { - type: String, - required: true, - }, - metricsEndpoint: { - type: String, - required: true, - }, - deploymentEndpoint: { - type: String, - required: false, - default: null, - }, - emptyGettingStartedSvgPath: { - type: String, - required: true, - }, - emptyLoadingSvgPath: { - type: String, - required: true, - }, - emptyNoDataSvgPath: { - type: String, - required: true, - }, - emptyUnableToConnectSvgPath: { - type: String, - required: true, - }, + showLegend: { + type: Boolean, + required: false, + default: true, }, - - data() { - return { - store: new MonitoringStore(), - state: 'gettingStarted', - showEmptyState: true, - updateAspectRatio: false, - updatedAspectRatios: 0, - hoverData: {}, - resizeThrottled: {}, - }; + showPanels: { + type: Boolean, + required: false, + default: true, }, - - created() { - this.service = new MonitoringService({ - metricsEndpoint: this.metricsEndpoint, - deploymentEndpoint: this.deploymentEndpoint, - }); - eventHub.$on('toggleAspectRatio', this.toggleAspectRatio); - eventHub.$on('hoverChanged', this.hoverChanged); + forceSmallGraph: { + type: Boolean, + required: false, + default: false, }, - - beforeDestroy() { - eventHub.$off('toggleAspectRatio', this.toggleAspectRatio); - eventHub.$off('hoverChanged', this.hoverChanged); - window.removeEventListener('resize', this.resizeThrottled, false); + documentationPath: { + type: String, + required: true, }, - - mounted() { - this.resizeThrottled = _.throttle(this.resize, 600); - if (!this.hasMetrics) { - this.state = 'gettingStarted'; - } else { - this.getGraphsData(); - window.addEventListener('resize', this.resizeThrottled, false); + settingsPath: { + type: String, + required: true, + }, + clustersPath: { + type: String, + required: true, + }, + tagsPath: { + type: String, + required: true, + }, + projectPath: { + type: String, + required: true, + }, + metricsEndpoint: { + type: String, + required: true, + }, + deploymentEndpoint: { + type: String, + required: false, + default: null, + }, + emptyGettingStartedSvgPath: { + type: String, + required: true, + }, + emptyLoadingSvgPath: { + type: String, + required: true, + }, + emptyNoDataSvgPath: { + type: String, + required: true, + }, + emptyUnableToConnectSvgPath: { + type: String, + required: true, + }, + }, + data() { + return { + store: new MonitoringStore(), + state: 'gettingStarted', + showEmptyState: true, + updateAspectRatio: false, + updatedAspectRatios: 0, + hoverData: {}, + resizeThrottled: {}, + }; + }, + created() { + this.service = new MonitoringService({ + metricsEndpoint: this.metricsEndpoint, + deploymentEndpoint: this.deploymentEndpoint, + }); + eventHub.$on('toggleAspectRatio', this.toggleAspectRatio); + eventHub.$on('hoverChanged', this.hoverChanged); + }, + beforeDestroy() { + eventHub.$off('toggleAspectRatio', this.toggleAspectRatio); + eventHub.$off('hoverChanged', this.hoverChanged); + window.removeEventListener('resize', this.resizeThrottled, false); + }, + mounted() { + this.resizeThrottled = _.throttle(this.resize, 600); + if (!this.hasMetrics) { + this.state = 'gettingStarted'; + } else { + this.getGraphsData(); + window.addEventListener('resize', this.resizeThrottled, false); + } + }, + methods: { + getGraphsData() { + this.state = 'loading'; + Promise.all([ + this.service.getGraphsData().then(data => this.store.storeMetrics(data)), + this.service + .getDeploymentData() + .then(data => this.store.storeDeploymentData(data)) + .catch(() => new Flash('Error getting deployment information.')), + ]) + .then(() => { + if (this.store.groups.length < 1) { + this.state = 'noData'; + return; + } + this.showEmptyState = false; + }) + .catch(() => { + this.state = 'unableToConnect'; + }); + }, + resize() { + this.updateAspectRatio = true; + }, + toggleAspectRatio() { + this.updatedAspectRatios = this.updatedAspectRatios += 1; + if (this.store.getMetricsCount() === this.updatedAspectRatios) { + this.updateAspectRatio = !this.updateAspectRatio; + this.updatedAspectRatios = 0; } }, - - methods: { - getGraphsData() { - this.state = 'loading'; - Promise.all([ - this.service.getGraphsData() - .then(data => this.store.storeMetrics(data)), - this.service.getDeploymentData() - .then(data => this.store.storeDeploymentData(data)) - .catch(() => new Flash('Error getting deployment information.')), - ]) - .then(() => { - if (this.store.groups.length < 1) { - this.state = 'noData'; - return; - } - this.showEmptyState = false; - }) - .catch(() => { this.state = 'unableToConnect'; }); - }, - - resize() { - this.updateAspectRatio = true; - }, - - toggleAspectRatio() { - this.updatedAspectRatios = this.updatedAspectRatios += 1; - if (this.store.getMetricsCount() === this.updatedAspectRatios) { - this.updateAspectRatio = !this.updateAspectRatio; - this.updatedAspectRatios = 0; - } - }, - - hoverChanged(data) { - this.hoverData = data; - }, + hoverChanged(data) { + this.hoverData = data; }, - }; + }, +}; </script> <template> diff --git a/app/assets/javascripts/monitoring/components/empty_state.vue b/app/assets/javascripts/monitoring/components/empty_state.vue index fbf451fce68..c77f451c2d3 100644 --- a/app/assets/javascripts/monitoring/components/empty_state.vue +++ b/app/assets/javascripts/monitoring/components/empty_state.vue @@ -1,91 +1,90 @@ <script> - export default { - props: { - documentationPath: { - type: String, - required: true, - }, - settingsPath: { - type: String, - required: false, - default: '', - }, - clustersPath: { - type: String, - required: false, - default: '', - }, - selectedState: { - type: String, - required: true, - }, - emptyGettingStartedSvgPath: { - type: String, - required: true, - }, - emptyLoadingSvgPath: { - type: String, - required: true, - }, - emptyNoDataSvgPath: { - type: String, - required: true, - }, - emptyUnableToConnectSvgPath: { - type: String, - required: true, - }, +export default { + props: { + documentationPath: { + type: String, + required: true, + }, + settingsPath: { + type: String, + required: false, + default: '', + }, + clustersPath: { + type: String, + required: false, + default: '', + }, + selectedState: { + type: String, + required: true, }, - data() { - return { - states: { - gettingStarted: { - svgUrl: this.emptyGettingStartedSvgPath, - title: 'Get started with performance monitoring', - description: `Stay updated about the performance and health + emptyGettingStartedSvgPath: { + type: String, + required: true, + }, + emptyLoadingSvgPath: { + type: String, + required: true, + }, + emptyNoDataSvgPath: { + type: String, + required: true, + }, + emptyUnableToConnectSvgPath: { + type: String, + required: true, + }, + }, + data() { + return { + states: { + gettingStarted: { + svgUrl: this.emptyGettingStartedSvgPath, + title: 'Get started with performance monitoring', + description: `Stay updated about the performance and health of your environment by configuring Prometheus to monitor your deployments.`, - buttonText: 'Install Prometheus on clusters', - buttonPath: this.clustersPath, - secondaryButtonText: 'Configure existing Prometheus', - secondaryButtonPath: this.settingsPath, - }, - loading: { - svgUrl: this.emptyLoadingSvgPath, - title: 'Waiting for performance data', - description: `Creating graphs uses the data from the Prometheus server. + buttonText: 'Install Prometheus on clusters', + buttonPath: this.clustersPath, + secondaryButtonText: 'Configure existing Prometheus', + secondaryButtonPath: this.settingsPath, + }, + loading: { + svgUrl: this.emptyLoadingSvgPath, + title: 'Waiting for performance data', + description: `Creating graphs uses the data from the Prometheus server. If this takes a long time, ensure that data is available.`, - buttonText: 'View documentation', - buttonPath: this.documentationPath, - }, - noData: { - svgUrl: this.emptyNoDataSvgPath, - title: 'No data found', - description: `You are connected to the Prometheus server, but there is currently + buttonText: 'View documentation', + buttonPath: this.documentationPath, + }, + noData: { + svgUrl: this.emptyNoDataSvgPath, + title: 'No data found', + description: `You are connected to the Prometheus server, but there is currently no data to display.`, - buttonText: 'Configure Prometheus', - buttonPath: this.settingsPath, - }, - unableToConnect: { - svgUrl: this.emptyUnableToConnectSvgPath, - title: 'Unable to connect to Prometheus server', - description: 'Ensure connectivity is available from the GitLab server to the ', - buttonText: 'View documentation', - buttonPath: this.documentationPath, - }, + buttonText: 'Configure Prometheus', + buttonPath: this.settingsPath, + }, + unableToConnect: { + svgUrl: this.emptyUnableToConnectSvgPath, + title: 'Unable to connect to Prometheus server', + description: 'Ensure connectivity is available from the GitLab server to the ', + buttonText: 'View documentation', + buttonPath: this.documentationPath, }, - }; - }, - computed: { - currentState() { - return this.states[this.selectedState]; - }, - - showButtonDescription() { - if (this.selectedState === 'unableToConnect') return true; - return false; }, + }; + }, + computed: { + currentState() { + return this.states[this.selectedState]; + }, + showButtonDescription() { + if (this.selectedState === 'unableToConnect') return true; + return false; }, - }; + }, +}; </script> <template> diff --git a/app/assets/javascripts/monitoring/components/graph.vue b/app/assets/javascripts/monitoring/components/graph.vue index 42615d2bb8e..04d546fafa0 100644 --- a/app/assets/javascripts/monitoring/components/graph.vue +++ b/app/assets/javascripts/monitoring/components/graph.vue @@ -1,236 +1,229 @@ <script> - import { scaleLinear, scaleTime } from 'd3-scale'; - import { axisLeft, axisBottom } from 'd3-axis'; - import { max, extent } from 'd3-array'; - import { select } from 'd3-selection'; - import GraphLegend from './graph/legend.vue'; - import GraphFlag from './graph/flag.vue'; - import GraphDeployment from './graph/deployment.vue'; - import GraphPath from './graph/path.vue'; - import MonitoringMixin from '../mixins/monitoring_mixins'; - import eventHub from '../event_hub'; - import measurements from '../utils/measurements'; - import { bisectDate, timeScaleFormat } from '../utils/date_time_formatters'; - import createTimeSeries from '../utils/multiple_time_series'; - import bp from '../../breakpoints'; +import { scaleLinear, scaleTime } from 'd3-scale'; +import { axisLeft, axisBottom } from 'd3-axis'; +import { max, extent } from 'd3-array'; +import { select } from 'd3-selection'; +import GraphLegend from './graph/legend.vue'; +import GraphFlag from './graph/flag.vue'; +import GraphDeployment from './graph/deployment.vue'; +import GraphPath from './graph/path.vue'; +import MonitoringMixin from '../mixins/monitoring_mixins'; +import eventHub from '../event_hub'; +import measurements from '../utils/measurements'; +import { bisectDate, timeScaleFormat } from '../utils/date_time_formatters'; +import createTimeSeries from '../utils/multiple_time_series'; +import bp from '../../breakpoints'; - const d3 = { scaleLinear, scaleTime, axisLeft, axisBottom, max, extent, select }; +const d3 = { scaleLinear, scaleTime, axisLeft, axisBottom, max, extent, select }; - export default { - components: { - GraphLegend, - GraphFlag, - GraphDeployment, - GraphPath, +export default { + components: { + GraphLegend, + GraphFlag, + GraphDeployment, + GraphPath, + }, + mixins: [MonitoringMixin], + props: { + graphData: { + type: Object, + required: true, }, - - mixins: [MonitoringMixin], - - props: { - graphData: { - type: Object, - required: true, - }, - updateAspectRatio: { - type: Boolean, - required: true, - }, - deploymentData: { - type: Array, - required: true, - }, - hoverData: { - type: Object, - required: false, - default: () => ({}), - }, - projectPath: { - type: String, - required: true, - }, - tagsPath: { - type: String, - required: true, - }, - showLegend: { - type: Boolean, - required: false, - default: true, - }, - smallGraph: { - type: Boolean, - required: false, - default: false, + updateAspectRatio: { + type: Boolean, + required: true, + }, + deploymentData: { + type: Array, + required: true, + }, + hoverData: { + type: Object, + required: false, + default: () => ({}), + }, + projectPath: { + type: String, + required: true, + }, + tagsPath: { + type: String, + required: true, + }, + showLegend: { + type: Boolean, + required: false, + default: true, + }, + smallGraph: { + type: Boolean, + required: false, + default: false, + }, + }, + data() { + return { + baseGraphHeight: 450, + baseGraphWidth: 600, + graphHeight: 450, + graphWidth: 600, + graphHeightOffset: 120, + margin: {}, + unitOfDisplay: '', + yAxisLabel: '', + legendTitle: '', + reducedDeploymentData: [], + measurements: measurements.large, + currentData: { + time: new Date(), + value: 0, }, + currentDataIndex: 0, + currentXCoordinate: 0, + currentFlagPosition: 0, + showFlag: false, + showFlagContent: false, + timeSeries: [], + realPixelRatio: 1, + }; + }, + computed: { + outerViewBox() { + return `0 0 ${this.baseGraphWidth} ${this.baseGraphHeight}`; }, - - data() { + innerViewBox() { + return `0 0 ${this.baseGraphWidth - 150} ${this.baseGraphHeight}`; + }, + axisTransform() { + return `translate(70, ${this.graphHeight - 100})`; + }, + paddingBottomRootSvg() { return { - baseGraphHeight: 450, - baseGraphWidth: 600, - graphHeight: 450, - graphWidth: 600, - graphHeightOffset: 120, - margin: {}, - unitOfDisplay: '', - yAxisLabel: '', - legendTitle: '', - reducedDeploymentData: [], - measurements: measurements.large, - currentData: { - time: new Date(), - value: 0, - }, - currentDataIndex: 0, - currentXCoordinate: 0, - currentFlagPosition: 0, - showFlag: false, - showFlagContent: false, - timeSeries: [], - realPixelRatio: 1, + paddingBottom: `${Math.ceil(this.baseGraphHeight * 100) / this.baseGraphWidth || 0}%`, }; }, - - computed: { - outerViewBox() { - return `0 0 ${this.baseGraphWidth} ${this.baseGraphHeight}`; - }, - - innerViewBox() { - return `0 0 ${this.baseGraphWidth - 150} ${this.baseGraphHeight}`; - }, - - axisTransform() { - return `translate(70, ${this.graphHeight - 100})`; - }, - - paddingBottomRootSvg() { - return { - paddingBottom: `${(Math.ceil(this.baseGraphHeight * 100) / this.baseGraphWidth) || 0}%`, - }; - }, - - deploymentFlagData() { - return this.reducedDeploymentData.find(deployment => deployment.showDeploymentFlag); - }, + deploymentFlagData() { + return this.reducedDeploymentData.find(deployment => deployment.showDeploymentFlag); }, - - watch: { - updateAspectRatio() { - if (this.updateAspectRatio) { - this.graphHeight = 450; - this.graphWidth = 600; - this.measurements = measurements.large; - this.draw(); - eventHub.$emit('toggleAspectRatio'); - } - }, - - hoverData() { - this.positionFlag(); - }, + }, + watch: { + updateAspectRatio() { + if (this.updateAspectRatio) { + this.graphHeight = 450; + this.graphWidth = 600; + this.measurements = measurements.large; + this.draw(); + eventHub.$emit('toggleAspectRatio'); + } }, - - mounted() { - this.draw(); + hoverData() { + this.positionFlag(); }, + }, + mounted() { + this.draw(); + }, + methods: { + draw() { + const breakpointSize = bp.getBreakpointSize(); + const query = this.graphData.queries[0]; + this.margin = measurements.large.margin; + if (this.smallGraph || breakpointSize === 'xs' || breakpointSize === 'sm') { + this.graphHeight = 300; + this.margin = measurements.small.margin; + this.measurements = measurements.small; + } + this.unitOfDisplay = query.unit || ''; + this.yAxisLabel = this.graphData.y_label || 'Values'; + this.legendTitle = query.label || 'Average'; + this.graphWidth = this.$refs.baseSvg.clientWidth - this.margin.left - this.margin.right; + this.graphHeight = this.graphHeight - this.margin.top - this.margin.bottom; + this.baseGraphHeight = this.graphHeight; + this.baseGraphWidth = this.graphWidth; - methods: { - draw() { - const breakpointSize = bp.getBreakpointSize(); - const query = this.graphData.queries[0]; - this.margin = measurements.large.margin; - if (this.smallGraph || breakpointSize === 'xs' || breakpointSize === 'sm') { - this.graphHeight = 300; - this.margin = measurements.small.margin; - this.measurements = measurements.small; - } - this.unitOfDisplay = query.unit || ''; - this.yAxisLabel = this.graphData.y_label || 'Values'; - this.legendTitle = query.label || 'Average'; - this.graphWidth = this.$refs.baseSvg.clientWidth - - this.margin.left - this.margin.right; - this.graphHeight = this.graphHeight - this.margin.top - this.margin.bottom; - this.baseGraphHeight = this.graphHeight; - this.baseGraphWidth = this.graphWidth; - - // pixel offsets inside the svg and outside are not 1:1 - this.realPixelRatio = (this.$refs.baseSvg.clientWidth / this.baseGraphWidth); - - this.renderAxesPaths(); - this.formatDeployments(); - }, - - handleMouseOverGraph(e) { - let point = this.$refs.graphData.createSVGPoint(); - point.x = e.clientX; - point.y = e.clientY; - point = point.matrixTransform(this.$refs.graphData.getScreenCTM().inverse()); - point.x = point.x += 7; - const firstTimeSeries = this.timeSeries[0]; - const timeValueOverlay = firstTimeSeries.timeSeriesScaleX.invert(point.x); - const overlayIndex = bisectDate(firstTimeSeries.values, timeValueOverlay, 1); - const d0 = firstTimeSeries.values[overlayIndex - 1]; - const d1 = firstTimeSeries.values[overlayIndex]; - if (d0 === undefined || d1 === undefined) return; - const evalTime = timeValueOverlay - d0[0] > d1[0] - timeValueOverlay; - const hoveredDataIndex = evalTime ? overlayIndex : (overlayIndex - 1); - const hoveredDate = firstTimeSeries.values[hoveredDataIndex].time; - const currentDeployXPos = this.mouseOverDeployInfo(point.x); + // pixel offsets inside the svg and outside are not 1:1 + this.realPixelRatio = this.$refs.baseSvg.clientWidth / this.baseGraphWidth; - eventHub.$emit('hoverChanged', { - hoveredDate, - currentDeployXPos, - }); - }, + this.renderAxesPaths(); + this.formatDeployments(); + }, + handleMouseOverGraph(e) { + let point = this.$refs.graphData.createSVGPoint(); + point.x = e.clientX; + point.y = e.clientY; + point = point.matrixTransform(this.$refs.graphData.getScreenCTM().inverse()); + point.x = point.x += 7; + const firstTimeSeries = this.timeSeries[0]; + const timeValueOverlay = firstTimeSeries.timeSeriesScaleX.invert(point.x); + const overlayIndex = bisectDate(firstTimeSeries.values, timeValueOverlay, 1); + const d0 = firstTimeSeries.values[overlayIndex - 1]; + const d1 = firstTimeSeries.values[overlayIndex]; + if (d0 === undefined || d1 === undefined) return; + const evalTime = timeValueOverlay - d0[0] > d1[0] - timeValueOverlay; + const hoveredDataIndex = evalTime ? overlayIndex : overlayIndex - 1; + const hoveredDate = firstTimeSeries.values[hoveredDataIndex].time; + const currentDeployXPos = this.mouseOverDeployInfo(point.x); - renderAxesPaths() { - this.timeSeries = createTimeSeries( - this.graphData.queries, - this.graphWidth, - this.graphHeight, - this.graphHeightOffset, - ); + eventHub.$emit('hoverChanged', { + hoveredDate, + currentDeployXPos, + }); + }, + renderAxesPaths() { + this.timeSeries = createTimeSeries( + this.graphData.queries, + this.graphWidth, + this.graphHeight, + this.graphHeightOffset, + ); - if (!this.showLegend) { - this.baseGraphHeight -= 50; - } else if (this.timeSeries.length > 3) { - this.baseGraphHeight = this.baseGraphHeight += (this.timeSeries.length - 3) * 20; - } + if (!this.showLegend) { + this.baseGraphHeight -= 50; + } else if (this.timeSeries.length > 3) { + this.baseGraphHeight = this.baseGraphHeight += (this.timeSeries.length - 3) * 20; + } - const axisXScale = d3.scaleTime() - .range([0, this.graphWidth - 70]); - const axisYScale = d3.scaleLinear() - .range([this.graphHeight - this.graphHeightOffset, 0]); + const axisXScale = d3.scaleTime().range([0, this.graphWidth - 70]); + const axisYScale = d3.scaleLinear().range([this.graphHeight - this.graphHeightOffset, 0]); - const allValues = this.timeSeries.reduce((all, { values }) => all.concat(values), []); - axisXScale.domain(d3.extent(allValues, d => d.time)); - axisYScale.domain([0, d3.max(allValues.map(d => d.value))]); + const allValues = this.timeSeries.reduce((all, { values }) => all.concat(values), []); + axisXScale.domain(d3.extent(allValues, d => d.time)); + axisYScale.domain([0, d3.max(allValues.map(d => d.value))]); - const xAxis = d3.axisBottom() - .scale(axisXScale) - .ticks(this.graphWidth / 120) - .tickFormat(timeScaleFormat); + const xAxis = d3 + .axisBottom() + .scale(axisXScale) + .ticks(this.graphWidth / 120) + .tickFormat(timeScaleFormat); - const yAxis = d3.axisLeft() - .scale(axisYScale) - .ticks(measurements.yTicks); + const yAxis = d3 + .axisLeft() + .scale(axisYScale) + .ticks(measurements.yTicks); - d3.select(this.$refs.baseSvg).select('.x-axis').call(xAxis); + d3 + .select(this.$refs.baseSvg) + .select('.x-axis') + .call(xAxis); - const width = this.graphWidth; - d3.select(this.$refs.baseSvg).select('.y-axis').call(yAxis) - .selectAll('.tick') - .each(function createTickLines(d, i) { - if (i > 0) { - d3.select(this).select('line') - .attr('x2', width) - .attr('class', 'axis-tick'); - } // Avoid adding the class to the first tick, to prevent coloring - }); // This will select all of the ticks once they're rendered - }, + const width = this.graphWidth; + d3 + .select(this.$refs.baseSvg) + .select('.y-axis') + .call(yAxis) + .selectAll('.tick') + .each(function createTickLines(d, i) { + if (i > 0) { + d3 + .select(this) + .select('line') + .attr('x2', width) + .attr('class', 'axis-tick'); + } // Avoid adding the class to the first tick, to prevent coloring + }); // This will select all of the ticks once they're rendered }, - }; + }, +}; </script> <template> diff --git a/app/assets/javascripts/monitoring/components/graph/deployment.vue b/app/assets/javascripts/monitoring/components/graph/deployment.vue index 98c25307b74..4012191ceb9 100644 --- a/app/assets/javascripts/monitoring/components/graph/deployment.vue +++ b/app/assets/javascripts/monitoring/components/graph/deployment.vue @@ -1,32 +1,30 @@ <script> - export default { - props: { - deploymentData: { - type: Array, - required: true, - }, - graphHeight: { - type: Number, - required: true, - }, - graphHeightOffset: { - type: Number, - required: true, - }, +export default { + props: { + deploymentData: { + type: Array, + required: true, }, - - computed: { - calculatedHeight() { - return this.graphHeight - this.graphHeightOffset; - }, + graphHeight: { + type: Number, + required: true, }, - - methods: { - transformDeploymentGroup(deployment) { - return `translate(${Math.floor(deployment.xPos) - 5}, 20)`; - }, + graphHeightOffset: { + type: Number, + required: true, }, - }; + }, + computed: { + calculatedHeight() { + return this.graphHeight - this.graphHeightOffset; + }, + }, + methods: { + transformDeploymentGroup(deployment) { + return `translate(${Math.floor(deployment.xPos) - 5}, 20)`; + }, + }, +}; </script> <template> <g class="deploy-info"> diff --git a/app/assets/javascripts/monitoring/components/graph/flag.vue b/app/assets/javascripts/monitoring/components/graph/flag.vue index 07aa6a3e5de..906c7c51f52 100644 --- a/app/assets/javascripts/monitoring/components/graph/flag.vue +++ b/app/assets/javascripts/monitoring/components/graph/flag.vue @@ -1,127 +1,119 @@ <script> - import { dateFormat, timeFormat } from '../../utils/date_time_formatters'; - import { formatRelevantDigits } from '../../../lib/utils/number_utils'; - import icon from '../../../vue_shared/components/icon.vue'; +import { dateFormat, timeFormat } from '../../utils/date_time_formatters'; +import { formatRelevantDigits } from '../../../lib/utils/number_utils'; +import icon from '../../../vue_shared/components/icon.vue'; - export default { - components: { - icon, - }, - props: { - currentXCoordinate: { - type: Number, - required: true, - }, - currentData: { - type: Object, - required: true, - }, - deploymentFlagData: { - type: Object, - required: false, - default: null, - }, - graphHeight: { - type: Number, - required: true, - }, - graphHeightOffset: { - type: Number, - required: true, - }, - realPixelRatio: { - type: Number, - required: true, - }, - showFlagContent: { - type: Boolean, - required: true, - }, - timeSeries: { - type: Array, - required: true, - }, - unitOfDisplay: { - type: String, - required: true, - }, - currentDataIndex: { - type: Number, - required: true, - }, - legendTitle: { - type: String, - required: true, - }, +export default { + components: { + icon, + }, + props: { + currentXCoordinate: { + type: Number, + required: true, }, - - computed: { - formatTime() { - return this.deploymentFlagData ? - timeFormat(this.deploymentFlagData.time) : - timeFormat(this.currentData.time); - }, - - formatDate() { - return this.deploymentFlagData ? - dateFormat(this.deploymentFlagData.time) : - dateFormat(this.currentData.time); - }, - - cursorStyle() { - const xCoordinate = this.deploymentFlagData ? - this.deploymentFlagData.xPos : - this.currentXCoordinate; - - const offsetTop = 20 * this.realPixelRatio; - const offsetLeft = (70 + xCoordinate) * this.realPixelRatio; - const height = (this.graphHeight - this.graphHeightOffset) * this.realPixelRatio; - - return { - top: `${offsetTop}px`, - left: `${offsetLeft}px`, - height: `${height}px`, - }; - }, - - flagOrientation() { - if (this.currentXCoordinate * this.realPixelRatio > 120) { - return 'left'; - } - return 'right'; - }, + currentData: { + type: Object, + required: true, }, + deploymentFlagData: { + type: Object, + required: false, + default: null, + }, + graphHeight: { + type: Number, + required: true, + }, + graphHeightOffset: { + type: Number, + required: true, + }, + realPixelRatio: { + type: Number, + required: true, + }, + showFlagContent: { + type: Boolean, + required: true, + }, + timeSeries: { + type: Array, + required: true, + }, + unitOfDisplay: { + type: String, + required: true, + }, + currentDataIndex: { + type: Number, + required: true, + }, + legendTitle: { + type: String, + required: true, + }, + }, + computed: { + formatTime() { + return this.deploymentFlagData + ? timeFormat(this.deploymentFlagData.time) + : timeFormat(this.currentData.time); + }, + formatDate() { + return this.deploymentFlagData + ? dateFormat(this.deploymentFlagData.time) + : dateFormat(this.currentData.time); + }, + cursorStyle() { + const xCoordinate = this.deploymentFlagData + ? this.deploymentFlagData.xPos + : this.currentXCoordinate; - methods: { - seriesMetricValue(series) { - const index = this.deploymentFlagData ? - this.deploymentFlagData.seriesIndex : - this.currentDataIndex; - const value = series.values[index] && - series.values[index].value; - if (isNaN(value)) { - return '-'; - } - return `${formatRelevantDigits(value)}${this.unitOfDisplay}`; - }, - - seriesMetricLabel(index, series) { - if (this.timeSeries.length < 2) { - return this.legendTitle; - } - if (series.metricTag) { - return series.metricTag; - } - return `series ${index + 1}`; - }, + const offsetTop = 20 * this.realPixelRatio; + const offsetLeft = (70 + xCoordinate) * this.realPixelRatio; + const height = (this.graphHeight - this.graphHeightOffset) * this.realPixelRatio; - strokeDashArray(type) { - if (type === 'dashed') return '6, 3'; - if (type === 'dotted') return '3, 3'; - return null; - }, + return { + top: `${offsetTop}px`, + left: `${offsetLeft}px`, + height: `${height}px`, + }; + }, + flagOrientation() { + if (this.currentXCoordinate * this.realPixelRatio > 120) { + return 'left'; + } + return 'right'; + }, + }, + methods: { + seriesMetricValue(series) { + const index = this.deploymentFlagData + ? this.deploymentFlagData.seriesIndex + : this.currentDataIndex; + const value = series.values[index] && series.values[index].value; + if (isNaN(value)) { + return '-'; + } + return `${formatRelevantDigits(value)}${this.unitOfDisplay}`; + }, + seriesMetricLabel(index, series) { + if (this.timeSeries.length < 2) { + return this.legendTitle; + } + if (series.metricTag) { + return series.metricTag; + } + return `series ${index + 1}`; + }, + strokeDashArray(type) { + if (type === 'dashed') return '6, 3'; + if (type === 'dotted') return '3, 3'; + return null; }, - }; + }, +}; </script> <template> diff --git a/app/assets/javascripts/monitoring/components/graph/legend.vue b/app/assets/javascripts/monitoring/components/graph/legend.vue index 3149397b61f..a7a058a9203 100644 --- a/app/assets/javascripts/monitoring/components/graph/legend.vue +++ b/app/assets/javascripts/monitoring/components/graph/legend.vue @@ -1,127 +1,119 @@ <script> - import { formatRelevantDigits } from '../../../lib/utils/number_utils'; - - export default { - props: { - graphWidth: { - type: Number, - required: true, - }, - graphHeight: { - type: Number, - required: true, - }, - margin: { - type: Object, - required: true, - }, - measurements: { - type: Object, - required: true, - }, - legendTitle: { - type: String, - required: true, - }, - yAxisLabel: { - type: String, - required: true, - }, - timeSeries: { - type: Array, - required: true, - }, - unitOfDisplay: { - type: String, - required: true, - }, - currentDataIndex: { - type: Number, - required: true, - }, - showLegendGroup: { - type: Boolean, - required: false, - default: true, - }, - }, - data() { - return { - yLabelWidth: 0, - yLabelHeight: 0, - seriesXPosition: 0, - metricUsageXPosition: 0, - }; - }, - computed: { - textTransform() { - const yCoordinate = (((this.graphHeight - this.margin.top) - + this.measurements.axisLabelLineOffset) / 2) || 0; - - return `translate(15, ${yCoordinate}) rotate(-90)`; - }, - - rectTransform() { - const yCoordinate = (((this.graphHeight - this.margin.top) - + this.measurements.axisLabelLineOffset) / 2) - + (this.yLabelWidth / 2) || 0; - - return `translate(0, ${yCoordinate}) rotate(-90)`; - }, - - xPosition() { - return (((this.graphWidth + this.measurements.axisLabelLineOffset) / 2) - - this.margin.right) || 0; - }, - - yPosition() { - return ((this.graphHeight - this.margin.top) + this.measurements.axisLabelLineOffset) || 0; - }, +import { formatRelevantDigits } from '../../../lib/utils/number_utils'; +export default { + props: { + graphWidth: { + type: Number, + required: true, }, - mounted() { - this.$nextTick(() => { - const bbox = this.$refs.ylabel.getBBox(); - this.metricUsageXPosition = 0; - this.seriesXPosition = 0; - if (this.$refs.legendTitleSvg != null) { - this.seriesXPosition = this.$refs.legendTitleSvg[0].getBBox().width; - } - if (this.$refs.seriesTitleSvg != null) { - this.metricUsageXPosition = this.$refs.seriesTitleSvg[0].getBBox().width; - } - this.yLabelWidth = bbox.width + 10; // Added some padding - this.yLabelHeight = bbox.height + 5; - }); - }, - methods: { - translateLegendGroup(index) { - return `translate(0, ${12 * (index)})`; - }, - - formatMetricUsage(series) { - const value = series.values[this.currentDataIndex] && - series.values[this.currentDataIndex].value; - if (isNaN(value)) { - return '-'; - } - return `${formatRelevantDigits(value)} ${this.unitOfDisplay}`; - }, + graphHeight: { + type: Number, + required: true, + }, + margin: { + type: Object, + required: true, + }, + measurements: { + type: Object, + required: true, + }, + legendTitle: { + type: String, + required: true, + }, + yAxisLabel: { + type: String, + required: true, + }, + timeSeries: { + type: Array, + required: true, + }, + unitOfDisplay: { + type: String, + required: true, + }, + currentDataIndex: { + type: Number, + required: true, + }, + showLegendGroup: { + type: Boolean, + required: false, + default: true, + }, + }, + data() { + return { + yLabelWidth: 0, + yLabelHeight: 0, + seriesXPosition: 0, + metricUsageXPosition: 0, + }; + }, + computed: { + textTransform() { + const yCoordinate = + (this.graphHeight - this.margin.top + this.measurements.axisLabelLineOffset) / 2 || 0; - createSeriesString(index, series) { - if (series.metricTag) { - return `${series.metricTag} ${this.formatMetricUsage(series)}`; - } - return `${this.legendTitle} series ${index + 1} ${this.formatMetricUsage(series)}`; - }, + return `translate(15, ${yCoordinate}) rotate(-90)`; + }, + rectTransform() { + const yCoordinate = + (this.graphHeight - this.margin.top + this.measurements.axisLabelLineOffset) / 2 + + this.yLabelWidth / 2 || 0; - strokeDashArray(type) { - if (type === 'dashed') return '6, 3'; - if (type === 'dotted') return '3, 3'; - return null; - }, + return `translate(0, ${yCoordinate}) rotate(-90)`; + }, + xPosition() { + return (this.graphWidth + this.measurements.axisLabelLineOffset) / 2 - this.margin.right || 0; + }, + yPosition() { + return this.graphHeight - this.margin.top + this.measurements.axisLabelLineOffset || 0; + }, + }, + mounted() { + this.$nextTick(() => { + const bbox = this.$refs.ylabel.getBBox(); + this.metricUsageXPosition = 0; + this.seriesXPosition = 0; + if (this.$refs.legendTitleSvg != null) { + this.seriesXPosition = this.$refs.legendTitleSvg[0].getBBox().width; + } + if (this.$refs.seriesTitleSvg != null) { + this.metricUsageXPosition = this.$refs.seriesTitleSvg[0].getBBox().width; + } + this.yLabelWidth = bbox.width + 10; // Added some padding + this.yLabelHeight = bbox.height + 5; + }); + }, + methods: { + translateLegendGroup(index) { + return `translate(0, ${12 * index})`; + }, + formatMetricUsage(series) { + const value = + series.values[this.currentDataIndex] && series.values[this.currentDataIndex].value; + if (isNaN(value)) { + return '-'; + } + return `${formatRelevantDigits(value)} ${this.unitOfDisplay}`; + }, + createSeriesString(index, series) { + if (series.metricTag) { + return `${series.metricTag} ${this.formatMetricUsage(series)}`; + } + return `${this.legendTitle} series ${index + 1} ${this.formatMetricUsage(series)}`; + }, + strokeDashArray(type) { + if (type === 'dashed') return '6, 3'; + if (type === 'dotted') return '3, 3'; + return null; }, - }; + }, +}; </script> <template> <g class="axis-label-container"> diff --git a/app/assets/javascripts/monitoring/components/graph/path.vue b/app/assets/javascripts/monitoring/components/graph/path.vue index c9721c4cb01..881560124a5 100644 --- a/app/assets/javascripts/monitoring/components/graph/path.vue +++ b/app/assets/javascripts/monitoring/components/graph/path.vue @@ -1,36 +1,36 @@ <script> - export default { - props: { - generatedLinePath: { - type: String, - required: true, - }, - generatedAreaPath: { - type: String, - required: true, - }, - lineStyle: { - type: String, - required: false, - default: '', - }, - lineColor: { - type: String, - required: true, - }, - areaColor: { - type: String, - required: true, - }, +export default { + props: { + generatedLinePath: { + type: String, + required: true, }, - computed: { - strokeDashArray() { - if (this.lineStyle === 'dashed') return '3, 1'; - if (this.lineStyle === 'dotted') return '1, 1'; - return null; - }, + generatedAreaPath: { + type: String, + required: true, }, - }; + lineStyle: { + type: String, + required: false, + default: '', + }, + lineColor: { + type: String, + required: true, + }, + areaColor: { + type: String, + required: true, + }, + }, + computed: { + strokeDashArray() { + if (this.lineStyle === 'dashed') return '3, 1'; + if (this.lineStyle === 'dotted') return '1, 1'; + return null; + }, + }, +}; </script> <template> <g> diff --git a/app/assets/javascripts/monitoring/components/graph_group.vue b/app/assets/javascripts/monitoring/components/graph_group.vue index f71cf614552..a6dbe42a8f0 100644 --- a/app/assets/javascripts/monitoring/components/graph_group.vue +++ b/app/assets/javascripts/monitoring/components/graph_group.vue @@ -1,17 +1,17 @@ <script> - export default { - props: { - name: { - type: String, - required: true, - }, - showPanels: { - type: Boolean, - required: false, - default: true, - }, +export default { + props: { + name: { + type: String, + required: true, }, - }; + showPanels: { + type: Boolean, + required: false, + default: true, + }, + }, +}; </script> <template> diff --git a/app/assets/javascripts/pages/ci/lints/ci_lint_editor.js b/app/assets/javascripts/pages/projects/ci/lints/ci_lint_editor.js index 9ab73be80a0..9ab73be80a0 100644 --- a/app/assets/javascripts/pages/ci/lints/ci_lint_editor.js +++ b/app/assets/javascripts/pages/projects/ci/lints/ci_lint_editor.js diff --git a/app/assets/javascripts/pages/ci/lints/new/index.js b/app/assets/javascripts/pages/projects/ci/lints/new/index.js index 8e8a843da0b..8e8a843da0b 100644 --- a/app/assets/javascripts/pages/ci/lints/new/index.js +++ b/app/assets/javascripts/pages/projects/ci/lints/new/index.js diff --git a/app/assets/javascripts/pages/ci/lints/show/index.js b/app/assets/javascripts/pages/projects/ci/lints/show/index.js index 8e8a843da0b..8e8a843da0b 100644 --- a/app/assets/javascripts/pages/ci/lints/show/index.js +++ b/app/assets/javascripts/pages/projects/ci/lints/show/index.js diff --git a/app/assets/javascripts/vue_merge_request_widget/components/states/sha_mismatch.vue b/app/assets/javascripts/vue_merge_request_widget/components/states/sha_mismatch.vue index 04100871a94..7cc07401911 100644 --- a/app/assets/javascripts/vue_merge_request_widget/components/states/sha_mismatch.vue +++ b/app/assets/javascripts/vue_merge_request_widget/components/states/sha_mismatch.vue @@ -17,8 +17,8 @@ export default { /> <div class="media-body space-children"> <span class="bold"> - The source branch HEAD has recently changed. - Please reload the page and review the changes before merging. + {{ s__(`mrWidget|The source branch HEAD has recently changed. +Please reload the page and review the changes before merging`) }} </span> </div> </div> diff --git a/app/assets/stylesheets/framework/gitlab_theme.scss b/app/assets/stylesheets/framework/gitlab_theme.scss index db36e27fa74..7f3f7e67d76 100644 --- a/app/assets/stylesheets/framework/gitlab_theme.scss +++ b/app/assets/stylesheets/framework/gitlab_theme.scss @@ -2,7 +2,15 @@ * Styles the GitLab application with a specific color theme */ -@mixin gitlab-theme($color-100, $color-200, $color-500, $color-700, $color-800, $color-900, $color-alternate) { +@mixin gitlab-theme( + $color-100, + $color-200, + $color-500, + $color-700, + $color-800, + $color-900, + $color-alternate +) { // Header .navbar-gitlab { @@ -23,7 +31,7 @@ > li { > a:hover, > a:focus { - background-color: rgba($color-200, .2); + background-color: rgba($color-200, 0.2); } &.active > a, @@ -33,7 +41,7 @@ } &.line-separator { - border-left: 1px solid rgba($color-200, .2); + border-left: 1px solid rgba($color-200, 0.2); } } } @@ -56,7 +64,7 @@ &:hover, &:focus { @media (min-width: $screen-sm-min) { - background-color: rgba($color-200, .2); + background-color: rgba($color-200, 0.2); } svg { @@ -91,34 +99,34 @@ > a { &:hover, &:focus { - background-color: rgba($color-200, .2); + background-color: rgba($color-200, 0.2); } } } .search { form { - background-color: rgba($color-200, .2); + background-color: rgba($color-200, 0.2); &:hover { - background-color: rgba($color-200, .3); + background-color: rgba($color-200, 0.3); } } .location-badge { color: $color-100; - background-color: rgba($color-200, .1); + background-color: rgba($color-200, 0.1); border-right: 1px solid $color-800; } .search-input::placeholder { - color: rgba($color-200, .8); + color: rgba($color-200, 0.8); } .search-input-wrap { .search-icon, .clear-icon { - fill: rgba($color-200, .8); + fill: rgba($color-200, 0.8); } } @@ -133,7 +141,7 @@ .search-input-wrap { .search-icon { - fill: rgba($color-200, .8); + fill: rgba($color-200, 0.8); } } } @@ -144,7 +152,6 @@ color: $color-900; } - // Sidebar .nav-sidebar li.active { box-shadow: inset 4px 0 0 $color-700; @@ -169,28 +176,90 @@ font-weight: $gl-font-weight-bold; } } -} + // Web IDE + .ide-sidebar-link { + color: $color-200; + background-color: $color-700; + + &:hover, + &:focus { + background-color: $color-500; + } + + &:active { + background: $color-800; + } + } + + .branch-container { + border-left-color: $color-700; + } + + .branch-header-title { + color: $color-700; + } +} body { &.ui_indigo { - @include gitlab-theme($indigo-100, $indigo-200, $indigo-500, $indigo-700, $indigo-800, $indigo-900, $white-light); + @include gitlab-theme( + $indigo-100, + $indigo-200, + $indigo-500, + $indigo-700, + $indigo-800, + $indigo-900, + $white-light + ); } &.ui_dark { - @include gitlab-theme($theme-gray-100, $theme-gray-200, $theme-gray-500, $theme-gray-700, $theme-gray-800, $theme-gray-900, $white-light); + @include gitlab-theme( + $theme-gray-100, + $theme-gray-200, + $theme-gray-500, + $theme-gray-700, + $theme-gray-800, + $theme-gray-900, + $white-light + ); } &.ui_blue { - @include gitlab-theme($theme-blue-100, $theme-blue-200, $theme-blue-500, $theme-blue-700, $theme-blue-800, $theme-blue-900, $white-light); + @include gitlab-theme( + $theme-blue-100, + $theme-blue-200, + $theme-blue-500, + $theme-blue-700, + $theme-blue-800, + $theme-blue-900, + $white-light + ); } &.ui_green { - @include gitlab-theme($theme-green-100, $theme-green-200, $theme-green-500, $theme-green-700, $theme-green-800, $theme-green-900, $white-light); + @include gitlab-theme( + $theme-green-100, + $theme-green-200, + $theme-green-500, + $theme-green-700, + $theme-green-800, + $theme-green-900, + $white-light + ); } &.ui_light { - @include gitlab-theme($theme-gray-900, $theme-gray-700, $theme-gray-800, $theme-gray-700, $theme-gray-700, $theme-gray-100, $theme-gray-700); + @include gitlab-theme( + $theme-gray-900, + $theme-gray-700, + $theme-gray-800, + $theme-gray-700, + $theme-gray-700, + $theme-gray-100, + $theme-gray-700 + ); .navbar-gitlab { background-color: $theme-gray-100; @@ -270,5 +339,9 @@ body { .sidebar-top-level-items > li.active .badge { color: $theme-gray-900; } + + .ide-sidebar-link { + color: $white-light; + } } } diff --git a/app/assets/stylesheets/pages/lint.scss b/app/assets/stylesheets/pages/lint.scss deleted file mode 100644 index 68b6c5ecbd4..00000000000 --- a/app/assets/stylesheets/pages/lint.scss +++ /dev/null @@ -1,21 +0,0 @@ -.ci-body { - .incorrect-syntax { - font-size: 18px; - color: $lint-incorrect-color; - } - - .correct-syntax { - font-size: 18px; - color: $lint-correct-color; - } -} - -.ci-linter { - .ci-editor { - height: 400px; - } - - .ci-template pre { - white-space: pre-wrap; - } -} diff --git a/app/assets/stylesheets/pages/projects.scss b/app/assets/stylesheets/pages/projects.scss index 584b0579b72..9a770d77685 100644 --- a/app/assets/stylesheets/pages/projects.scss +++ b/app/assets/stylesheets/pages/projects.scss @@ -1121,3 +1121,25 @@ pre.light-well { padding-top: $gl-padding; padding-bottom: 37px; } + +.project-ci-body { + .incorrect-syntax { + font-size: 18px; + color: $lint-incorrect-color; + } + + .correct-syntax { + font-size: 18px; + color: $lint-correct-color; + } +} + +.project-ci-linter { + .ci-editor { + height: 400px; + } + + .ci-template pre { + white-space: pre-wrap; + } +} diff --git a/app/assets/stylesheets/pages/repo.scss b/app/assets/stylesheets/pages/repo.scss index 57b995adb64..65046f6665e 100644 --- a/app/assets/stylesheets/pages/repo.scss +++ b/app/assets/stylesheets/pages/repo.scss @@ -402,7 +402,7 @@ } .branch-container { - border-left: 4px solid $indigo-700; + border-left: 4px solid; margin-bottom: $gl-bar-padding; } @@ -414,7 +414,6 @@ .branch-header-title { flex: 1; padding: $grid-size $gl-padding; - color: $indigo-700; font-weight: $gl-font-weight-bold; svg { @@ -767,20 +766,7 @@ .ide-sidebar-link { padding: $gl-padding-8 $gl-padding; - background: $indigo-700; - color: $white-light; - text-decoration: none; display: flex; align-items: center; - - &:focus, - &:hover { - color: $white-light; - text-decoration: underline; - background: $indigo-500; - } - - &:active { - background: $indigo-800; - } + font-weight: $gl-font-weight-bold; } diff --git a/app/controllers/ci/lints_controller.rb b/app/controllers/ci/lints_controller.rb index e9bd1689a1e..738a6a5173e 100644 --- a/app/controllers/ci/lints_controller.rb +++ b/app/controllers/ci/lints_controller.rb @@ -4,20 +4,5 @@ module Ci def show end - - def create - @content = params[:content] - @error = Gitlab::Ci::YamlProcessor.validation_message(@content) - @status = @error.blank? - - if @error.blank? - @config_processor = Gitlab::Ci::YamlProcessor.new(@content) - @stages = @config_processor.stages - @builds = @config_processor.builds - @jobs = @config_processor.jobs - end - - render :show - end end end diff --git a/app/controllers/projects/ci/lints_controller.rb b/app/controllers/projects/ci/lints_controller.rb new file mode 100644 index 00000000000..a2185572a20 --- /dev/null +++ b/app/controllers/projects/ci/lints_controller.rb @@ -0,0 +1,27 @@ +class Projects::Ci::LintsController < Projects::ApplicationController + before_action :authorize_create_pipeline! + + def show + end + + def create + @content = params[:content] + @error = Gitlab::Ci::YamlProcessor.validation_message(@content, yaml_processor_options) + @status = @error.blank? + + if @error.blank? + @config_processor = Gitlab::Ci::YamlProcessor.new(@content, yaml_processor_options) + @stages = @config_processor.stages + @builds = @config_processor.builds + @jobs = @config_processor.jobs + end + + render :show + end + + private + + def yaml_processor_options + { project: @project, sha: project.repository.commit.sha } + end +end diff --git a/app/controllers/projects/lfs_storage_controller.rb b/app/controllers/projects/lfs_storage_controller.rb index 6b16f1ccbbb..2515e4b9a17 100644 --- a/app/controllers/projects/lfs_storage_controller.rb +++ b/app/controllers/projects/lfs_storage_controller.rb @@ -17,20 +17,23 @@ class Projects::LfsStorageController < Projects::GitHttpClientController def upload_authorize set_workhorse_internal_api_content_type - render json: Gitlab::Workhorse.lfs_upload_ok(oid, size) + + authorized = LfsObjectUploader.workhorse_authorize + authorized.merge!(LfsOid: oid, LfsSize: size) + + render json: authorized end def upload_finalize - unless tmp_filename - render_lfs_forbidden - return - end - - if store_file(oid, size, tmp_filename) + if store_file!(oid, size) head 200 else render plain: 'Unprocessable entity', status: 422 end + rescue ActiveRecord::RecordInvalid + render_400 + rescue ObjectStorage::RemoteStoreError + render_lfs_forbidden end private @@ -51,35 +54,28 @@ class Projects::LfsStorageController < Projects::GitHttpClientController params[:size].to_i end - def tmp_filename - name = request.headers['X-Gitlab-Lfs-Tmp'] - return if name.include?('/') - return unless oid.present? && name.start_with?(oid) - - name - end + def store_file!(oid, size) + object = LfsObject.find_by(oid: oid, size: size) + unless object&.file&.exists? + object = create_file!(oid, size) + end - def store_file(oid, size, tmp_file) - # Define tmp_file_path early because we use it in "ensure" - tmp_file_path = File.join(LfsObjectUploader.workhorse_upload_path, tmp_file) + return unless object - object = LfsObject.find_or_create_by(oid: oid, size: size) - file_exists = object.file.exists? || move_tmp_file_to_storage(object, tmp_file_path) - file_exists && link_to_project(object) - ensure - FileUtils.rm_f(tmp_file_path) + link_to_project!(object) end - def move_tmp_file_to_storage(object, path) - object.file = File.open(path) - object.file.store! - object.save + def create_file!(oid, size) + LfsObject.new(oid: oid, size: size).tap do |object| + object.file.store_workhorse_file!(params, :file) + object.save! + end end - def link_to_project(object) + def link_to_project!(object) if object && !object.projects.exists?(storage_project.id) object.projects << storage_project - object.save + object.save! end end end diff --git a/app/controllers/projects/protected_branches_controller.rb b/app/controllers/projects/protected_branches_controller.rb index d1719f12072..64954ac9a42 100644 --- a/app/controllers/projects/protected_branches_controller.rb +++ b/app/controllers/projects/protected_branches_controller.rb @@ -5,12 +5,8 @@ class Projects::ProtectedBranchesController < Projects::ProtectedRefsController @project.repository.branches end - def create_service_class - ::ProtectedBranches::CreateService - end - - def update_service_class - ::ProtectedBranches::UpdateService + def service_namespace + ::ProtectedBranches end def load_protected_ref diff --git a/app/controllers/projects/protected_refs_controller.rb b/app/controllers/projects/protected_refs_controller.rb index b51bdf7aa78..9e757a8d25f 100644 --- a/app/controllers/projects/protected_refs_controller.rb +++ b/app/controllers/projects/protected_refs_controller.rb @@ -37,7 +37,7 @@ class Projects::ProtectedRefsController < Projects::ApplicationController end def destroy - @protected_ref.destroy + destroy_service_class.new(@project, current_user).execute(@protected_ref) respond_to do |format| format.html { redirect_to_repository_settings(@project) } @@ -47,6 +47,18 @@ class Projects::ProtectedRefsController < Projects::ApplicationController protected + def create_service_class + service_namespace::CreateService + end + + def update_service_class + service_namespace::UpdateService + end + + def destroy_service_class + service_namespace::DestroyService + end + def access_level_attributes %i(access_level id) end diff --git a/app/controllers/projects/protected_tags_controller.rb b/app/controllers/projects/protected_tags_controller.rb index a5dbd7e46ae..198c938ff35 100644 --- a/app/controllers/projects/protected_tags_controller.rb +++ b/app/controllers/projects/protected_tags_controller.rb @@ -5,12 +5,8 @@ class Projects::ProtectedTagsController < Projects::ProtectedRefsController @project.repository.tags end - def create_service_class - ::ProtectedTags::CreateService - end - - def update_service_class - ::ProtectedTags::UpdateService + def service_namespace + ::ProtectedTags end def load_protected_ref diff --git a/app/controllers/projects/settings/ci_cd_controller.rb b/app/controllers/projects/settings/ci_cd_controller.rb index 259809f3429..96125b549b7 100644 --- a/app/controllers/projects/settings/ci_cd_controller.rb +++ b/app/controllers/projects/settings/ci_cd_controller.rb @@ -29,12 +29,12 @@ module Projects @project_runners = @project.runners.ordered @assignable_runners = current_user.ci_authorized_runners .assignable_for(project).ordered.page(params[:page]).per(20) - @shared_runners = Ci::Runner.shared.active + @shared_runners = ::Ci::Runner.shared.active @shared_runners_count = @shared_runners.count(:all) end def define_secret_variables - @variable = Ci::Variable.new(project: project) + @variable = ::Ci::Variable.new(project: project) .present(current_user: current_user) @variables = project.variables.order_key_asc .map { |variable| variable.present(current_user: current_user) } @@ -42,7 +42,7 @@ module Projects def define_triggers_variables @triggers = @project.triggers - @trigger = Ci::Trigger.new + @trigger = ::Ci::Trigger.new end def define_badges_variables diff --git a/app/controllers/root_controller.rb b/app/controllers/root_controller.rb index 8acefd58e77..651b82f04f4 100644 --- a/app/controllers/root_controller.rb +++ b/app/controllers/root_controller.rb @@ -42,6 +42,10 @@ class RootController < Dashboard::ProjectsController redirect_to(dashboard_groups_path) when 'todos' redirect_to(dashboard_todos_path) + when 'issues' + redirect_to(issues_dashboard_path(assignee_id: current_user.id)) + when 'merge_requests' + redirect_to(merge_requests_dashboard_path(assignee_id: current_user.id)) end end diff --git a/app/helpers/preferences_helper.rb b/app/helpers/preferences_helper.rb index 373dfd457f7..fb523cb865b 100644 --- a/app/helpers/preferences_helper.rb +++ b/app/helpers/preferences_helper.rb @@ -9,12 +9,14 @@ module PreferencesHelper # Maps `dashboard` values to more user-friendly option text DASHBOARD_CHOICES = { - projects: 'Your Projects (default)', - stars: 'Starred Projects', - project_activity: "Your Projects' Activity", - starred_project_activity: "Starred Projects' Activity", - groups: "Your Groups", - todos: "Your Todos" + projects: _("Your Projects (default)"), + stars: _("Starred Projects"), + project_activity: _("Your Projects' Activity"), + starred_project_activity: _("Starred Projects' Activity"), + groups: _("Your Groups"), + todos: _("Your Todos"), + issues: _("Assigned Issues"), + merge_requests: _("Assigned Merge Requests") }.with_indifferent_access.freeze # Returns an Array usable by a select field for more user-friendly option text diff --git a/app/models/lfs_object.rb b/app/models/lfs_object.rb index 64e88d5a6a2..b7de46fa202 100644 --- a/app/models/lfs_object.rb +++ b/app/models/lfs_object.rb @@ -11,6 +11,12 @@ class LfsObject < ActiveRecord::Base mount_uploader :file, LfsObjectUploader + before_save :update_file_store + + def update_file_store + self.file_store = file.object_store + end + def project_allowed_access?(project) projects.exists?(project.lfs_storage_project.id) end diff --git a/app/models/merge_request.rb b/app/models/merge_request.rb index 7e6d89ec9c7..91d8be5559b 100644 --- a/app/models/merge_request.rb +++ b/app/models/merge_request.rb @@ -536,18 +536,25 @@ class MergeRequest < ActiveRecord::Base merge_request_diff(true) end + def viewable_diffs + @viewable_diffs ||= merge_request_diffs.viewable.to_a + end + def merge_request_diff_for(diff_refs_or_sha) - @merge_request_diffs_by_diff_refs_or_sha ||= Hash.new do |h, diff_refs_or_sha| - diffs = merge_request_diffs.viewable - h[diff_refs_or_sha] = - if diff_refs_or_sha.is_a?(Gitlab::Diff::DiffRefs) - diffs.find_by_diff_refs(diff_refs_or_sha) - else - diffs.find_by(head_commit_sha: diff_refs_or_sha) - end - end + matcher = + if diff_refs_or_sha.is_a?(Gitlab::Diff::DiffRefs) + { + 'start_commit_sha' => diff_refs_or_sha.start_sha, + 'head_commit_sha' => diff_refs_or_sha.head_sha, + 'base_commit_sha' => diff_refs_or_sha.base_sha + } + else + { 'head_commit_sha' => diff_refs_or_sha } + end - @merge_request_diffs_by_diff_refs_or_sha[diff_refs_or_sha] + viewable_diffs.find do |diff| + diff.attributes.slice(*matcher.keys) == matcher + end end def version_params_for(diff_refs) diff --git a/app/models/user.rb b/app/models/user.rb index fa54581d220..187878f4fb5 100644 --- a/app/models/user.rb +++ b/app/models/user.rb @@ -187,7 +187,7 @@ class User < ActiveRecord::Base # User's Dashboard preference # Note: When adding an option, it MUST go on the end of the array. - enum dashboard: [:projects, :stars, :project_activity, :starred_project_activity, :groups, :todos] + enum dashboard: [:projects, :stars, :project_activity, :starred_project_activity, :groups, :todos, :issues, :merge_requests] # User's Project preference # Note: When adding an option, it MUST go on the end of the array. diff --git a/app/policies/protected_branch_policy.rb b/app/policies/protected_branch_policy.rb new file mode 100644 index 00000000000..1a7faa4db40 --- /dev/null +++ b/app/policies/protected_branch_policy.rb @@ -0,0 +1,9 @@ +class ProtectedBranchPolicy < BasePolicy + delegate { @subject.project } + + rule { can?(:admin_project) }.policy do + enable :create_protected_branch + enable :update_protected_branch + enable :destroy_protected_branch + end +end diff --git a/app/services/protected_branches/create_service.rb b/app/services/protected_branches/create_service.rb index 6212fd69077..9d947f73af1 100644 --- a/app/services/protected_branches/create_service.rb +++ b/app/services/protected_branches/create_service.rb @@ -1,11 +1,20 @@ module ProtectedBranches class CreateService < BaseService - attr_reader :protected_branch - def execute(skip_authorization: false) - raise Gitlab::Access::AccessDeniedError unless skip_authorization || can?(current_user, :admin_project, project) + raise Gitlab::Access::AccessDeniedError unless skip_authorization || authorized? + + protected_branch.save + protected_branch + end + + def authorized? + can?(current_user, :create_protected_branch, protected_branch) + end + + private - project.protected_branches.create(params) + def protected_branch + @protected_branch ||= project.protected_branches.new(params) end end end diff --git a/app/services/protected_branches/destroy_service.rb b/app/services/protected_branches/destroy_service.rb new file mode 100644 index 00000000000..8172c896e76 --- /dev/null +++ b/app/services/protected_branches/destroy_service.rb @@ -0,0 +1,9 @@ +module ProtectedBranches + class DestroyService < BaseService + def execute(protected_branch) + raise Gitlab::Access::AccessDeniedError unless can?(current_user, :destroy_protected_branch, protected_branch) + + protected_branch.destroy + end + end +end diff --git a/app/services/protected_branches/update_service.rb b/app/services/protected_branches/update_service.rb index 4b3337a5c9d..95e46645374 100644 --- a/app/services/protected_branches/update_service.rb +++ b/app/services/protected_branches/update_service.rb @@ -1,7 +1,7 @@ module ProtectedBranches class UpdateService < BaseService def execute(protected_branch) - raise Gitlab::Access::AccessDeniedError unless can?(current_user, :admin_project, project) + raise Gitlab::Access::AccessDeniedError unless can?(current_user, :update_protected_branch, protected_branch) protected_branch.update(params) protected_branch diff --git a/app/services/protected_tags/destroy_service.rb b/app/services/protected_tags/destroy_service.rb new file mode 100644 index 00000000000..c868d7ad8e6 --- /dev/null +++ b/app/services/protected_tags/destroy_service.rb @@ -0,0 +1,7 @@ +module ProtectedTags + class DestroyService < BaseService + def execute(protected_tag) + protected_tag.destroy + end + end +end diff --git a/app/uploaders/object_storage.rb b/app/uploaders/object_storage.rb index 7218cb0a0fc..30cc4425ae4 100644 --- a/app/uploaders/object_storage.rb +++ b/app/uploaders/object_storage.rb @@ -10,6 +10,9 @@ module ObjectStorage UnknownStoreError = Class.new(StandardError) ObjectStorageUnavailable = Class.new(StandardError) + DIRECT_UPLOAD_TIMEOUT = 4.hours + TMP_UPLOAD_PATH = 'tmp/upload'.freeze + module Store LOCAL = 1 REMOTE = 2 @@ -124,6 +127,10 @@ module ObjectStorage object_store_options.enabled end + def direct_upload_enabled? + object_store_options.direct_upload + end + def background_upload_enabled? object_store_options.background_upload end @@ -147,6 +154,45 @@ module ObjectStorage def serialization_column(model_class, mount_point) model_class.uploader_options.dig(mount_point, :mount_on) || mount_point end + + def workhorse_authorize + if options = workhorse_remote_upload_options + { RemoteObject: options } + else + { TempPath: workhorse_local_upload_path } + end + end + + def workhorse_local_upload_path + File.join(self.root, TMP_UPLOAD_PATH) + end + + def workhorse_remote_upload_options + return unless self.object_store_enabled? + return unless self.direct_upload_enabled? + + id = [CarrierWave.generate_cache_id, SecureRandom.hex].join('-') + upload_path = File.join(TMP_UPLOAD_PATH, id) + connection = ::Fog::Storage.new(self.object_store_credentials) + expire_at = Time.now + DIRECT_UPLOAD_TIMEOUT + options = { 'Content-Type' => 'application/octet-stream' } + + { + ID: id, + GetURL: connection.get_object_url(remote_store_path, upload_path, expire_at), + DeleteURL: connection.delete_object_url(remote_store_path, upload_path, expire_at), + StoreURL: connection.put_object_url(remote_store_path, upload_path, expire_at, options) + } + end + end + + # allow to configure and overwrite the filename + def filename + @filename || super || file&.filename # rubocop:disable Gitlab/ModuleWithInstanceVariables + end + + def filename=(filename) + @filename = filename # rubocop:disable Gitlab/ModuleWithInstanceVariables end def file_storage? @@ -195,10 +241,6 @@ module ObjectStorage end end - def filename - super || file&.filename - end - # # Move the file to another store # @@ -253,6 +295,18 @@ module ObjectStorage } end + def store_workhorse_file!(params, identifier) + filename = params["#{identifier}.name"] + + if remote_object_id = params["#{identifier}.remote_id"] + store_remote_file!(remote_object_id, filename) + elsif local_path = params["#{identifier}.path"] + store_local_file!(local_path, filename) + else + raise RemoteStoreError, 'Bad file' + end + end + private def schedule_background_upload? @@ -261,6 +315,38 @@ module ObjectStorage self.file_storage? end + def store_remote_file!(remote_object_id, filename) + raise RemoteStoreError, 'Missing filename' unless filename + + file_path = File.join(TMP_UPLOAD_PATH, remote_object_id) + file_path = Pathname.new(file_path).cleanpath.to_s + raise RemoteStoreError, 'Bad file path' unless file_path.start_with?(TMP_UPLOAD_PATH + '/') + + self.object_store = Store::REMOTE + + # TODO: + # This should be changed to make use of `tmp/cache` mechanism + # instead of using custom upload directory, + # using tmp/cache makes this implementation way easier than it is today + CarrierWave::Storage::Fog::File.new(self, storage, file_path).tap do |file| + raise RemoteStoreError, 'Missing file' unless file.exists? + + self.filename = filename + self.file = storage.store!(file) + end + end + + def store_local_file!(local_path, filename) + raise RemoteStoreError, 'Missing filename' unless filename + + root_path = File.realpath(self.class.workhorse_local_upload_path) + file_path = File.realpath(local_path) + raise RemoteStoreError, 'Bad file path' unless file_path.start_with?(root_path) + + self.object_store = Store::LOCAL + self.store!(UploadedFile.new(file_path, filename)) + end + # this is a hack around CarrierWave. The #migrate method needs to be # able to force the current file to the migrated file upon success. def file=(file) diff --git a/app/views/ci/lints/show.html.haml b/app/views/ci/lints/show.html.haml index 3c0881caa06..22f149d1caa 100644 --- a/app/views/ci/lints/show.html.haml +++ b/app/views/ci/lints/show.html.haml @@ -1,27 +1,9 @@ -- page_title "CI Lint" -- page_description "Validate your GitLab CI configuration file" -- content_for :library_javascripts do - = page_specific_javascript_tag('lib/ace.js') - -%h2 Check your .gitlab-ci.yml - -.ci-linter - .row - = form_tag ci_lint_path, method: :post do - .form-group - .col-sm-12 - .file-holder - .js-file-title.file-title.clearfix - Content of .gitlab-ci.yml - #ci-editor.ci-editor= @content - = text_area_tag(:content, @content, class: 'hidden form-control span1', rows: 7, require: true) - .col-sm-12 - .pull-left.prepend-top-10 - = submit_tag('Validate', class: 'btn btn-success submit-yml') - .pull-right.prepend-top-10 - = button_tag('Clear', type: 'button', class: 'btn btn-default clear-yml') - - .row.prepend-top-20 - .col-sm-12 - .results.ci-template - = render partial: 'create' if defined?(@status) +.row.empty-state + .col-xs-12 + .svg-content + = image_tag 'illustrations/feature_moved.svg' + .col-xs-12 + .text-content.text-center + %h4= _("GitLab CI Linter has been moved") + %p + = _("To validate your GitLab CI configurations, go to 'CI/CD → Pipelines' inside your project, and click on the 'CI Lint' button.") diff --git a/app/views/ci/lints/_create.html.haml b/app/views/projects/ci/lints/_create.html.haml index 30bf1384b22..30bf1384b22 100644 --- a/app/views/ci/lints/_create.html.haml +++ b/app/views/projects/ci/lints/_create.html.haml diff --git a/app/views/projects/ci/lints/show.html.haml b/app/views/projects/ci/lints/show.html.haml new file mode 100644 index 00000000000..6ca8152183d --- /dev/null +++ b/app/views/projects/ci/lints/show.html.haml @@ -0,0 +1,27 @@ +- page_title "CI Lint" +- page_description "Validate your GitLab CI configuration file" +- content_for :library_javascripts do + = page_specific_javascript_tag('lib/ace.js') + +%h2 Check your .gitlab-ci.yml + +.project-ci-linter + .row + = form_tag project_ci_lint_path(@project), method: :post do + .form-group + .col-sm-12 + .file-holder + .js-file-title.file-title.clearfix + Content of .gitlab-ci.yml + #ci-editor.ci-editor= @content + = text_area_tag(:content, @content, class: 'hidden form-control span1', rows: 7, require: true) + .col-sm-12 + .pull-left.prepend-top-10 + = submit_tag('Validate', class: 'btn btn-success submit-yml') + .pull-right.prepend-top-10 + = button_tag('Clear', type: 'button', class: 'btn btn-default clear-yml') + + .row.prepend-top-20 + .col-sm-12 + .results.project-ci-template + = render partial: 'create' if defined?(@status) diff --git a/app/views/projects/pipelines/index.html.haml b/app/views/projects/pipelines/index.html.haml index 3e6b3346787..c0ee81fe28d 100644 --- a/app/views/projects/pipelines/index.html.haml +++ b/app/views/projects/pipelines/index.html.haml @@ -10,6 +10,6 @@ "no-pipelines-svg-path" => image_path('illustrations/pipelines_pending.svg'), "can-create-pipeline" => can?(current_user, :create_pipeline, @project).to_s, "new-pipeline-path" => can?(current_user, :create_pipeline, @project) && new_project_pipeline_path(@project), - "ci-lint-path" => can?(current_user, :create_pipeline, @project) && ci_lint_path, + "ci-lint-path" => can?(current_user, :create_pipeline, @project) && project_ci_lint_path(@project), "reset-cache-path" => can?(current_user, :admin_pipeline, @project) && reset_cache_project_settings_ci_cd_path(@project) , "has-gitlab-ci" => (@project.has_ci? && @project.builds_enabled?).to_s } } diff --git a/changelogs/unreleased/38167-ui-bug-when-creating-new-branch.yml b/changelogs/unreleased/38167-ui-bug-when-creating-new-branch.yml new file mode 100644 index 00000000000..cec06bf2dfe --- /dev/null +++ b/changelogs/unreleased/38167-ui-bug-when-creating-new-branch.yml @@ -0,0 +1,5 @@ +--- +title: Fixed bug in dropdown selector when selecting the same selection again +merge_request: 14631 +author: bitsapien +type: fixed diff --git a/changelogs/unreleased/43603-ci-lint-support.yml b/changelogs/unreleased/43603-ci-lint-support.yml new file mode 100644 index 00000000000..8e4a92c0287 --- /dev/null +++ b/changelogs/unreleased/43603-ci-lint-support.yml @@ -0,0 +1,5 @@ +--- +title: Move ci/lint under project's namespace +merge_request: 17729 +author: +type: added diff --git a/changelogs/unreleased/44232-docs-for-runner-ip-address.yml b/changelogs/unreleased/44232-docs-for-runner-ip-address.yml deleted file mode 100644 index 82485d31b24..00000000000 --- a/changelogs/unreleased/44232-docs-for-runner-ip-address.yml +++ /dev/null @@ -1,5 +0,0 @@ ---- -title: Add documentation for runner IP address (#44232) -merge_request: 17837 -author: -type: other diff --git a/changelogs/unreleased/44564-error-500-while-attempting-to-resolve-conflicts-due-to-utf-8-conversion-error.yml b/changelogs/unreleased/44564-error-500-while-attempting-to-resolve-conflicts-due-to-utf-8-conversion-error.yml deleted file mode 100644 index 3fb96153b9c..00000000000 --- a/changelogs/unreleased/44564-error-500-while-attempting-to-resolve-conflicts-due-to-utf-8-conversion-error.yml +++ /dev/null @@ -1,5 +0,0 @@ ---- -title: Fix 500 error when trying to resolve non-ASCII conflicts in the editor -merge_request: 17962 -author: -type: fixed diff --git a/changelogs/unreleased/44712-update-asciidoctor-from-1-5-3-to-1-5-6-2.yml b/changelogs/unreleased/44712-update-asciidoctor-from-1-5-3-to-1-5-6-2.yml new file mode 100644 index 00000000000..bdfed89d2ea --- /dev/null +++ b/changelogs/unreleased/44712-update-asciidoctor-from-1-5-3-to-1-5-6-2.yml @@ -0,0 +1,5 @@ +--- +title: Update asciidoctor-plantuml to 0.0.8 +merge_request: 18022 +author: Takuya Noguchi +type: performance diff --git a/changelogs/unreleased/ab-44446-add-indexes-for-user-activity-queries.yml b/changelogs/unreleased/ab-44446-add-indexes-for-user-activity-queries.yml deleted file mode 100644 index 0f89c06fcee..00000000000 --- a/changelogs/unreleased/ab-44446-add-indexes-for-user-activity-queries.yml +++ /dev/null @@ -1,5 +0,0 @@ ---- -title: Add indexes for user activity queries. -merge_request: 17890 -author: -type: performance diff --git a/changelogs/unreleased/ab-44467-remove-index.yml b/changelogs/unreleased/ab-44467-remove-index.yml new file mode 100644 index 00000000000..fb772ce85d5 --- /dev/null +++ b/changelogs/unreleased/ab-44467-remove-index.yml @@ -0,0 +1,5 @@ +--- +title: Remove unused index from events table. +merge_request: 18014 +author: +type: other diff --git a/changelogs/unreleased/ac-lfs-direct-upload-ee-to-ce.yml b/changelogs/unreleased/ac-lfs-direct-upload-ee-to-ce.yml new file mode 100644 index 00000000000..4db7f76e0af --- /dev/null +++ b/changelogs/unreleased/ac-lfs-direct-upload-ee-to-ce.yml @@ -0,0 +1,5 @@ +--- +title: Port direct upload of LFS artifacts from EE +merge_request: 17752 +author: +type: added diff --git a/changelogs/unreleased/dashboard-view-user-choices-issues-merge-requests.yml b/changelogs/unreleased/dashboard-view-user-choices-issues-merge-requests.yml new file mode 100644 index 00000000000..92a03070d78 --- /dev/null +++ b/changelogs/unreleased/dashboard-view-user-choices-issues-merge-requests.yml @@ -0,0 +1,5 @@ +--- +title: Add 'Assigned Issues' and 'Assigned Merge Requests' as dashboard view choices for users +merge_request: 17860 +author: Elias Werberich +type: added diff --git a/changelogs/unreleased/dm-refs-contains-sha-encoding.yml b/changelogs/unreleased/dm-refs-contains-sha-encoding.yml new file mode 100644 index 00000000000..cdd9ead5a65 --- /dev/null +++ b/changelogs/unreleased/dm-refs-contains-sha-encoding.yml @@ -0,0 +1,5 @@ +--- +title: Fix listing commit branch/tags that contain special characters +merge_request: +author: +type: fixed diff --git a/changelogs/unreleased/fix-ci-job-auto-retry.yml b/changelogs/unreleased/fix-ci-job-auto-retry.yml deleted file mode 100644 index 442126461f0..00000000000 --- a/changelogs/unreleased/fix-ci-job-auto-retry.yml +++ /dev/null @@ -1,5 +0,0 @@ ---- -title: Prevent auto-retry AccessDenied error from stopping transition to failed -merge_request: 17862 -author: -type: fixed diff --git a/changelogs/unreleased/reduce-query-count-for-mergerequestscontroller-show.yml b/changelogs/unreleased/reduce-query-count-for-mergerequestscontroller-show.yml new file mode 100644 index 00000000000..1f793fe5e7c --- /dev/null +++ b/changelogs/unreleased/reduce-query-count-for-mergerequestscontroller-show.yml @@ -0,0 +1,5 @@ +--- +title: Reduce number of queries when viewing a merge request +merge_request: +author: +type: performance diff --git a/changelogs/unreleased/sh-update-loofah.yml b/changelogs/unreleased/sh-update-loofah.yml deleted file mode 100644 index 6aff0f91939..00000000000 --- a/changelogs/unreleased/sh-update-loofah.yml +++ /dev/null @@ -1,5 +0,0 @@ ---- -title: Bump rails-html-sanitizer to 1.0.4 -merge_request: -author: -type: security diff --git a/changelogs/unreleased/update-unresolved-discussions-vue-component.yml b/changelogs/unreleased/update-unresolved-discussions-vue-component.yml new file mode 100644 index 00000000000..246eaaae2bd --- /dev/null +++ b/changelogs/unreleased/update-unresolved-discussions-vue-component.yml @@ -0,0 +1,5 @@ +--- +title: Add i18n and update specs for ShaMismatch vue component +merge_request: 17870 +author: George Tsiolis +type: performance diff --git a/changelogs/unreleased/workhorse-gitaly-mandatory.yml b/changelogs/unreleased/workhorse-gitaly-mandatory.yml new file mode 100644 index 00000000000..dc33aba48aa --- /dev/null +++ b/changelogs/unreleased/workhorse-gitaly-mandatory.yml @@ -0,0 +1,5 @@ +--- +title: Make all workhorse gitaly calls opt-out +merge_request: 18002 +author: +type: other diff --git a/config/gitlab.yml.example b/config/gitlab.yml.example index a23145de3e5..8db66037d61 100644 --- a/config/gitlab.yml.example +++ b/config/gitlab.yml.example @@ -711,7 +711,7 @@ test: provider: AWS # Only AWS supported at the moment aws_access_key_id: AWS_ACCESS_KEY_ID aws_secret_access_key: AWS_SECRET_ACCESS_KEY - region: eu-central-1 + region: us-east-1 artifacts: path: tmp/tests/artifacts enabled: true @@ -725,7 +725,7 @@ test: provider: AWS # Only AWS supported at the moment aws_access_key_id: AWS_ACCESS_KEY_ID aws_secret_access_key: AWS_SECRET_ACCESS_KEY - region: eu-central-1 + region: us-east-1 uploads: storage_path: tmp/tests/public object_store: @@ -734,7 +734,7 @@ test: provider: AWS # Only AWS supported at the moment aws_access_key_id: AWS_ACCESS_KEY_ID aws_secret_access_key: AWS_SECRET_ACCESS_KEY - region: eu-central-1 + region: us-east-1 gitlab: host: localhost port: 80 diff --git a/config/initializers/1_settings.rb b/config/initializers/1_settings.rb index 906ae8b6180..69b59b26d8c 100644 --- a/config/initializers/1_settings.rb +++ b/config/initializers/1_settings.rb @@ -350,6 +350,7 @@ Settings.lfs['storage_path'] = Settings.absolute(Settings.lfs['storage_path'] || Settings.lfs['object_store'] ||= Settingslogic.new({}) Settings.lfs['object_store']['enabled'] = false if Settings.lfs['object_store']['enabled'].nil? Settings.lfs['object_store']['remote_directory'] ||= nil +Settings.lfs['object_store']['direct_upload'] = false if Settings.lfs['object_store']['direct_upload'].nil? Settings.lfs['object_store']['background_upload'] = true if Settings.lfs['object_store']['background_upload'].nil? Settings.lfs['object_store']['proxy_download'] = false if Settings.lfs['object_store']['proxy_download'].nil? # Convert upload connection settings to use string keys, to make Fog happy diff --git a/config/initializers/carrierwave.rb b/config/initializers/carrierwave.rb index cd7df44351a..5cde6cbb0ff 100644 --- a/config/initializers/carrierwave.rb +++ b/config/initializers/carrierwave.rb @@ -28,16 +28,4 @@ if File.exist?(aws_file) # when fog_public is false and provider is AWS or Google, defaults to 600 config.fog_authenticated_url_expiration = 1 << 29 end - - # Mocking Fog requests, based on: https://github.com/carrierwaveuploader/carrierwave/wiki/How-to%3A-Test-Fog-based-uploaders - if Rails.env.test? - Fog.mock! - connection = ::Fog::Storage.new( - aws_access_key_id: AWS_CONFIG['access_key_id'], - aws_secret_access_key: AWS_CONFIG['secret_access_key'], - provider: 'AWS', - region: AWS_CONFIG['region'] - ) - connection.directories.create(key: AWS_CONFIG['bucket']) - end end diff --git a/config/routes/ci.rb b/config/routes/ci.rb index 60c1724bc05..ebd321ed097 100644 --- a/config/routes/ci.rb +++ b/config/routes/ci.rb @@ -1,5 +1,5 @@ namespace :ci do - resource :lint, only: [:show, :create] + resource :lint, only: :show root to: redirect('') end diff --git a/config/routes/project.rb b/config/routes/project.rb index f50b9aded8d..48ba8ef06f9 100644 --- a/config/routes/project.rb +++ b/config/routes/project.rb @@ -280,6 +280,10 @@ constraints(::Constraints::ProjectUrlConstrainer.new) do post :keep end end + + namespace :ci do + resource :lint, only: [:show, :create] + end end draw :legacy_builds diff --git a/db/migrate/20180327101207_remove_index_from_events_table.rb b/db/migrate/20180327101207_remove_index_from_events_table.rb new file mode 100644 index 00000000000..172441da65b --- /dev/null +++ b/db/migrate/20180327101207_remove_index_from_events_table.rb @@ -0,0 +1,18 @@ +# See http://doc.gitlab.com/ce/development/migration_style_guide.html +# for more information on how to write migrations for GitLab. + +class RemoveIndexFromEventsTable < ActiveRecord::Migration + include Gitlab::Database::MigrationHelpers + + DOWNTIME = false + + disable_ddl_transaction! + + def up + remove_concurrent_index :events, :author_id + end + + def down + add_concurrent_index :events, :author_id + end +end diff --git a/db/schema.rb b/db/schema.rb index b3b2d5b0da9..3bf42080870 100644 --- a/db/schema.rb +++ b/db/schema.rb @@ -11,7 +11,7 @@ # # It's strongly recommended that you check this file into your version control system. -ActiveRecord::Schema.define(version: 20180323150945) do +ActiveRecord::Schema.define(version: 20180327101207) do # These are extensions that must be enabled in order to support this database enable_extension "plpgsql" @@ -732,7 +732,6 @@ ActiveRecord::Schema.define(version: 20180323150945) do add_index "events", ["action"], name: "index_events_on_action", using: :btree add_index "events", ["author_id", "project_id"], name: "index_events_on_author_id_and_project_id", using: :btree - add_index "events", ["author_id"], name: "index_events_on_author_id", using: :btree add_index "events", ["project_id", "id"], name: "index_events_on_project_id_and_id", using: :btree add_index "events", ["target_type", "target_id"], name: "index_events_on_target_type_and_target_id", using: :btree diff --git a/doc/administration/index.md b/doc/administration/index.md index 69efaf75140..4366590578a 100644 --- a/doc/administration/index.md +++ b/doc/administration/index.md @@ -111,6 +111,7 @@ server with IMAP authentication on Ubuntu, to be used with Reply by email. - [Enable/disable GitLab CI/CD](../ci/enable_or_disable_ci.md#site-wide-admin-setting): Enable or disable GitLab CI/CD for your instance. - [GitLab CI/CD admin settings](../user/admin_area/settings/continuous_integration.md): Define max artifacts size and expiration time. - [Job artifacts](job_artifacts.md): Enable, disable, and configure job artifacts (a set of files and directories which are outputted by a job when it completes successfully). +- [Job traces](job_traces.md): Information about the job traces (logs). - [Artifacts size and expiration](../user/admin_area/settings/continuous_integration.md#maximum-artifacts-size): Define maximum artifacts limits and expiration date. - [Register Shared and specific Runners](../ci/runners/README.md#registering-a-shared-runner): Learn how to register and configure Shared and specific Runners to your own instance. - [Shared Runners pipelines quota](../user/admin_area/settings/continuous_integration.md#shared-runners-pipeline-minutes-quota): Limit the usage of pipeline minutes for Shared Runners. diff --git a/doc/administration/job_traces.md b/doc/administration/job_traces.md new file mode 100644 index 00000000000..84a1ffeec98 --- /dev/null +++ b/doc/administration/job_traces.md @@ -0,0 +1,42 @@ +# Job traces (logs) + +By default, all job traces (logs) are saved to `/var/opt/gitlab/gitlab-ci/builds` +and `/home/git/gitlab/builds` for Omnibus packages and installations from source +respectively. The job logs are organized by year and month (for example, `2017_03`), +and then by project ID. + +There isn't a way to automatically expire old job logs, but it's safe to remove +them if they're taking up too much space. If you remove the logs manually, the +job output in the UI will be empty. + +## Changing the job traces location + +To change the location where the job logs will be stored, follow the steps below. + +**In Omnibus installations:** + +1. Edit `/etc/gitlab/gitlab.rb` and add or amend the following line: + + ``` + gitlab_ci['builds_directory'] = '/mnt/to/gitlab-ci/builds' + ``` + +1. Save the file and [reconfigure GitLab][] for the changes to take effect. + +--- + +**In installations from source:** + +1. Edit `/home/git/gitlab/config/gitlab.yml` and add or amend the following lines: + + ```yaml + gitlab_ci: + # The location where build traces are stored (default: builds/). + # Relative paths are relative to Rails.root. + builds_path: path/to/builds/ + ``` + +1. Save the file and [restart GitLab][] for the changes to take effect. + +[reconfigure gitlab]: restart_gitlab.md#omnibus-gitlab-reconfigure "How to reconfigure Omnibus GitLab" +[restart gitlab]: restart_gitlab.md#installations-from-source "How to restart GitLab" diff --git a/doc/ci/docker/using_docker_build.md b/doc/ci/docker/using_docker_build.md index 22afcb9199d..183808641c0 100644 --- a/doc/ci/docker/using_docker_build.md +++ b/doc/ci/docker/using_docker_build.md @@ -1,26 +1,29 @@ -# Using Docker Build +# Building Docker images with GitLab CI/CD -GitLab CI allows you to use Docker Engine to build and test docker-based projects. +GitLab CI/CD allows you to use Docker Engine to build and test docker-based projects. -**This also allows to you to use `docker-compose` and other docker-enabled tools.** +TIP: **Tip:** +This also allows to you to use `docker-compose` and other docker-enabled tools. One of the new trends in Continuous Integration/Deployment is to: -1. create an application image, -1. run tests against the created image, -1. push image to a remote registry, and -1. deploy to a server from the pushed image. +1. Create an application image +1. Run tests against the created image +1. Push image to a remote registry +1. Deploy to a server from the pushed image -It's also useful when your application already has the `Dockerfile` that can be used to create and test an image: +It's also useful when your application already has the `Dockerfile` that can be +used to create and test an image: ```bash -$ docker build -t my-image dockerfiles/ -$ docker run my-docker-image /script/to/run/tests -$ docker tag my-image my-registry:5000/my-image -$ docker push my-registry:5000/my-image +docker build -t my-image dockerfiles/ +docker run my-docker-image /script/to/run/tests +docker tag my-image my-registry:5000/my-image +docker push my-registry:5000/my-image ``` -This requires special configuration of GitLab Runner to enable `docker` support during jobs. +This requires special configuration of GitLab Runner to enable `docker` support +during jobs. ## Runner Configuration @@ -74,8 +77,8 @@ GitLab Runner then executes job scripts as the `gitlab-runner` user. 5. You can now use `docker` command and install `docker-compose` if needed. -> **Note:** -* By adding `gitlab-runner` to the `docker` group you are effectively granting `gitlab-runner` full root permissions. +NOTE: **Note:** +By adding `gitlab-runner` to the `docker` group you are effectively granting `gitlab-runner` full root permissions. For more information please read [On Docker security: `docker` group considered harmful](https://www.andreas-jung.com/contents/on-docker-security-docker-group-considered-harmful). ### Use docker-in-docker executor @@ -259,8 +262,66 @@ aware of the following implications: docker run --rm -t -i -v $(pwd)/src:/home/app/src test-image:latest run_app_tests ``` +## Making docker-in-docker builds faster with Docker layer caching + +When using docker-in-docker, Docker will download all layers of your image every +time you create a build. Recent versions of Docker (Docker 1.13 and above) can +use a pre-existing image as a cache during the `docker build` step, considerably +speeding up the build process. + +### How Docker caching works + +When running `docker build`, each command in `Dockerfile` results in a layer. +These layers are kept around as a cache and can be reused if there haven't been +any changes. Change in one layer causes all subsequent layers to be recreated. + +You can specify a tagged image to be used as a cache source for the `docker build` +command by using the `--cache-from` argument. Multiple images can be specified +as a cache source by using multiple `--cache-from` arguments. Keep in mind that +any image that's used with the `--cache-from` argument must first be pulled +(using `docker pull`) before it can be used as a cache source. + +### Using Docker caching + +Here's a simple `.gitlab-ci.yml` file showing how Docker caching can be utilized: + +```yaml +image: docker:latest + +services: + - docker:dind + +variables: + CONTAINER_IMAGE: registry.gitlab.com/$CI_PROJECT_PATH + DOCKER_DRIVER: overlay2 + +before_script: + - docker login -u gitlab-ci-token -p $CI_JOB_TOKEN registry.gitlab.com + +build: + stage: build + script: + - docker pull $CONTAINER_IMAGE:latest || true + - docker build --cache-from $CONTAINER_IMAGE:latest --tag $CONTAINER_IMAGE:$CI_BUILD_REF --tag $CONTAINER_IMAGE:latest . + - docker push $CONTAINER_IMAGE:$CI_BUILD_REF + - docker push $CONTAINER_IMAGE:latest +``` + +The steps in the `script` section for the `build` stage can be summed up to: + +1. The first command tries to pull the image from the registry so that it can be + used as a cache for the `docker build` command. +1. The second command builds a Docker image using the pulled image as a + cache (notice the `--cache-from $CONTAINER_IMAGE:latest` argument) if + available, and tags it. +1. The last two commands push the tagged Docker images to the container registry + so that they may also be used as cache for subsequent builds. + ## Using the OverlayFS driver +NOTE: **Note:** +The shared Runners on GitLab.com use the `overlay2` driver by default. + By default, when using `docker:dind`, Docker uses the `vfs` storage driver which copies the filesystem on every run. This is a very disk-intensive operation which can be avoided if a different driver is used, for example `overlay2`. diff --git a/doc/ci/examples/README.md b/doc/ci/examples/README.md index c1e258aedca..de60cd27cd1 100644 --- a/doc/ci/examples/README.md +++ b/doc/ci/examples/README.md @@ -49,6 +49,10 @@ There's also a collection of repositories with [example projects](https://gitlab **(Ultimate)** [Scan your code for vulnerabilities](https://docs.gitlab.com/ee/ci/examples/sast.html) +## Dependency Scanning + +**(Ultimate)** [Scan your dependencies for vulnerabilities](https://docs.gitlab.com/ee/ci/examples/dependency_scanning.html) + ## Container Scanning [Scan your Docker images for vulnerabilities](container_scanning.md) diff --git a/doc/ci/quick_start/README.md b/doc/ci/quick_start/README.md index e504b81eae8..f64e868d390 100644 --- a/doc/ci/quick_start/README.md +++ b/doc/ci/quick_start/README.md @@ -104,8 +104,8 @@ Jobs are used to create jobs, which are then picked by What is important is that each job is run independently from each other. -If you want to check whether your `.gitlab-ci.yml` file is valid, there is a -Lint tool under the page `/ci/lint` of your GitLab instance. You can also find +If you want to check whether the `.gitlab-ci.yml` of your project is valid, there is a +Lint tool under the page `/ci/lint` of your project namespace. You can also find a "CI Lint" button to go to this page under **CI/CD ➔ Pipelines** and **Pipelines ➔ Jobs** in your project. diff --git a/doc/ci/yaml/README.md b/doc/ci/yaml/README.md index 7184f3367be..c2b06e53c2f 100644 --- a/doc/ci/yaml/README.md +++ b/doc/ci/yaml/README.md @@ -1526,8 +1526,9 @@ capitalization, the commit will be created but the pipeline will be skipped. ## Validate the .gitlab-ci.yml -Each instance of GitLab CI has an embedded debug tool called Lint. -You can find the link under `/ci/lint` of your gitlab instance. +Each instance of GitLab CI has an embedded debug tool called Lint, which validates the +content of your `.gitlab-ci.yml` files. You can find the Lint under the page `ci/lint` of your +project namespace (e.g, `http://gitlab-example.com/gitlab-org/project-123/ci/lint`) ## Using reserved keywords diff --git a/doc/topics/autodevops/index.md b/doc/topics/autodevops/index.md index 4dc3adc1441..e88b787187c 100644 --- a/doc/topics/autodevops/index.md +++ b/doc/topics/autodevops/index.md @@ -20,6 +20,7 @@ project in an easy and automatic way: 1. [Auto Test](#auto-test) 1. [Auto Code Quality](#auto-code-quality) 1. [Auto SAST (Static Application Security Testing)](#auto-sast) +1. [Auto Dependency Scanning](#auto-dependency-scanning) 1. [Auto Container Scanning](#auto-container-scanning) 1. [Auto Review Apps](#auto-review-apps) 1. [Auto DAST (Dynamic Application Security Testing)](#auto-dast) @@ -95,7 +96,7 @@ Auto Deploy, and Auto Monitoring will be silently skipped. The Auto DevOps base domain is required if you want to make use of [Auto Review Apps](#auto-review-apps) and [Auto Deploy](#auto-deploy). It is defined -either under the project's CI/CD settings while +either under the project's CI/CD settings while [enabling Auto DevOps](#enabling-auto-devops) or in instance-wide settings in the CI/CD section. It can also be set at the project or group level as a variable, `AUTO_DEVOPS_DOMAIN`. @@ -209,7 +210,7 @@ target branches are also > Introduced in [GitLab Ultimate][ee] 10.3. Static Application Security Testing (SAST) uses the -[gl-sast Docker image](https://gitlab.com/gitlab-org/gl-sast) to run static +[SAST Docker image](https://gitlab.com/gitlab-org/security-products/sast) to run static analysis on the current code and checks for potential security issues. Once the report is created, it's uploaded as an artifact which you can later download and check out. @@ -217,6 +218,19 @@ check out. In GitLab Ultimate, any security warnings are also [shown in the merge request widget](https://docs.gitlab.com/ee/user/project/merge_requests/sast.html). +### Auto Dependency Scanning + +> Introduced in [GitLab Ultimate][ee] 10.7. + +Dependency Scanning uses the +[Dependency Scanning Docker image](https://gitlab.com/gitlab-org/security-products/dependency-scanning) +to run analysis on the project dependencies and checks for potential security issues. Once the +report is created, it's uploaded as an artifact which you can later download and +check out. + +In GitLab Ultimate, any security warnings are also +[shown in the merge request widget](https://docs.gitlab.com/ee/user/project/merge_requests/dependency_scanning.html). + ### Auto Container Scanning > Introduced in GitLab 10.4. diff --git a/doc/user/profile/preferences.md b/doc/user/profile/preferences.md index 022d6317555..930e506802a 100644 --- a/doc/user/profile/preferences.md +++ b/doc/user/profile/preferences.md @@ -41,7 +41,7 @@ select few, the amount of activity on the default Dashboard page can be overwhelming. Changing this setting allows you to redefine what your default dashboard will be. -You have 6 options here that you can use for your default dashboard view: +You have 8 options here that you can use for your default dashboard view: - Your projects (default) - Starred projects @@ -49,6 +49,8 @@ You have 6 options here that you can use for your default dashboard view: - Starred projects' activity - Your groups - Your [Todos] +- Assigned Issues +- Assigned Merge Requests ### Project home page content diff --git a/doc/user/project/integrations/prometheus_library/kubernetes.md b/doc/user/project/integrations/prometheus_library/kubernetes.md index 8ac753c07bf..6b190deaa6c 100644 --- a/doc/user/project/integrations/prometheus_library/kubernetes.md +++ b/doc/user/project/integrations/prometheus_library/kubernetes.md @@ -11,10 +11,17 @@ integration services must be enabled. ## Metrics supported -| Name | Query | -| ---- | ----- | -| Average Memory Usage (MB) | avg(sum(container_memory_usage_bytes{container_name!="POD",pod_name=~"^%{ci_environment_slug}-([^c].*|c([^a]|a([^n]|n([^a]|a([^r]|r[^y])))).*|)-(.*)",namespace="%{kube_namespace}"}) by (job)) without (job) / count(avg(container_memory_usage_bytes{container_name!="POD",pod_name=~"^%{ci_environment_slug}-([^c].*|c([^a]|a([^n]|n([^a]|a([^r]|r[^y])))).*|)-(.*)",namespace="%{kube_namespace}"}) without (job)) /1024/1024 | -| Average CPU Utilization (%) | avg(sum(rate(container_cpu_usage_seconds_total{container_name!="POD",pod_name=~"^%{ci_environment_slug}-([^c].*|c([^a]|a([^n]|n([^a]|a([^r]|r[^y])))).*|)-(.*)",namespace="%{kube_namespace}"}[15m])) by (job)) without (job) / count(sum(rate(container_cpu_usage_seconds_total{container_name!="POD",pod_name=~"^%{ci_environment_slug}-([^c].*|c([^a]|a([^n]|n([^a]|a([^r]|r[^y])))).*|)-(.*)",namespace="%{kube_namespace}"}[15m])) by (pod_name)) | +- Average Memory Usage (MB): + + ``` + avg(sum(container_memory_usage_bytes{container_name!="POD",pod_name=~"^%{ci_environment_slug}-([^c].*|c([^a]|a([^n]|n([^a]|a([^r]|r[^y])))).*|)-(.*)",namespace="%{kube_namespace}"}) by (job)) without (job) / count(avg(container_memory_usage_bytes{container_name!="POD",pod_name=~"^%{ci_environment_slug}-([^c].*|c([^a]|a([^n]|n([^a]|a([^r]|r[^y])))).*|)-(.*)",namespace="%{kube_namespace}"}) without (job)) /1024/1024 + ``` + +- Average CPU Utilization (%): + + ``` + avg(sum(rate(container_cpu_usage_seconds_total{container_name!="POD",pod_name=~"^%{ci_environment_slug}-([^c].*|c([^a]|a([^n]|n([^a]|a([^r]|r[^y])))).*|)-(.*)",namespace="%{kube_namespace}"}[15m])) by (job)) without (job) / count(sum(rate(container_cpu_usage_seconds_total{container_name!="POD",pod_name=~"^%{ci_environment_slug}-([^c].*|c([^a]|a([^n]|n([^a]|a([^r]|r[^y])))).*|)-(.*)",namespace="%{kube_namespace}"}[15m])) by (pod_name)) + ``` ## Configuring Prometheus to monitor for Kubernetes metrics diff --git a/doc/user/project/merge_requests/img/remove_source_branch_status.png b/doc/user/project/merge_requests/img/remove_source_branch_status.png Binary files differnew file mode 100644 index 00000000000..1377fab54ec --- /dev/null +++ b/doc/user/project/merge_requests/img/remove_source_branch_status.png diff --git a/doc/user/project/merge_requests/index.md b/doc/user/project/merge_requests/index.md index 10d67729734..3640d236db4 100644 --- a/doc/user/project/merge_requests/index.md +++ b/doc/user/project/merge_requests/index.md @@ -77,6 +77,22 @@ You can [search and filter the results](../../search/index.md#issues-and-merge-r ![Group Issues list view](img/group_merge_requests_list_view.png) +## Removing the source branch + +When creating a merge request, select the "Remove source branch when merge +request accepted" option and the source branch will be removed when the merge +request is merged. + +This option is also visible in an existing merge request next to the merge +request button and can be selected/deselected before merging. It's only visible +to users with [Master permissions](../../permissions.md) in the source project. + +If the user viewing the merge request does not have the correct permissions to +remove the source branch and the source branch is set for removal, the merge +request widget will show the "Removes source branch" text. + +![Remove source branch status](img/remove_source_branch_status.png) + ## Authorization for merge requests There are two main ways to have a merge request flow with GitLab: diff --git a/doc/workflow/lfs/lfs_administration.md b/doc/workflow/lfs/lfs_administration.md index ca28e0a3304..cac3cb599dd 100644 --- a/doc/workflow/lfs/lfs_administration.md +++ b/doc/workflow/lfs/lfs_administration.md @@ -63,6 +63,7 @@ For source installations the following settings are nested under `lfs:` and then |---------|-------------|---------| | `enabled` | Enable/disable object storage | `false` | | `remote_directory` | The bucket name where LFS objects will be stored| | +| `direct_upload` | Set to true to enable direct upload of LFS without the need of local shared storage. Option may be removed once we decide to support only single storage for all files. | `false` | | `background_upload` | Set to false to disable automatic upload. Option may be removed once upload is direct to S3 | `true` | | `proxy_download` | Set to true to enable proxying all files served. Option allows to reduce egress traffic as this allows clients to download directly from remote storage instead of proxying all data | `false` | | `connection` | Various connection options described below | | diff --git a/lib/api/protected_branches.rb b/lib/api/protected_branches.rb index 33321db46e9..aa7cab4a741 100644 --- a/lib/api/protected_branches.rb +++ b/lib/api/protected_branches.rb @@ -70,7 +70,10 @@ module API delete ':id/protected_branches/:name', requirements: BRANCH_ENDPOINT_REQUIREMENTS do protected_branch = user_project.protected_branches.find_by!(name: params[:name]) - destroy_conditionally!(protected_branch) + destroy_conditionally!(protected_branch) do + destroy_service = ::ProtectedBranches::DestroyService.new(user_project, current_user) + destroy_service.execute(protected_branch) + end end end end diff --git a/lib/gitlab/ci/config.rb b/lib/gitlab/ci/config.rb index f7ff7ea212e..66ac4a40616 100644 --- a/lib/gitlab/ci/config.rb +++ b/lib/gitlab/ci/config.rb @@ -4,7 +4,8 @@ module Gitlab # Base GitLab CI Configuration facade # class Config - def initialize(config) + # EE would override this and utilize opts argument + def initialize(config, opts = {}) @config = Loader.new(config).load! @global = Entry::Global.new(@config) diff --git a/lib/gitlab/ci/yaml_processor.rb b/lib/gitlab/ci/yaml_processor.rb index bc2a6f98dae..e829f2a95f8 100644 --- a/lib/gitlab/ci/yaml_processor.rb +++ b/lib/gitlab/ci/yaml_processor.rb @@ -7,8 +7,8 @@ module Gitlab attr_reader :cache, :stages, :jobs - def initialize(config) - @ci_config = Gitlab::Ci::Config.new(config) + def initialize(config, opts = {}) + @ci_config = Gitlab::Ci::Config.new(config, opts) @config = @ci_config.to_hash unless @ci_config.valid? @@ -73,11 +73,11 @@ module Gitlab end end - def self.validation_message(content) + def self.validation_message(content, opts = {}) return 'Please provide content of .gitlab-ci.yml' if content.blank? begin - Gitlab::Ci::YamlProcessor.new(content) + Gitlab::Ci::YamlProcessor.new(content, opts) nil rescue ValidationError => e e.message diff --git a/lib/gitlab/git/repository.rb b/lib/gitlab/git/repository.rb index 20b0647fce9..2d16a81c888 100644 --- a/lib/gitlab/git/repository.rb +++ b/lib/gitlab/git/repository.rb @@ -8,6 +8,7 @@ module Gitlab class Repository include Gitlab::Git::RepositoryMirroring include Gitlab::Git::Popen + include Gitlab::EncodingHelper ALLOWED_OBJECT_DIRECTORIES_VARIABLES = %w[ GIT_OBJECT_DIRECTORY @@ -1479,7 +1480,7 @@ module Gitlab names.lines.each do |line| next unless line.start_with?(refs_prefix) - refs << line.rstrip[left_slice_count..-1] + refs << encode_utf8(line.rstrip[left_slice_count..-1]) end refs diff --git a/lib/gitlab/workhorse.rb b/lib/gitlab/workhorse.rb index 0b0d667d4fd..1887f0dc2b3 100644 --- a/lib/gitlab/workhorse.rb +++ b/lib/gitlab/workhorse.rb @@ -39,20 +39,12 @@ module Gitlab params end - def lfs_upload_ok(oid, size) - { - StoreLFSPath: LfsObjectUploader.workhorse_upload_path, - LfsOid: oid, - LfsSize: size - } - end - def artifact_upload_ok { TempPath: JobArtifactUploader.workhorse_upload_path } end def send_git_blob(repository, blob) - params = if Gitlab::GitalyClient.feature_enabled?(:workhorse_raw_show) + params = if Gitlab::GitalyClient.feature_enabled?(:workhorse_raw_show, Gitlab::GitalyClient::MigrationStatus::OPT_OUT) { 'GitalyServer' => gitaly_server_hash(repository), 'GetBlobRequest' => { @@ -80,7 +72,7 @@ module Gitlab params = repository.archive_metadata(ref, Gitlab.config.gitlab.repository_downloads_path, format) raise "Repository or ref not found" if params.empty? - if Gitlab::GitalyClient.feature_enabled?(:workhorse_archive) + if Gitlab::GitalyClient.feature_enabled?(:workhorse_archive, Gitlab::GitalyClient::MigrationStatus::OPT_OUT) params.merge!( 'GitalyServer' => gitaly_server_hash(repository), 'GitalyRepository' => repository.gitaly_repository.to_h @@ -97,7 +89,7 @@ module Gitlab end def send_git_diff(repository, diff_refs) - params = if Gitlab::GitalyClient.feature_enabled?(:workhorse_send_git_diff) + params = if Gitlab::GitalyClient.feature_enabled?(:workhorse_send_git_diff, Gitlab::GitalyClient::MigrationStatus::OPT_OUT) { 'GitalyServer' => gitaly_server_hash(repository), 'RawDiffRequest' => Gitaly::RawDiffRequest.new( @@ -115,7 +107,7 @@ module Gitlab end def send_git_patch(repository, diff_refs) - params = if Gitlab::GitalyClient.feature_enabled?(:workhorse_send_git_patch) + params = if Gitlab::GitalyClient.feature_enabled?(:workhorse_send_git_patch, Gitlab::GitalyClient::MigrationStatus::OPT_OUT) { 'GitalyServer' => gitaly_server_hash(repository), 'RawPatchRequest' => Gitaly::RawPatchRequest.new( diff --git a/package.json b/package.json index 56fd2575e91..31edc3a8016 100644 --- a/package.json +++ b/package.json @@ -121,5 +121,8 @@ "nodemon": "^1.15.1", "prettier": "1.11.1", "webpack-dev-server": "^2.11.2" + }, + "optionalDependencies": { + "fsevents": "^1.1.3" } } diff --git a/spec/controllers/projects/ci/lints_controller_spec.rb b/spec/controllers/projects/ci/lints_controller_spec.rb new file mode 100644 index 00000000000..1249a5528a9 --- /dev/null +++ b/spec/controllers/projects/ci/lints_controller_spec.rb @@ -0,0 +1,123 @@ +require 'spec_helper' + +describe Projects::Ci::LintsController do + let(:project) { create(:project, :repository) } + let(:user) { create(:user) } + + before do + sign_in(user) + end + + describe 'GET #show' do + context 'with enough privileges' do + before do + project.add_developer(user) + + get :show, namespace_id: project.namespace, project_id: project + end + + it 'should be success' do + expect(response).to be_success + end + + it 'should render show page' do + expect(response).to render_template :show + end + + it 'should retrieve project' do + expect(assigns(:project)).to eq(project) + end + end + + context 'without enough privileges' do + before do + project.add_guest(user) + + get :show, namespace_id: project.namespace, project_id: project + end + + it 'should respond with 404' do + expect(response).to have_gitlab_http_status(404) + end + end + end + + describe 'POST #create' do + let(:remote_file_path) { 'https://gitlab.com/gitlab-org/gitlab-ce/blob/1234/.gitlab-ci-1.yml' } + + let(:remote_file_content) do + <<~HEREDOC + before_script: + - apt-get update -qq && apt-get install -y -qq sqlite3 libsqlite3-dev nodejs + - ruby -v + - which ruby + - gem install bundler --no-ri --no-rdoc + - bundle install --jobs $(nproc) "${FLAGS[@]}" + HEREDOC + end + + let(:content) do + <<~HEREDOC + include: + - #{remote_file_path} + + rubocop: + script: + - bundle exec rubocop + HEREDOC + end + + context 'with a valid gitlab-ci.yml' do + before do + WebMock.stub_request(:get, remote_file_path).to_return(body: remote_file_content) + project.add_developer(user) + + post :create, namespace_id: project.namespace, project_id: project, content: content + end + + it 'should be success' do + expect(response).to be_success + end + + it 'render show page' do + expect(response).to render_template :show + end + + it 'should retrieve project' do + expect(assigns(:project)).to eq(project) + end + end + + context 'with an invalid gitlab-ci.yml' do + let(:content) do + <<~HEREDOC + rubocop: + scriptt: + - bundle exec rubocop + HEREDOC + end + + before do + project.add_developer(user) + + post :create, namespace_id: project.namespace, project_id: project, content: content + end + + it 'should assign errors' do + expect(assigns[:error]).to eq('jobs:rubocop config contains unknown keys: scriptt') + end + end + + context 'without enough privileges' do + before do + project.add_guest(user) + + post :create, namespace_id: project.namespace, project_id: project, content: content + end + + it 'should respond with 404' do + expect(response).to have_gitlab_http_status(404) + end + end + end +end diff --git a/spec/controllers/projects/protected_branches_controller_spec.rb b/spec/controllers/projects/protected_branches_controller_spec.rb index 80be135b5d8..096e29bc39f 100644 --- a/spec/controllers/projects/protected_branches_controller_spec.rb +++ b/spec/controllers/projects/protected_branches_controller_spec.rb @@ -1,6 +1,16 @@ require('spec_helper') describe Projects::ProtectedBranchesController do + let(:project) { create(:project, :repository) } + let(:protected_branch) { create(:protected_branch, project: project) } + let(:project_params) { { namespace_id: project.namespace.to_param, project_id: project } } + let(:base_params) { project_params.merge(id: protected_branch.id) } + let(:user) { create(:user) } + + before do + project.add_master(user) + end + describe "GET #index" do let(:project) { create(:project_empty_repo, :public) } @@ -8,4 +18,91 @@ describe Projects::ProtectedBranchesController do get(:index, namespace_id: project.namespace.to_param, project_id: project) end end + + describe "POST #create" do + let(:master_access_level) { [{ access_level: Gitlab::Access::MASTER }] } + let(:access_level_params) do + { merge_access_levels_attributes: master_access_level, + push_access_levels_attributes: master_access_level } + end + let(:create_params) { attributes_for(:protected_branch).merge(access_level_params) } + + before do + sign_in(user) + end + + it 'creates the protected branch rule' do + expect do + post(:create, project_params.merge(protected_branch: create_params)) + end.to change(ProtectedBranch, :count).by(1) + end + + context 'when a policy restricts rule deletion' do + before do + policy = instance_double(ProtectedBranchPolicy, can?: false) + allow(ProtectedBranchPolicy).to receive(:new).and_return(policy) + end + + it "prevents creation of the protected branch rule" do + post(:create, project_params.merge(protected_branch: create_params)) + + expect(ProtectedBranch.count).to eq 0 + end + end + end + + describe "PUT #update" do + let(:update_params) { { name: 'new_name' } } + + before do + sign_in(user) + end + + it 'updates the protected branch rule' do + put(:update, base_params.merge(protected_branch: update_params)) + + expect(protected_branch.reload.name).to eq('new_name') + expect(json_response["name"]).to eq('new_name') + end + + context 'when a policy restricts rule deletion' do + before do + policy = instance_double(ProtectedBranchPolicy, can?: false) + allow(ProtectedBranchPolicy).to receive(:new).and_return(policy) + end + + it "prevents update of the protected branch rule" do + old_name = protected_branch.name + + put(:update, base_params.merge(protected_branch: update_params)) + + expect(protected_branch.reload.name).to eq(old_name) + end + end + end + + describe "DELETE #destroy" do + before do + sign_in(user) + end + + it "deletes the protected branch rule" do + delete(:destroy, base_params) + + expect { ProtectedBranch.find(protected_branch.id) }.to raise_error(ActiveRecord::RecordNotFound) + end + + context 'when a policy restricts rule deletion' do + before do + policy = instance_double(ProtectedBranchPolicy, can?: false) + allow(ProtectedBranchPolicy).to receive(:new).and_return(policy) + end + + it "prevents deletion of the protected branch rule" do + delete(:destroy, base_params) + + expect(response.status).to eq(403) + end + end + end end diff --git a/spec/controllers/root_controller_spec.rb b/spec/controllers/root_controller_spec.rb index b32eb39b1fb..7688538a468 100644 --- a/spec/controllers/root_controller_spec.rb +++ b/spec/controllers/root_controller_spec.rb @@ -90,6 +90,30 @@ describe RootController do end end + context 'who has customized their dashboard setting for assigned issues' do + before do + user.dashboard = 'issues' + end + + it 'redirects to their assigned issues' do + get :index + + expect(response).to redirect_to issues_dashboard_path(assignee_id: user.id) + end + end + + context 'who has customized their dashboard setting for assigned merge requests' do + before do + user.dashboard = 'merge_requests' + end + + it 'redirects to their assigned merge requests' do + get :index + + expect(response).to redirect_to merge_requests_dashboard_path(assignee_id: user.id) + end + end + context 'who uses the default dashboard setting' do it 'renders the default dashboard' do get :index diff --git a/spec/features/ci_lint_spec.rb b/spec/features/projects/ci/lint_spec.rb index 220b934154e..313950072e7 100644 --- a/spec/features/ci_lint_spec.rb +++ b/spec/features/projects/ci/lint_spec.rb @@ -1,10 +1,14 @@ require 'spec_helper' describe 'CI Lint', :js do + let(:project) { create(:project, :repository) } + let(:user) { create(:user) } + before do - sign_in(create(:user)) + project.add_developer(user) + sign_in(user) - visit ci_lint_path + visit project_ci_lint_path(project) find('#ci-editor') execute_script("ace.edit('ci-editor').setValue(#{yaml_content.to_json});") diff --git a/spec/helpers/preferences_helper_spec.rb b/spec/helpers/preferences_helper_spec.rb index e2a0c4322ff..c9d2ec8a4ae 100644 --- a/spec/helpers/preferences_helper_spec.rb +++ b/spec/helpers/preferences_helper_spec.rb @@ -21,7 +21,9 @@ describe PreferencesHelper do ["Your Projects' Activity", 'project_activity'], ["Starred Projects' Activity", 'starred_project_activity'], ["Your Groups", 'groups'], - ["Your Todos", 'todos'] + ["Your Todos", 'todos'], + ["Assigned Issues", 'issues'], + ["Assigned Merge Requests", 'merge_requests'] ] end end diff --git a/spec/javascripts/fixtures/gl_dropdown.html.haml b/spec/javascripts/fixtures/gl_dropdown.html.haml index a20390c08ee..43d57c2c4dc 100644 --- a/spec/javascripts/fixtures/gl_dropdown.html.haml +++ b/spec/javascripts/fixtures/gl_dropdown.html.haml @@ -1,7 +1,8 @@ %div .dropdown.inline %button#js-project-dropdown.dropdown-menu-toggle{type: 'button', data: {toggle: 'dropdown'}} - Projects + .dropdown-toggle-text + Projects %i.fa.fa-chevron-down.dropdown-toggle-caret.js-projects-dropdown-toggle .dropdown-menu.dropdown-select.dropdown-menu-selectable .dropdown-title diff --git a/spec/javascripts/gl_dropdown_spec.js b/spec/javascripts/gl_dropdown_spec.js index 0e4a7017406..5393502196e 100644 --- a/spec/javascripts/gl_dropdown_spec.js +++ b/spec/javascripts/gl_dropdown_spec.js @@ -256,4 +256,29 @@ describe('glDropdown', function describeDropdown() { }); }); }); + + it('should keep selected item after selecting a second time', () => { + const options = { + isSelectable(item, $el) { + return !$el.hasClass('is-active'); + }, + toggleLabel(item) { + return item && item.id; + }, + }; + initDropDown.call(this, false, false, options); + const $item = $(`${ITEM_SELECTOR}:first() a`, this.$dropdownMenuElement); + + // select item the first time + this.dropdownButtonElement.click(); + $item.click(); + expect($item).toHaveClass('is-active'); + // select item the second time + this.dropdownButtonElement.click(); + $item.click(); + expect($item).toHaveClass('is-active'); + + expect($('.dropdown-toggle-text')).toHaveText(this.projectsData[0].id.toString()); + }); }); + diff --git a/spec/javascripts/helpers/vue_component_helper.js b/spec/javascripts/helpers/vue_component_helper.js new file mode 100644 index 00000000000..257c9f5526a --- /dev/null +++ b/spec/javascripts/helpers/vue_component_helper.js @@ -0,0 +1,3 @@ +export default function removeBreakLine (data) { + return data.replace(/\r?\n|\r/g, ' '); +} diff --git a/spec/javascripts/vue_mr_widget/components/states/mr_widget_conflicts_spec.js b/spec/javascripts/vue_mr_widget/components/states/mr_widget_conflicts_spec.js index 5323523abc0..fcbd8169bc7 100644 --- a/spec/javascripts/vue_mr_widget/components/states/mr_widget_conflicts_spec.js +++ b/spec/javascripts/vue_mr_widget/components/states/mr_widget_conflicts_spec.js @@ -1,6 +1,7 @@ import Vue from 'vue'; import conflictsComponent from '~/vue_merge_request_widget/components/states/mr_widget_conflicts.vue'; import mountComponent from 'spec/helpers/vue_mount_component_helper'; +import removeBreakLine from 'spec/helpers/vue_component_helper'; describe('MRWidgetConflicts', () => { let Component; @@ -78,8 +79,9 @@ describe('MRWidgetConflicts', () => { }); it('should tell you to rebase locally', () => { - expect(vm.$el.textContent.trim().replace(/\s\s+/g, ' ')).toContain('Fast-forward merge is not possible.'); - expect(vm.$el.textContent.trim().replace(/\s\s+/g, ' ')).toContain('To merge this request, first rebase locally'); + expect( + removeBreakLine(vm.$el.textContent).trim(), + ).toContain('Fast-forward merge is not possible. To merge this request, first rebase locally.'); }); }); }); diff --git a/spec/javascripts/vue_mr_widget/components/states/mr_widget_pipeline_blocked_spec.js b/spec/javascripts/vue_mr_widget/components/states/mr_widget_pipeline_blocked_spec.js index baacbc03fb1..894dbe3382f 100644 --- a/spec/javascripts/vue_mr_widget/components/states/mr_widget_pipeline_blocked_spec.js +++ b/spec/javascripts/vue_mr_widget/components/states/mr_widget_pipeline_blocked_spec.js @@ -1,6 +1,7 @@ import Vue from 'vue'; import pipelineBlockedComponent from '~/vue_merge_request_widget/components/states/mr_widget_pipeline_blocked.vue'; import mountComponent from 'spec/helpers/vue_mount_component_helper'; +import removeBreakLine from 'spec/helpers/vue_component_helper'; describe('MRWidgetPipelineBlocked', () => { let vm; @@ -18,6 +19,8 @@ describe('MRWidgetPipelineBlocked', () => { }); it('renders information text', () => { - expect(vm.$el.textContent.trim().replace(/[\r\n]+/g, ' ')).toContain('Pipeline blocked. The pipeline for this merge request requires a manual action to proceed'); + expect( + removeBreakLine(vm.$el.textContent).trim(), + ).toContain('Pipeline blocked. The pipeline for this merge request requires a manual action to proceed'); }); }); diff --git a/spec/javascripts/vue_mr_widget/components/states/mr_widget_sha_mismatch_spec.js b/spec/javascripts/vue_mr_widget/components/states/mr_widget_sha_mismatch_spec.js index 25684861724..b02af94d03a 100644 --- a/spec/javascripts/vue_mr_widget/components/states/mr_widget_sha_mismatch_spec.js +++ b/spec/javascripts/vue_mr_widget/components/states/mr_widget_sha_mismatch_spec.js @@ -1,17 +1,25 @@ import Vue from 'vue'; import ShaMismatch from '~/vue_merge_request_widget/components/states/sha_mismatch.vue'; +import mountComponent from 'spec/helpers/vue_mount_component_helper'; +import removeBreakLine from 'spec/helpers/vue_component_helper'; describe('ShaMismatch', () => { - describe('template', () => { + let vm; + + beforeEach(() => { const Component = Vue.extend(ShaMismatch); - const vm = new Component({ - el: document.createElement('div'), - }); - it('should have correct elements', () => { - expect(vm.$el.classList.contains('mr-widget-body')).toBeTruthy(); - expect(vm.$el.querySelector('button').getAttribute('disabled')).toBeTruthy(); - expect(vm.$el.innerText).toContain('The source branch HEAD has recently changed.'); - expect(vm.$el.innerText).toContain('Please reload the page and review the changes before merging.'); - }); + vm = mountComponent(Component); + }); + + afterEach(() => { + vm.$destroy(); + }); + + it('should render information message', () => { + expect(vm.$el.querySelector('button').disabled).toEqual(true); + + expect( + removeBreakLine(vm.$el.textContent).trim(), + ).toContain('The source branch HEAD has recently changed. Please reload the page and review the changes before merging'); }); }); diff --git a/spec/lib/backup/manager_spec.rb b/spec/lib/backup/manager_spec.rb index 5100f5737c2..84688845fa5 100644 --- a/spec/lib/backup/manager_spec.rb +++ b/spec/lib/backup/manager_spec.rb @@ -278,6 +278,10 @@ describe Backup::Manager do connection.directories.create(key: Gitlab.config.backup.upload.remote_directory) end + after do + Fog.unmock! + end + context 'target path' do it 'uses the tar filename by default' do expect_any_instance_of(Fog::Collection).to receive(:create) diff --git a/spec/lib/gitlab/asciidoc_spec.rb b/spec/lib/gitlab/asciidoc_spec.rb index 2a0e19ae796..e1782cff81a 100644 --- a/spec/lib/gitlab/asciidoc_spec.rb +++ b/spec/lib/gitlab/asciidoc_spec.rb @@ -48,7 +48,7 @@ module Gitlab }, 'images' => { input: 'image:https://localhost.com/image.png[Alt text" onerror="alert(7)]', - output: "<img src=\"https://localhost.com/image.png\" alt=\"Alt text\">" + output: "<div>\n<p><span><img src=\"https://localhost.com/image.png\" alt='Alt text\" onerror=\"alert(7)'></span></p>\n</div>" }, 'pre' => { input: '```mypre"><script>alert(3)</script>', diff --git a/spec/lib/gitlab/git/repository_spec.rb b/spec/lib/gitlab/git/repository_spec.rb index 54ada3e423f..0e315b3f49e 100644 --- a/spec/lib/gitlab/git/repository_spec.rb +++ b/spec/lib/gitlab/git/repository_spec.rb @@ -604,17 +604,20 @@ describe Gitlab::Git::Repository, seed_helper: true do shared_examples 'returning the right branches' do let(:head_id) { repository.rugged.head.target.oid } let(:new_branch) { head_id } + let(:utf8_branch) { 'branch-é' } before do repository.create_branch(new_branch, 'master') + repository.create_branch(utf8_branch, 'master') end after do repository.delete_branch(new_branch) + repository.delete_branch(utf8_branch) end it 'displays that branch' do - expect(repository.branch_names_contains_sha(head_id)).to include('master', new_branch) + expect(repository.branch_names_contains_sha(head_id)).to include('master', new_branch, utf8_branch) end end diff --git a/spec/lib/gitlab/workhorse_spec.rb b/spec/lib/gitlab/workhorse_spec.rb index 37a0bf1ad36..95b63fc91fc 100644 --- a/spec/lib/gitlab/workhorse_spec.rb +++ b/spec/lib/gitlab/workhorse_spec.rb @@ -55,7 +55,7 @@ describe Gitlab::Workhorse do end end - context 'when Gitaly workhorse_archive feature is disabled', :skip_gitaly_mock do + context 'when Gitaly workhorse_archive feature is disabled', :disable_gitaly do it 'sets the header correctly' do key, command, params = decode_workhorse_header(subject) @@ -100,7 +100,7 @@ describe Gitlab::Workhorse do end end - context 'when Gitaly workhorse_send_git_patch feature is disabled', :skip_gitaly_mock do + context 'when Gitaly workhorse_send_git_patch feature is disabled', :disable_gitaly do it 'sets the header correctly' do key, command, params = decode_workhorse_header(subject) @@ -173,7 +173,7 @@ describe Gitlab::Workhorse do end end - context 'when Gitaly workhorse_send_git_diff feature is disabled', :skip_gitaly_mock do + context 'when Gitaly workhorse_send_git_diff feature is disabled', :disable_gitaly do it 'sets the header correctly' do key, command, params = decode_workhorse_header(subject) @@ -455,7 +455,7 @@ describe Gitlab::Workhorse do end end - context 'when Gitaly workhorse_raw_show feature is disabled', :skip_gitaly_mock do + context 'when Gitaly workhorse_raw_show feature is disabled', :disable_gitaly do it 'sets the header correctly' do key, command, params = decode_workhorse_header(subject) diff --git a/spec/models/merge_request_spec.rb b/spec/models/merge_request_spec.rb index ff5a6f63010..f73f44ca0ad 100644 --- a/spec/models/merge_request_spec.rb +++ b/spec/models/merge_request_spec.rb @@ -1961,6 +1961,17 @@ describe MergeRequest do expect(subject.merge_request_diff_for(merge_request_diff3.head_commit_sha)).to eq(merge_request_diff3) end end + + it 'runs a single query on the initial call, and none afterwards' do + expect { subject.merge_request_diff_for(merge_request_diff1.diff_refs) } + .not_to exceed_query_limit(1) + + expect { subject.merge_request_diff_for(merge_request_diff2.diff_refs) } + .not_to exceed_query_limit(0) + + expect { subject.merge_request_diff_for(merge_request_diff3.head_commit_sha) } + .not_to exceed_query_limit(0) + end end describe '#version_params_for' do diff --git a/spec/policies/protected_branch_policy_spec.rb b/spec/policies/protected_branch_policy_spec.rb new file mode 100644 index 00000000000..b39de42d721 --- /dev/null +++ b/spec/policies/protected_branch_policy_spec.rb @@ -0,0 +1,22 @@ +require 'spec_helper' + +describe ProtectedBranchPolicy do + let(:user) { create(:user) } + let(:name) { 'feature' } + let(:protected_branch) { create(:protected_branch, name: name) } + let(:project) { protected_branch.project } + + subject { described_class.new(user, protected_branch) } + + it 'branches can be updated via project masters' do + project.add_master(user) + + is_expected.to be_allowed(:update_protected_branch) + end + + it "branches can't be updated by guests" do + project.add_guest(user) + + is_expected.to be_disallowed(:update_protected_branch) + end +end diff --git a/spec/requests/api/protected_branches_spec.rb b/spec/requests/api/protected_branches_spec.rb index 1d23e023bb6..576fde46615 100644 --- a/spec/requests/api/protected_branches_spec.rb +++ b/spec/requests/api/protected_branches_spec.rb @@ -193,6 +193,19 @@ describe API::ProtectedBranches do expect(json_response['merge_access_levels'][0]['access_level']).to eq(Gitlab::Access::MASTER) end end + + context 'when a policy restricts rule deletion' do + before do + policy = instance_double(ProtectedBranchPolicy, can?: false) + expect(ProtectedBranchPolicy).to receive(:new).and_return(policy) + end + + it "prevents deletion of the protected branch rule" do + post post_endpoint, name: branch_name + + expect(response).to have_gitlab_http_status(403) + end + end end context 'when authenticated as a guest' do @@ -209,18 +222,20 @@ describe API::ProtectedBranches do end describe "DELETE /projects/:id/protected_branches/unprotect/:branch" do + let(:delete_endpoint) { api("/projects/#{project.id}/protected_branches/#{branch_name}", user) } + before do project.add_master(user) end it "unprotects a single branch" do - delete api("/projects/#{project.id}/protected_branches/#{branch_name}", user) + delete delete_endpoint expect(response).to have_gitlab_http_status(204) end it_behaves_like '412 response' do - let(:request) { api("/projects/#{project.id}/protected_branches/#{branch_name}", user) } + let(:request) { delete_endpoint } end it "returns 404 if branch does not exist" do @@ -229,11 +244,24 @@ describe API::ProtectedBranches do expect(response).to have_gitlab_http_status(404) end + context 'when a policy restricts rule deletion' do + before do + policy = instance_double(ProtectedBranchPolicy, can?: false) + expect(ProtectedBranchPolicy).to receive(:new).and_return(policy) + end + + it "prevents deletion of the protected branch rule" do + delete delete_endpoint + + expect(response).to have_gitlab_http_status(403) + end + end + context 'when branch has a wildcard in its name' do let(:protected_name) { 'feature*' } it "unprotects a wildcard branch" do - delete api("/projects/#{project.id}/protected_branches/#{branch_name}", user) + delete delete_endpoint expect(response).to have_gitlab_http_status(204) end diff --git a/spec/requests/lfs_http_spec.rb b/spec/requests/lfs_http_spec.rb index f7c04c19903..1e6bd993c08 100644 --- a/spec/requests/lfs_http_spec.rb +++ b/spec/requests/lfs_http_spec.rb @@ -243,17 +243,34 @@ describe 'Git LFS API and storage' do it_behaves_like 'responds with a file' context 'when LFS uses object storage' do - let(:before_get) do - stub_lfs_object_storage - lfs_object.file.migrate!(LfsObjectUploader::Store::REMOTE) + context 'when proxy download is enabled' do + let(:before_get) do + stub_lfs_object_storage(proxy_download: true) + lfs_object.file.migrate!(LfsObjectUploader::Store::REMOTE) + end + + it 'responds with redirect' do + expect(response).to have_gitlab_http_status(200) + end + + it 'responds with the workhorse send-url' do + expect(response.headers[Gitlab::Workhorse::SEND_DATA_HEADER]).to start_with("send-url:") + end end - it 'responds with redirect' do - expect(response).to have_gitlab_http_status(302) - end + context 'when proxy download is disabled' do + let(:before_get) do + stub_lfs_object_storage(proxy_download: false) + lfs_object.file.migrate!(LfsObjectUploader::Store::REMOTE) + end + + it 'responds with redirect' do + expect(response).to have_gitlab_http_status(302) + end - it 'responds with the file location' do - expect(response.location).to include(lfs_object.reload.file.path) + it 'responds with the file location' do + expect(response.location).to include(lfs_object.reload.file.path) + end end end end @@ -962,22 +979,61 @@ describe 'Git LFS API and storage' do end context 'and request is sent by gitlab-workhorse to authorize the request' do - before do - put_authorize + shared_examples 'a valid response' do + before do + put_authorize + end + + it 'responds with status 200' do + expect(response).to have_gitlab_http_status(200) + end + + it 'uses the gitlab-workhorse content type' do + expect(response.content_type.to_s).to eq(Gitlab::Workhorse::INTERNAL_API_CONTENT_TYPE) + end end - it 'responds with status 200' do - expect(response).to have_gitlab_http_status(200) + shared_examples 'a local file' do + it_behaves_like 'a valid response' do + it 'responds with status 200, location of lfs store and object details' do + expect(json_response['TempPath']).to eq(LfsObjectUploader.workhorse_local_upload_path) + expect(json_response['RemoteObject']).to be_nil + expect(json_response['LfsOid']).to eq(sample_oid) + expect(json_response['LfsSize']).to eq(sample_size) + end + end end - it 'uses the gitlab-workhorse content type' do - expect(response.content_type.to_s).to eq(Gitlab::Workhorse::INTERNAL_API_CONTENT_TYPE) + context 'when using local storage' do + it_behaves_like 'a local file' end - it 'responds with status 200, location of lfs store and object details' do - expect(json_response['StoreLFSPath']).to eq(LfsObjectUploader.workhorse_upload_path) - expect(json_response['LfsOid']).to eq(sample_oid) - expect(json_response['LfsSize']).to eq(sample_size) + context 'when using remote storage' do + context 'when direct upload is enabled' do + before do + stub_lfs_object_storage(enabled: true, direct_upload: true) + end + + it_behaves_like 'a valid response' do + it 'responds with status 200, location of lfs remote store and object details' do + expect(json_response['TempPath']).to be_nil + expect(json_response['RemoteObject']).to have_key('ID') + expect(json_response['RemoteObject']).to have_key('GetURL') + expect(json_response['RemoteObject']).to have_key('StoreURL') + expect(json_response['RemoteObject']).to have_key('DeleteURL') + expect(json_response['LfsOid']).to eq(sample_oid) + expect(json_response['LfsSize']).to eq(sample_size) + end + end + end + + context 'when direct upload is disabled' do + before do + stub_lfs_object_storage(enabled: true, direct_upload: false) + end + + it_behaves_like 'a local file' + end end end @@ -1009,26 +1065,81 @@ describe 'Git LFS API and storage' do end context 'with object storage enabled' do - before do - stub_lfs_object_storage(background_upload: true) + context 'and direct upload enabled' do + let!(:fog_connection) do + stub_lfs_object_storage(direct_upload: true) + end + + ['123123', '../../123123'].each do |remote_id| + context "with invalid remote_id: #{remote_id}" do + subject do + put_finalize_with_args('file.remote_id' => remote_id) + end + + it 'responds with status 403' do + subject + + expect(response).to have_gitlab_http_status(403) + end + end + end + + context 'with valid remote_id' do + before do + fog_connection.directories.get('lfs-objects').files.create( + key: 'tmp/upload/12312300', + body: 'content' + ) + end + + subject do + put_finalize_with_args( + 'file.remote_id' => '12312300', + 'file.name' => 'name') + end + + it 'responds with status 200' do + subject + + expect(response).to have_gitlab_http_status(200) + end + + it 'schedules migration of file to object storage' do + subject + + expect(LfsObject.last.projects).to include(project) + end + + it 'have valid file' do + subject + + expect(LfsObject.last.file_store).to eq(ObjectStorage::Store::REMOTE) + expect(LfsObject.last.file).to be_exists + end + end end - it 'schedules migration of file to object storage' do - expect(ObjectStorage::BackgroundMoveWorker).to receive(:perform_async).with('LfsObjectUploader', 'LfsObject', :file, kind_of(Numeric)) + context 'and background upload enabled' do + before do + stub_lfs_object_storage(background_upload: true) + end - put_finalize(with_tempfile: true) + it 'schedules migration of file to object storage' do + expect(ObjectStorage::BackgroundMoveWorker).to receive(:perform_async).with('LfsObjectUploader', 'LfsObject', :file, kind_of(Numeric)) + + put_finalize(with_tempfile: true) + end end end end context 'invalid tempfiles' do - it 'rejects slashes in the tempfile name (path traversal' do - put_finalize('foo/bar') - expect(response).to have_gitlab_http_status(403) + before do + lfs_object.destroy end - it 'rejects tempfile names that do not start with the oid' do - put_finalize("foo#{sample_oid}") + it 'rejects slashes in the tempfile name (path traversal)' do + put_finalize('../bar', with_tempfile: true) expect(response).to have_gitlab_http_status(403) end end @@ -1118,7 +1229,7 @@ describe 'Git LFS API and storage' do end it 'with location of lfs store and object details' do - expect(json_response['StoreLFSPath']).to eq(LfsObjectUploader.workhorse_upload_path) + expect(json_response['TempPath']).to eq(LfsObjectUploader.workhorse_local_upload_path) expect(json_response['LfsOid']).to eq(sample_oid) expect(json_response['LfsSize']).to eq(sample_size) end @@ -1221,21 +1332,28 @@ describe 'Git LFS API and storage' do end def put_finalize(lfs_tmp = lfs_tmp_file, with_tempfile: false) - setup_tempfile(lfs_tmp) if with_tempfile + upload_path = LfsObjectUploader.workhorse_local_upload_path + file_path = upload_path + '/' + lfs_tmp if lfs_tmp - put "#{project.http_url_to_repo}/gitlab-lfs/objects/#{sample_oid}/#{sample_size}", nil, - headers.merge('X-Gitlab-Lfs-Tmp' => lfs_tmp).compact - end + if with_tempfile + FileUtils.mkdir_p(upload_path) + FileUtils.touch(file_path) + end - def lfs_tmp_file - "#{sample_oid}012345678" + args = { + 'file.path' => file_path, + 'file.name' => File.basename(file_path) + }.compact + + put_finalize_with_args(args) end - def setup_tempfile(lfs_tmp) - upload_path = LfsObjectUploader.workhorse_upload_path + def put_finalize_with_args(args) + put "#{project.http_url_to_repo}/gitlab-lfs/objects/#{sample_oid}/#{sample_size}", args, headers + end - FileUtils.mkdir_p(upload_path) - FileUtils.touch(File.join(upload_path, lfs_tmp)) + def lfs_tmp_file + "#{sample_oid}012345678" end end diff --git a/spec/services/protected_branches/create_service_spec.rb b/spec/services/protected_branches/create_service_spec.rb index 53b3e5e365d..786493c3577 100644 --- a/spec/services/protected_branches/create_service_spec.rb +++ b/spec/services/protected_branches/create_service_spec.rb @@ -35,5 +35,18 @@ describe ProtectedBranches::CreateService do expect { service.execute }.to raise_error(Gitlab::Access::AccessDeniedError) end end + + context 'when a policy restricts rule creation' do + before do + policy = instance_double(ProtectedBranchPolicy, can?: false) + expect(ProtectedBranchPolicy).to receive(:new).and_return(policy) + end + + it "prevents creation of the protected branch rule" do + expect do + service.execute + end.to raise_error(Gitlab::Access::AccessDeniedError) + end + end end end diff --git a/spec/services/protected_branches/destroy_service_spec.rb b/spec/services/protected_branches/destroy_service_spec.rb new file mode 100644 index 00000000000..4a391b6c25c --- /dev/null +++ b/spec/services/protected_branches/destroy_service_spec.rb @@ -0,0 +1,30 @@ +require 'spec_helper' + +describe ProtectedBranches::DestroyService do + let(:protected_branch) { create(:protected_branch) } + let(:project) { protected_branch.project } + let(:user) { project.owner } + + describe '#execute' do + subject(:service) { described_class.new(project, user) } + + it 'destroys a protected branch' do + service.execute(protected_branch) + + expect(protected_branch).to be_destroyed + end + + context 'when a policy restricts rule deletion' do + before do + policy = instance_double(ProtectedBranchPolicy, can?: false) + expect(ProtectedBranchPolicy).to receive(:new).and_return(policy) + end + + it "prevents deletion of the protected branch rule" do + expect do + service.execute(protected_branch) + end.to raise_error(Gitlab::Access::AccessDeniedError) + end + end + end +end diff --git a/spec/services/protected_branches/update_service_spec.rb b/spec/services/protected_branches/update_service_spec.rb index 9fa5983db66..3f6f8e09565 100644 --- a/spec/services/protected_branches/update_service_spec.rb +++ b/spec/services/protected_branches/update_service_spec.rb @@ -22,5 +22,16 @@ describe ProtectedBranches::UpdateService do expect { service.execute(protected_branch) }.to raise_error(Gitlab::Access::AccessDeniedError) end end + + context 'when a policy restricts rule creation' do + before do + policy = instance_double(ProtectedBranchPolicy, can?: false) + expect(ProtectedBranchPolicy).to receive(:new).and_return(policy) + end + + it "prevents creation of the protected branch rule" do + expect { service.execute(protected_branch) }.to raise_error(Gitlab::Access::AccessDeniedError) + end + end end end diff --git a/spec/services/protected_tags/destroy_service_spec.rb b/spec/services/protected_tags/destroy_service_spec.rb new file mode 100644 index 00000000000..e12f53a2221 --- /dev/null +++ b/spec/services/protected_tags/destroy_service_spec.rb @@ -0,0 +1,17 @@ +require 'spec_helper' + +describe ProtectedTags::DestroyService do + let(:protected_tag) { create(:protected_tag) } + let(:project) { protected_tag.project } + let(:user) { project.owner } + + describe '#execute' do + subject(:service) { described_class.new(project, user) } + + it 'destroy a protected tag' do + service.execute(protected_tag) + + expect(protected_tag).to be_destroyed + end + end +end diff --git a/spec/support/stub_object_storage.rb b/spec/support/stub_object_storage.rb index 1a0a2feb27d..6e88641da42 100644 --- a/spec/support/stub_object_storage.rb +++ b/spec/support/stub_object_storage.rb @@ -1,17 +1,22 @@ module StubConfiguration def stub_object_storage_uploader( - config:, uploader:, remote_directory:, + config:, + uploader:, + remote_directory:, enabled: true, proxy_download: false, - background_upload: false) - Fog.mock! - + background_upload: false, + direct_upload: false + ) allow(config).to receive(:enabled) { enabled } allow(config).to receive(:proxy_download) { proxy_download } allow(config).to receive(:background_upload) { background_upload } + allow(config).to receive(:direct_upload) { direct_upload } return unless enabled + Fog.mock! + ::Fog::Storage.new(uploader.object_store_credentials).tap do |connection| begin connection.directories.create(key: remote_directory) diff --git a/spec/uploaders/gitlab_uploader_spec.rb b/spec/uploaders/gitlab_uploader_spec.rb index 60e35dcf235..4fba122cce1 100644 --- a/spec/uploaders/gitlab_uploader_spec.rb +++ b/spec/uploaders/gitlab_uploader_spec.rb @@ -27,7 +27,7 @@ describe GitlabUploader do describe '#file_cache_storage?' do context 'when file storage is used' do before do - uploader_class.cache_storage(:file) + expect(uploader_class).to receive(:cache_storage) { CarrierWave::Storage::File } end it { is_expected.to be_file_cache_storage } @@ -35,7 +35,7 @@ describe GitlabUploader do context 'when is remote storage' do before do - uploader_class.cache_storage(:fog) + expect(uploader_class).to receive(:cache_storage) { CarrierWave::Storage::Fog } end it { is_expected.not_to be_file_cache_storage } diff --git a/spec/uploaders/object_storage_spec.rb b/spec/uploaders/object_storage_spec.rb index 489b6707c6e..1d406c71955 100644 --- a/spec/uploaders/object_storage_spec.rb +++ b/spec/uploaders/object_storage_spec.rb @@ -21,11 +21,11 @@ describe ObjectStorage do let(:object) { build_stubbed(:user) } let(:uploader) { uploader_class.new(object, :file) } - before do - allow(uploader_class).to receive(:object_store_enabled?).and_return(true) - end - describe '#object_store=' do + before do + allow(uploader_class).to receive(:object_store_enabled?).and_return(true) + end + it "reload the local storage" do uploader.object_store = described_class::Store::LOCAL expect(uploader.file_storage?).to be_truthy @@ -35,28 +35,28 @@ describe ObjectStorage do uploader.object_store = described_class::Store::REMOTE expect(uploader.file_storage?).to be_falsey end - end - context 'object_store is Store::LOCAL' do - before do - uploader.object_store = described_class::Store::LOCAL - end + context 'object_store is Store::LOCAL' do + before do + uploader.object_store = described_class::Store::LOCAL + end - describe '#store_dir' do - it 'is the composition of (base_dir, dynamic_segment)' do - expect(uploader.store_dir).to start_with("uploads/-/system/user/") + describe '#store_dir' do + it 'is the composition of (base_dir, dynamic_segment)' do + expect(uploader.store_dir).to start_with("uploads/-/system/user/") + end end end - end - context 'object_store is Store::REMOTE' do - before do - uploader.object_store = described_class::Store::REMOTE - end + context 'object_store is Store::REMOTE' do + before do + uploader.object_store = described_class::Store::REMOTE + end - describe '#store_dir' do - it 'is the composition of (dynamic_segment)' do - expect(uploader.store_dir).to start_with("user/") + describe '#store_dir' do + it 'is the composition of (dynamic_segment)' do + expect(uploader.store_dir).to start_with("user/") + end end end end @@ -92,7 +92,7 @@ describe ObjectStorage do describe '#file_cache_storage?' do context 'when file storage is used' do before do - uploader_class.cache_storage(:file) + expect(uploader_class).to receive(:cache_storage) { CarrierWave::Storage::File } end it { expect(uploader).to be_file_cache_storage } @@ -100,7 +100,7 @@ describe ObjectStorage do context 'when is remote storage' do before do - uploader_class.cache_storage(:fog) + expect(uploader_class).to receive(:cache_storage) { CarrierWave::Storage::Fog } end it { expect(uploader).not_to be_file_cache_storage } @@ -298,7 +298,9 @@ describe ObjectStorage do let(:remote_directory) { 'directory' } before do - uploader_class.storage_options double(object_store: double(remote_directory: remote_directory)) + allow(uploader_class).to receive(:options) do + double(object_store: double(remote_directory: remote_directory)) + end end subject { uploader.fog_directory } @@ -310,7 +312,9 @@ describe ObjectStorage do let(:connection) { Settingslogic.new("provider" => "AWS") } before do - uploader_class.storage_options double(object_store: double(connection: connection)) + allow(uploader_class).to receive(:options) do + double(object_store: double(connection: connection)) + end end subject { uploader.fog_credentials } @@ -323,4 +327,304 @@ describe ObjectStorage do it { is_expected.to eq(false) } end + + describe '.workhorse_authorize' do + subject { uploader_class.workhorse_authorize } + + before do + # ensure that we use regular Fog libraries + # other tests might call `Fog.mock!` and + # it will make tests to fail + Fog.unmock! + end + + shared_examples 'uses local storage' do + it "returns temporary path" do + is_expected.to have_key(:TempPath) + + expect(subject[:TempPath]).to start_with(uploader_class.root) + expect(subject[:TempPath]).to include(described_class::TMP_UPLOAD_PATH) + end + + it "does not return remote store" do + is_expected.not_to have_key('RemoteObject') + end + end + + shared_examples 'uses remote storage' do + it "returns remote store" do + is_expected.to have_key(:RemoteObject) + + expect(subject[:RemoteObject]).to have_key(:ID) + expect(subject[:RemoteObject]).to have_key(:GetURL) + expect(subject[:RemoteObject]).to have_key(:DeleteURL) + expect(subject[:RemoteObject]).to have_key(:StoreURL) + expect(subject[:RemoteObject][:GetURL]).to include(described_class::TMP_UPLOAD_PATH) + expect(subject[:RemoteObject][:DeleteURL]).to include(described_class::TMP_UPLOAD_PATH) + expect(subject[:RemoteObject][:StoreURL]).to include(described_class::TMP_UPLOAD_PATH) + end + + it "does not return local store" do + is_expected.not_to have_key('TempPath') + end + end + + context 'when object storage is disabled' do + before do + allow(Gitlab.config.uploads.object_store).to receive(:enabled) { false } + end + + it_behaves_like 'uses local storage' + end + + context 'when object storage is enabled' do + before do + allow(Gitlab.config.uploads.object_store).to receive(:enabled) { true } + end + + context 'when direct upload is enabled' do + before do + allow(Gitlab.config.uploads.object_store).to receive(:direct_upload) { true } + end + + context 'uses AWS' do + before do + expect(uploader_class).to receive(:object_store_credentials) do + { provider: "AWS", + aws_access_key_id: "AWS_ACCESS_KEY_ID", + aws_secret_access_key: "AWS_SECRET_ACCESS_KEY", + region: "eu-central-1" } + end + end + + it_behaves_like 'uses remote storage' do + let(:storage_url) { "https://uploads.s3-eu-central-1.amazonaws.com/" } + + it 'returns links for S3' do + expect(subject[:RemoteObject][:GetURL]).to start_with(storage_url) + expect(subject[:RemoteObject][:DeleteURL]).to start_with(storage_url) + expect(subject[:RemoteObject][:StoreURL]).to start_with(storage_url) + end + end + end + + context 'uses Google' do + before do + expect(uploader_class).to receive(:object_store_credentials) do + { provider: "Google", + google_storage_access_key_id: 'ACCESS_KEY_ID', + google_storage_secret_access_key: 'SECRET_ACCESS_KEY' } + end + end + + it_behaves_like 'uses remote storage' do + let(:storage_url) { "https://storage.googleapis.com/uploads/" } + + it 'returns links for Google Cloud' do + expect(subject[:RemoteObject][:GetURL]).to start_with(storage_url) + expect(subject[:RemoteObject][:DeleteURL]).to start_with(storage_url) + expect(subject[:RemoteObject][:StoreURL]).to start_with(storage_url) + end + end + end + + context 'uses GDK/minio' do + before do + expect(uploader_class).to receive(:object_store_credentials) do + { provider: "AWS", + aws_access_key_id: "AWS_ACCESS_KEY_ID", + aws_secret_access_key: "AWS_SECRET_ACCESS_KEY", + endpoint: 'http://127.0.0.1:9000', + path_style: true, + region: "gdk" } + end + end + + it_behaves_like 'uses remote storage' do + let(:storage_url) { "http://127.0.0.1:9000/uploads/" } + + it 'returns links for S3' do + expect(subject[:RemoteObject][:GetURL]).to start_with(storage_url) + expect(subject[:RemoteObject][:DeleteURL]).to start_with(storage_url) + expect(subject[:RemoteObject][:StoreURL]).to start_with(storage_url) + end + end + end + end + + context 'when direct upload is disabled' do + before do + allow(Gitlab.config.uploads.object_store).to receive(:direct_upload) { false } + end + + it_behaves_like 'uses local storage' + end + end + end + + describe '#store_workhorse_file!' do + subject do + uploader.store_workhorse_file!(params, :file) + end + + context 'when local file is used' do + context 'when valid file is used' do + let(:target_path) do + File.join(uploader_class.root, uploader_class::TMP_UPLOAD_PATH) + end + + before do + FileUtils.mkdir_p(target_path) + end + + context 'when no filename is specified' do + let(:params) do + { "file.path" => "test/file" } + end + + it 'raises an error' do + expect { subject }.to raise_error(uploader_class::RemoteStoreError, /Missing filename/) + end + end + + context 'when invalid file is specified' do + let(:file_path) do + File.join(target_path, "..", "test.file") + end + + before do + FileUtils.touch(file_path) + end + + let(:params) do + { "file.path" => file_path, + "file.name" => "my_file.txt" } + end + + it 'raises an error' do + expect { subject }.to raise_error(uploader_class::RemoteStoreError, /Bad file path/) + end + end + + context 'when filename is specified' do + let(:params) do + { "file.path" => tmp_file, + "file.name" => "my_file.txt" } + end + + let(:tmp_file) { Tempfile.new('filename', target_path) } + + before do + FileUtils.touch(tmp_file) + end + + after do + FileUtils.rm_f(tmp_file) + end + + it 'succeeds' do + expect { subject }.not_to raise_error + + expect(uploader).to be_exists + end + + it 'proper path is being used' do + subject + + expect(uploader.path).to start_with(uploader_class.root) + expect(uploader.path).to end_with("my_file.txt") + end + + it 'source file to not exist' do + subject + + expect(File.exist?(tmp_file.path)).to be_falsey + end + end + end + end + + context 'when remote file is used' do + let!(:fog_connection) do + stub_uploads_object_storage(uploader_class) + end + + context 'when valid file is used' do + context 'when no filename is specified' do + let(:params) do + { "file.remote_id" => "test/123123" } + end + + it 'raises an error' do + expect { subject }.to raise_error(uploader_class::RemoteStoreError, /Missing filename/) + end + end + + context 'when invalid file is specified' do + let(:params) do + { "file.remote_id" => "../test/123123", + "file.name" => "my_file.txt" } + end + + it 'raises an error' do + expect { subject }.to raise_error(uploader_class::RemoteStoreError, /Bad file path/) + end + end + + context 'when non existing file is specified' do + let(:params) do + { "file.remote_id" => "test/12312300", + "file.name" => "my_file.txt" } + end + + it 'raises an error' do + expect { subject }.to raise_error(uploader_class::RemoteStoreError, /Missing file/) + end + end + + context 'when filename is specified' do + let(:params) do + { "file.remote_id" => "test/123123", + "file.name" => "my_file.txt" } + end + + let!(:fog_file) do + fog_connection.directories.get('uploads').files.create( + key: 'tmp/upload/test/123123', + body: 'content' + ) + end + + it 'succeeds' do + expect { subject }.not_to raise_error + + expect(uploader).to be_exists + end + + it 'path to not be temporary' do + subject + + expect(uploader.path).not_to be_nil + expect(uploader.path).not_to include('tmp/upload') + expect(uploader.url).to include('/my_file.txt') + end + + it 'url is used' do + subject + + expect(uploader.url).not_to be_nil + expect(uploader.url).to include('/my_file.txt') + end + end + end + end + + context 'when no file is used' do + let(:params) { {} } + + it 'raises an error' do + expect { subject }.to raise_error(uploader_class::RemoteStoreError, /Bad file/) + end + end + end end diff --git a/spec/views/ci/lints/show.html.haml_spec.rb b/spec/views/projects/ci/lints/show.html.haml_spec.rb index ded320793ea..2f0cd38c14a 100644 --- a/spec/views/ci/lints/show.html.haml_spec.rb +++ b/spec/views/projects/ci/lints/show.html.haml_spec.rb @@ -1,12 +1,13 @@ require 'spec_helper' -describe 'ci/lints/show' do +describe 'projects/ci/lints/show' do include Devise::Test::ControllerHelpers + let(:project) { create(:project, :repository) } + let(:config_processor) { Gitlab::Ci::YamlProcessor.new(YAML.dump(content)) } describe 'XSS protection' do - let(:config_processor) { Gitlab::Ci::YamlProcessor.new(YAML.dump(content)) } - before do + assign(:project, project) assign(:status, true) assign(:builds, config_processor.builds) assign(:stages, config_processor.stages) @@ -48,22 +49,21 @@ describe 'ci/lints/show' do end end - let(:content) do - { - build_template: { - script: './build.sh', - tags: ['dotnet'], - only: ['test@dude/repo'], - except: ['deploy'], - environment: 'testing' + context 'when the content is valid' do + let(:content) do + { + build_template: { + script: './build.sh', + tags: ['dotnet'], + only: ['test@dude/repo'], + except: ['deploy'], + environment: 'testing' + } } - } - end - - let(:config_processor) { Gitlab::Ci::YamlProcessor.new(YAML.dump(content)) } + end - context 'when the content is valid' do before do + assign(:project, project) assign(:status, true) assign(:builds, config_processor.builds) assign(:stages, config_processor.stages) @@ -83,6 +83,7 @@ describe 'ci/lints/show' do context 'when the content is invalid' do before do + assign(:project, project) assign(:status, false) assign(:error, 'Undefined error') end diff --git a/yarn.lock b/yarn.lock index af7bda5d562..584951b5da0 100644 --- a/yarn.lock +++ b/yarn.lock @@ -3605,7 +3605,7 @@ fs.realpath@^1.0.0: version "1.0.0" resolved "https://registry.yarnpkg.com/fs.realpath/-/fs.realpath-1.0.0.tgz#1504ad2523158caa40db4a2787cb01411994ea4f" -fsevents@^1.0.0: +fsevents@^1.0.0, fsevents@^1.1.3: version "1.1.3" resolved "https://registry.yarnpkg.com/fsevents/-/fsevents-1.1.3.tgz#11f82318f5fe7bb2cd22965a108e9306208216d8" dependencies: |