diff options
77 files changed, 1607 insertions, 900 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) @@ -52,6 +52,7 @@ gem 'omniauth-shibboleth', '~> 1.2.0' gem 'omniauth-twitter', '~> 1.4' gem 'omniauth_crowd', '~> 2.2.0' gem 'omniauth-authentiq', '~> 0.3.1' +gem 'omniauth-jwt', '~> 0.0.2' gem 'rack-oauth2', '~> 1.2.1' gem 'jwt', '~> 1.5.6' diff --git a/Gemfile.lock b/Gemfile.lock index 47466779341..d50b3b1857a 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -555,6 +555,9 @@ GEM multi_json (~> 1.3) omniauth (>= 1.1.1) omniauth-oauth2 (>= 1.3.1) + omniauth-jwt (0.0.2) + jwt + omniauth (~> 1.1) omniauth-kerberos (0.3.0) omniauth-multipassword timfel-krb5-auth (~> 0.8) @@ -1116,6 +1119,7 @@ DEPENDENCIES omniauth-github (~> 1.1.1) omniauth-gitlab (~> 1.0.2) omniauth-google-oauth2 (~> 0.5.2) + omniauth-jwt (~> 0.0.2) omniauth-kerberos (~> 0.3.0) omniauth-oauth2-generic (~> 0.2.2) omniauth-saml (~> 1.10) 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/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/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/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/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/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/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/43512-add-support-for-omniauth-jwt-provider.yml b/changelogs/unreleased/43512-add-support-for-omniauth-jwt-provider.yml new file mode 100644 index 00000000000..039d3de7168 --- /dev/null +++ b/changelogs/unreleased/43512-add-support-for-omniauth-jwt-provider.yml @@ -0,0 +1,5 @@ +--- +title: Adds support for OmniAuth JWT provider +merge_request: 17774 +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/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/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/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 23e106440f5..8db66037d61 100644 --- a/config/gitlab.yml.example +++ b/config/gitlab.yml.example @@ -518,7 +518,17 @@ production: &base # - { name: 'twitter', # app_id: 'YOUR_APP_ID', # app_secret: 'YOUR_APP_SECRET' } - # + # - { name: 'jwt', + # app_secret: 'YOUR_APP_SECRET', + # args: { + # algorithm: 'HS256', + # uid_claim: 'email', + # required_claims: ["name", "email"], + # info_map: { name: "name", email: "email" }, + # auth_url: 'https://example.com/', + # valid_within: nil, + # } + # } # - { name: 'saml', # label: 'Our SAML Provider', # groups_attribute: 'Groups', @@ -799,6 +809,17 @@ test: - { name: 'twitter', app_id: 'YOUR_APP_ID', app_secret: 'YOUR_APP_SECRET' } + - { name: 'jwt', + app_secret: 'YOUR_APP_SECRET', + args: { + algorithm: 'HS256', + uid_claim: 'email', + required_claims: ["name", "email"], + info_map: { name: "name", email: "email" }, + auth_url: 'https://example.com/', + valid_within: nil, + } + } - { name: 'auth0', args: { client_id: 'YOUR_AUTH0_CLIENT_ID', 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/auth/jwt.md b/doc/administration/auth/jwt.md new file mode 100644 index 00000000000..b51e705ab52 --- /dev/null +++ b/doc/administration/auth/jwt.md @@ -0,0 +1,72 @@ +# JWT OmniAuth provider + +To enable the JWT OmniAuth provider, you must register your application with JWT. +JWT will provide you with a secret key for you to use. + +1. On your GitLab server, open the configuration file. + + For Omnibus GitLab: + + ```sh + sudo editor /etc/gitlab/gitlab.rb + ``` + + For installations from source: + + ```sh + cd /home/git/gitlab + sudo -u git -H editor config/gitlab.yml + ``` + +1. See [Initial OmniAuth Configuration](../../integration/omniauth.md#initial-omniauth-configuration) for initial settings. +1. Add the provider configuration. + + For Omnibus GitLab: + + ```ruby + gitlab_rails['omniauth_providers'] = [ + { name: 'jwt', + app_secret: 'YOUR_APP_SECRET', + args: { + algorithm: 'HS256', + uid_claim: 'email', + required_claims: ["name", "email"], + info_maps: { name: "name", email: "email" }, + auth_url: 'https://example.com/', + valid_within: nil, + } + } + ] + ``` + + For installation from source: + + ``` + - { name: 'jwt', + app_secret: 'YOUR_APP_SECRET', + args: { + algorithm: 'HS256', + uid_claim: 'email', + required_claims: ["name", "email"], + info_map: { name: "name", email: "email" }, + auth_url: 'https://example.com/', + valid_within: nil, + } + } + ``` + + NOTE: **Note:** For more information on each configuration option refer to + the [OmniAuth JWT usage documentation](https://github.com/mbleigh/omniauth-jwt#usage). + +1. Change `YOUR_APP_SECRET` to the client secret and set `auth_url` to your redirect URL. +1. Save the configuration file. +1. [Reconfigure GitLab][] or [restart GitLab][] for the changes to take effect if you + installed GitLab via Omnibus or from source respectively. + +On the sign in page there should now be a JWT icon below the regular sign in form. +Click the icon to begin the authentication process. JWT will ask the user to +sign in and authorize the GitLab application. If everything goes well, the user +will be redirected to GitLab and will be signed in. + +[reconfigure GitLab]: ../restart_gitlab.md#omnibus-gitlab-reconfigure +[restart GitLab]: ../restart_gitlab.md#installations-from-source 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/integration/omniauth.md b/doc/integration/omniauth.md index 20087a981f9..3edde3de83d 100644 --- a/doc/integration/omniauth.md +++ b/doc/integration/omniauth.md @@ -32,6 +32,7 @@ contains some settings that are common for all providers. - [Auth0](auth0.md) - [Authentiq](../administration/auth/authentiq.md) - [OAuth2Generic](oauth2_generic.md) +- [JWT](../administration/auth/jwt.md) ## Initial OmniAuth Configuration 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/clusters/index.md b/doc/user/project/clusters/index.md index bd9bcfadb99..716787532fc 100644 --- a/doc/user/project/clusters/index.md +++ b/doc/user/project/clusters/index.md @@ -71,7 +71,7 @@ You need Master [permissions] and above to access the Kubernetes page. To add an existing Kubernetes cluster to your project: 1. Navigate to your project's **CI/CD > Kubernetes** page. -1. Click on **Add Kuberntes cluster**. +1. Click on **Add Kubernetes cluster**. 1. Click on **Add an existing Kubernetes cluster** and fill in the details: - **Kubernetes cluster name** (required) - The name you wish to give the cluster. - **Environment scope** (required)- The @@ -101,7 +101,7 @@ To add an existing Kubernetes cluster to your project: - If you or someone created a secret specifically for the project, usually with limited permissions, the secret's namespace and project namespace may be the same. -1. Finally, click the **Create Kuberntes cluster** button. +1. Finally, click the **Create Kubernetes cluster** button. After a few moments, your cluster should be created. If something goes wrong, you will be notified. 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/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/workhorse.rb b/lib/gitlab/workhorse.rb index 9dc7ae4253c..1887f0dc2b3 100644 --- a/lib/gitlab/workhorse.rb +++ b/lib/gitlab/workhorse.rb @@ -44,7 +44,7 @@ module Gitlab 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' => { @@ -72,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 @@ -89,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( @@ -107,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/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/projects/hook_logs/user_reads_log_spec.rb b/spec/features/projects/hook_logs/user_reads_log_spec.rb new file mode 100644 index 00000000000..18e975fa653 --- /dev/null +++ b/spec/features/projects/hook_logs/user_reads_log_spec.rb @@ -0,0 +1,21 @@ +require 'spec_helper' + +feature 'Hook logs' do + given(:web_hook_log) { create(:web_hook_log, response_body: '<script>') } + given(:project) { web_hook_log.web_hook.project } + given(:user) { create(:user) } + + before do + project.add_master(user) + + sign_in(user) + end + + scenario 'user reads log without getting XSS' do + visit( + project_hook_hook_log_path( + project, web_hook_log.web_hook, web_hook_log)) + + expect(page).to have_content('<script>') + end +end 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/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/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/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: |