diff options
272 files changed, 3254 insertions, 1782 deletions
diff --git a/.gitlab/issue_templates/Security developer workflow.md b/.gitlab/issue_templates/Security developer workflow.md index 69cf7fe1548..08651195d98 100644 --- a/.gitlab/issue_templates/Security developer workflow.md +++ b/.gitlab/issue_templates/Security developer workflow.md @@ -16,6 +16,7 @@ Set the title to: `[Security] Description of the original issue` - [ ] Add a link to the MR to the [links section](#links) - [ ] Add a link to an EE MR if required - [ ] Make sure the MR remains in-progress and gets approved after the review cycle, **but never merged**. +- [ ] Add a link to this issue on the original security issue. #### Backports @@ -37,6 +38,7 @@ Set the title to: `[Security] Description of the original issue` - [ ] Fill in any upgrade notes that users may need to take into account in the [details section](#details) - [ ] Add Yes/No and further details if needed to the migration and settings columns in the [details section](#details) - [ ] Add the nickname of the external user who found the issue (and/or HackerOne profile) to the Thanks row in the [details section](#details) +- [ ] Once your `master` MR is merged, comment on the original security issue with a link to that MR indicating the issue is fixed. ### Summary diff --git a/CHANGELOG.md b/CHANGELOG.md index 57e946befb1..c84cc519396 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -287,6 +287,14 @@ entry. - Disables stop environment button while the deploy is in progress. +## 11.4.9 (2018-12-03) + +### Fixed (2 changes) + +- Display impersonation token value only after creation. !22916 +- Correctly handle data-loss scenarios when encrypting columns. !23306 + + ## 11.4.8 (2018-11-27) ### Security (24 changes) diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 2dc8ac40dd4..4304f6c8744 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -181,4 +181,4 @@ This [documentation](doc/development/contributing/merge_request_workflow.md) has ## Style guides -This [documentation](doc/development/contributing/design.md) has been moved. +This [documentation](doc/development/contributing/style_guides.md) has been moved. diff --git a/GITLAB_WORKHORSE_VERSION b/GITLAB_WORKHORSE_VERSION index b26a34e4705..1502020768a 100644 --- a/GITLAB_WORKHORSE_VERSION +++ b/GITLAB_WORKHORSE_VERSION @@ -1 +1 @@ -7.2.1 +7.3.0 @@ -82,7 +82,7 @@ gem 'validates_hostname', '~> 1.0.6' gem 'browser', '~> 2.5' # GPG -gem 'gpgme' +gem 'gpgme', '~> 2.0.18' # LDAP Auth # GitLab fork with several improvements to original library. For full list of changes @@ -91,7 +91,7 @@ gem 'gitlab_omniauth-ldap', '~> 2.0.4', require: 'omniauth-ldap' gem 'net-ldap' # API -gem 'grape', '~> 1.1' +gem 'grape', '~> 1.1.0' gem 'grape-entity', '~> 0.7.1' gem 'rack-cors', '~> 1.0.0', require: 'rack/cors' diff --git a/Gemfile.lock b/Gemfile.lock index d7ef3b114d3..e7873932dad 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -313,8 +313,8 @@ GEM multi_json (~> 1.11) os (>= 0.9, < 2.0) signet (~> 0.7) - gpgme (2.0.13) - mini_portile2 (~> 2.1) + gpgme (2.0.18) + mini_portile2 (~> 2.3) grape (1.1.0) activesupport builder @@ -1016,8 +1016,8 @@ DEPENDENCIES gon (~> 6.2) google-api-client (~> 0.23) google-protobuf (~> 3.6) - gpgme - grape (~> 1.1) + gpgme (~> 2.0.18) + grape (~> 1.1.0) grape-entity (~> 0.7.1) grape-path-helpers (~> 1.0) grape_logging (~> 1.7) diff --git a/Gemfile.rails4.lock b/Gemfile.rails4.lock index ea248dfdabe..7478e2173bd 100644 --- a/Gemfile.rails4.lock +++ b/Gemfile.rails4.lock @@ -310,8 +310,8 @@ GEM multi_json (~> 1.11) os (>= 0.9, < 2.0) signet (~> 0.7) - gpgme (2.0.13) - mini_portile2 (~> 2.1) + gpgme (2.0.18) + mini_portile2 (~> 2.3) grape (1.1.0) activesupport builder @@ -1007,8 +1007,8 @@ DEPENDENCIES gon (~> 6.2) google-api-client (~> 0.23) google-protobuf (~> 3.6) - gpgme - grape (~> 1.1) + gpgme (~> 2.0.18) + grape (~> 1.1.0) grape-entity (~> 0.7.1) grape-path-helpers (~> 1.0) grape_logging (~> 1.7) diff --git a/app/assets/javascripts/api.js b/app/assets/javascripts/api.js index 0da7ae1b229..f8dbe412f80 100644 --- a/app/assets/javascripts/api.js +++ b/app/assets/javascripts/api.js @@ -5,6 +5,7 @@ import axios from './lib/utils/axios_utils'; const Api = { groupsPath: '/api/:version/groups.json', groupPath: '/api/:version/groups/:id', + subgroupsPath: '/api/:version/groups/:id/subgroups', namespacesPath: '/api/:version/namespaces.json', groupProjectsPath: '/api/:version/groups/:id/projects.json', projectsPath: '/api/:version/projects.json', diff --git a/app/assets/javascripts/diffs/components/app.vue b/app/assets/javascripts/diffs/components/app.vue index 22da38ce7a5..bf9244df7f7 100644 --- a/app/assets/javascripts/diffs/components/app.vue +++ b/app/assets/javascripts/diffs/components/app.vue @@ -102,6 +102,12 @@ export default { if (this.shouldShow) { this.fetchData(); } + + const id = window && window.location && window.location.hash; + + if (id) { + this.setHighlightedRow(id.slice(1)); + } }, created() { this.adjustView(); @@ -114,6 +120,7 @@ export default { 'fetchDiffFiles', 'startRenderDiffsQueue', 'assignDiscussionsToDiff', + 'setHighlightedRow', ]), fetchData() { this.fetchDiffFiles() diff --git a/app/assets/javascripts/diffs/components/diff_line_gutter_content.vue b/app/assets/javascripts/diffs/components/diff_line_gutter_content.vue index aecdd133bf8..c0613d80d37 100644 --- a/app/assets/javascripts/diffs/components/diff_line_gutter_content.vue +++ b/app/assets/javascripts/diffs/components/diff_line_gutter_content.vue @@ -72,6 +72,13 @@ export default { diffFiles: state => state.diffs.diffFiles, }), ...mapGetters(['isLoggedIn']), + lineCode() { + return ( + this.line.line_code || + (this.line.left && this.line.line.left.line_code) || + (this.line.right && this.line.right.line_code) + ); + }, lineHref() { return `#${this.line.line_code || ''}`; }, @@ -97,7 +104,7 @@ export default { }, }, methods: { - ...mapActions('diffs', ['loadMoreLines', 'showCommentForm']), + ...mapActions('diffs', ['loadMoreLines', 'showCommentForm', 'setHighlightedRow']), handleCommentButton() { this.showCommentForm({ lineCode: this.line.line_code, fileHash: this.fileHash }); }, @@ -168,7 +175,13 @@ export default { > <icon :size="12" name="comment" /> </button> - <a v-if="lineNumber" :data-linenumber="lineNumber" :href="lineHref"> </a> + <a + v-if="lineNumber" + :data-linenumber="lineNumber" + :href="lineHref" + @click="setHighlightedRow(lineCode);" + > + </a> <diff-gutter-avatars v-if="shouldShowAvatarsOnGutter" :discussions="line.discussions" /> </template> </div> diff --git a/app/assets/javascripts/diffs/components/diff_table_cell.vue b/app/assets/javascripts/diffs/components/diff_table_cell.vue index f4eb956adcb..d174b13e133 100644 --- a/app/assets/javascripts/diffs/components/diff_table_cell.vue +++ b/app/assets/javascripts/diffs/components/diff_table_cell.vue @@ -1,5 +1,5 @@ <script> -import { mapGetters } from 'vuex'; +import { mapGetters, mapActions } from 'vuex'; import DiffLineGutterContent from './diff_line_gutter_content.vue'; import { MATCH_LINE_TYPE, @@ -30,6 +30,11 @@ export default { type: String, required: true, }, + isHighlighted: { + type: Boolean, + required: true, + default: false, + }, diffViewType: { type: String, required: false, @@ -85,6 +90,7 @@ export default { const { type } = this.line; return { + hll: this.isHighlighted, [type]: type, [LINE_UNFOLD_CLASS_NAME]: this.isMatchLine, [LINE_HOVER_CLASS_NAME]: @@ -99,6 +105,7 @@ export default { return this.lineType === OLD_LINE_TYPE ? this.line.old_line : this.line.new_line; }, }, + methods: mapActions('diffs', ['setHighlightedRow']), }; </script> diff --git a/app/assets/javascripts/diffs/components/inline_diff_table_row.vue b/app/assets/javascripts/diffs/components/inline_diff_table_row.vue index 8d53fbded73..c764cbeb8e0 100644 --- a/app/assets/javascripts/diffs/components/inline_diff_table_row.vue +++ b/app/assets/javascripts/diffs/components/inline_diff_table_row.vue @@ -1,5 +1,5 @@ <script> -import { mapGetters, mapActions } from 'vuex'; +import { mapGetters, mapActions, mapState } from 'vuex'; import DiffTableCell from './diff_table_cell.vue'; import { NEW_LINE_TYPE, @@ -40,6 +40,11 @@ export default { }; }, computed: { + ...mapState({ + isHighlighted(state) { + return this.line.line_code !== null && this.line.line_code === state.diffs.highlightedRow; + }, + }), ...mapGetters('diffs', ['isInlineView']), isContextLine() { return this.line.type === CONTEXT_LINE_TYPE; @@ -91,6 +96,7 @@ export default { :is-bottom="isBottom" :is-hover="isHover" :show-comment-button="true" + :is-highlighted="isHighlighted" class="diff-line-num old_line" /> <diff-table-cell @@ -100,8 +106,18 @@ export default { :line-type="newLineType" :is-bottom="isBottom" :is-hover="isHover" + :is-highlighted="isHighlighted" class="diff-line-num new_line qa-new-diff-line" /> - <td :class="line.type" class="line_content" v-html="line.rich_text"></td> + <td + :class="[ + line.type, + { + hll: isHighlighted, + }, + ]" + class="line_content" + v-html="line.rich_text" + ></td> </tr> </template> diff --git a/app/assets/javascripts/diffs/components/parallel_diff_table_row.vue b/app/assets/javascripts/diffs/components/parallel_diff_table_row.vue index 248dfd9815e..caf0df8a4e3 100644 --- a/app/assets/javascripts/diffs/components/parallel_diff_table_row.vue +++ b/app/assets/javascripts/diffs/components/parallel_diff_table_row.vue @@ -1,5 +1,5 @@ <script> -import { mapActions } from 'vuex'; +import { mapActions, mapState } from 'vuex'; import $ from 'jquery'; import DiffTableCell from './diff_table_cell.vue'; import { @@ -43,6 +43,15 @@ export default { }; }, computed: { + ...mapState({ + isHighlighted(state) { + const lineCode = + (this.line.left && this.line.left.line_code) || + (this.line.right && this.line.right.line_code); + + return lineCode ? lineCode === state.diffs.highlightedRow : false; + }, + }), isContextLine() { return this.line.left && this.line.left.type === CONTEXT_LINE_TYPE; }, @@ -57,7 +66,14 @@ export default { return OLD_NO_NEW_LINE_TYPE; } - return this.line.left ? this.line.left.type : EMPTY_CELL_TYPE; + const lineTypeClass = this.line.left ? this.line.left.type : EMPTY_CELL_TYPE; + + return [ + lineTypeClass, + { + hll: this.isHighlighted, + }, + ]; }, }, created() { @@ -114,6 +130,7 @@ export default { :line-type="oldLineType" :is-bottom="isBottom" :is-hover="isLeftHover" + :is-highlighted="isHighlighted" :show-comment-button="true" :diff-view-type="parallelDiffViewType" line-position="left" @@ -139,6 +156,7 @@ export default { :line-type="newLineType" :is-bottom="isBottom" :is-hover="isRightHover" + :is-highlighted="isHighlighted" :show-comment-button="true" :diff-view-type="parallelDiffViewType" line-position="right" @@ -146,7 +164,12 @@ export default { /> <td :id="line.right.line_code" - :class="line.right.type" + :class="[ + line.right.type, + { + hll: isHighlighted, + }, + ]" class="line_content parallel right-side" @mousedown.native="handleParallelLineMouseDown" v-html="line.right.rich_text" diff --git a/app/assets/javascripts/diffs/store/actions.js b/app/assets/javascripts/diffs/store/actions.js index 0899d793c91..8b477c678fd 100644 --- a/app/assets/javascripts/diffs/store/actions.js +++ b/app/assets/javascripts/diffs/store/actions.js @@ -33,6 +33,10 @@ export const fetchDiffFiles = ({ state, commit }) => { .then(handleLocationHash); }; +export const setHighlightedRow = ({ commit }, lineCode) => { + commit(types.SET_HIGHLIGHTED_ROW, lineCode); +}; + // This is adding line discussions to the actual lines in the diff tree // once for parallel and once for inline mode export const assignDiscussionsToDiff = ( @@ -127,7 +131,7 @@ export const loadMoreLines = ({ commit }, options) => { export const scrollToLineIfNeededInline = (_, line) => { const hash = getLocationHash(); - if (hash && line.lineCode === hash) { + if (hash && line.line_code === hash) { handleLocationHash(); } }; @@ -137,7 +141,7 @@ export const scrollToLineIfNeededParallel = (_, line) => { if ( hash && - ((line.left && line.left.lineCode === hash) || (line.right && line.right.lineCode === hash)) + ((line.left && line.left.line_code === hash) || (line.right && line.right.line_code === hash)) ) { handleLocationHash(); } diff --git a/app/assets/javascripts/diffs/store/modules/diff_state.js b/app/assets/javascripts/diffs/store/modules/diff_state.js index 8fb2f0e17ac..98e57d52d77 100644 --- a/app/assets/javascripts/diffs/store/modules/diff_state.js +++ b/app/assets/javascripts/diffs/store/modules/diff_state.js @@ -26,4 +26,5 @@ export default () => ({ currentDiffFileId: '', projectPath: '', commentForms: [], + highlightedRow: null, }); diff --git a/app/assets/javascripts/diffs/store/mutation_types.js b/app/assets/javascripts/diffs/store/mutation_types.js index 74961a74899..0338cde3658 100644 --- a/app/assets/javascripts/diffs/store/mutation_types.js +++ b/app/assets/javascripts/diffs/store/mutation_types.js @@ -17,3 +17,4 @@ export const UPDATE_CURRENT_DIFF_FILE_ID = 'UPDATE_CURRENT_DIFF_FILE_ID'; export const OPEN_DIFF_FILE_COMMENT_FORM = 'OPEN_DIFF_FILE_COMMENT_FORM'; export const UPDATE_DIFF_FILE_COMMENT_FORM = 'UPDATE_DIFF_FILE_COMMENT_FORM'; export const CLOSE_DIFF_FILE_COMMENT_FORM = 'CLOSE_DIFF_FILE_COMMENT_FORM'; +export const SET_HIGHLIGHTED_ROW = 'SET_HIGHLIGHTED_ROW'; diff --git a/app/assets/javascripts/diffs/store/mutations.js b/app/assets/javascripts/diffs/store/mutations.js index 7baf2ed17e6..f0895661bf2 100644 --- a/app/assets/javascripts/diffs/store/mutations.js +++ b/app/assets/javascripts/diffs/store/mutations.js @@ -241,4 +241,7 @@ export default { [types.CLOSE_DIFF_FILE_COMMENT_FORM](state, fileHash) { state.commentForms = state.commentForms.filter(form => form.fileHash !== fileHash); }, + [types.SET_HIGHLIGHTED_ROW](state, lineCode) { + state.highlightedRow = lineCode; + }, }; diff --git a/app/assets/javascripts/groups_select.js b/app/assets/javascripts/groups_select.js index b4a3037c1b7..2049760fe29 100644 --- a/app/assets/javascripts/groups_select.js +++ b/app/assets/javascripts/groups_select.js @@ -10,13 +10,18 @@ export default function groupsSelect() { const $select = $(this); const allAvailable = $select.data('allAvailable'); const skipGroups = $select.data('skipGroups') || []; + const parentGroupID = $select.data('parentId'); + const groupsPath = parentGroupID + ? Api.subgroupsPath.replace(':id', parentGroupID) + : Api.groupsPath; + $select.select2({ placeholder: 'Search for a group', allowClear: $select.hasClass('allowClear'), multiple: $select.hasClass('multiselect'), minimumInputLength: 0, ajax: { - url: Api.buildUrl(Api.groupsPath), + url: Api.buildUrl(groupsPath), dataType: 'json', quietMillis: 250, transport(params) { diff --git a/app/assets/javascripts/ide/components/panes/right.vue b/app/assets/javascripts/ide/components/panes/right.vue index e318367a5ec..7a57ccf2dd3 100644 --- a/app/assets/javascripts/ide/components/panes/right.vue +++ b/app/assets/javascripts/ide/components/panes/right.vue @@ -105,7 +105,7 @@ export default { :key="tabView.name" class="h-100" > - <component :is="tabView.name" /> + <component :is="tabView.component || tabView.name" /> </div> </resizable-panel> <nav class="ide-activity-bar"> diff --git a/app/assets/javascripts/lib/utils/url_utility.js b/app/assets/javascripts/lib/utils/url_utility.js index a282c2df441..9850f7ce782 100644 --- a/app/assets/javascripts/lib/utils/url_utility.js +++ b/app/assets/javascripts/lib/utils/url_utility.js @@ -17,27 +17,29 @@ export function getParameterValues(sParam) { // @param {Object} params - url keys and value to merge // @param {String} url export function mergeUrlParams(params, url) { - let newUrl = Object.keys(params).reduce((acc, paramName) => { - const paramValue = encodeURIComponent(params[paramName]); - const pattern = new RegExp(`\\b(${paramName}=).*?(&|$)`); - - if (paramValue === null) { - return acc.replace(pattern, ''); - } else if (url.search(pattern) !== -1) { - return acc.replace(pattern, `$1${paramValue}$2`); - } - - return `${acc}${acc.indexOf('?') > 0 ? '&' : '?'}${paramName}=${paramValue}`; - }, decodeURIComponent(url)); + const re = /^([^?#]*)(\?[^#]*)?(.*)/; + const merged = {}; + const urlparts = url.match(re); + + if (urlparts[2]) { + urlparts[2] + .substr(1) + .split('&') + .forEach(part => { + if (part.length) { + const kv = part.split('='); + merged[decodeURIComponent(kv[0])] = decodeURIComponent(kv.slice(1).join('=')); + } + }); + } - // Remove a trailing ampersand - const lastChar = newUrl[newUrl.length - 1]; + Object.assign(merged, params); - if (lastChar === '&') { - newUrl = newUrl.slice(0, -1); - } + const query = Object.keys(merged) + .map(key => `${encodeURIComponent(key)}=${encodeURIComponent(merged[key])}`) + .join('&'); - return newUrl; + return `${urlparts[1]}?${query}${urlparts[3]}`; } export function removeParamQueryString(url, param) { diff --git a/app/assets/javascripts/monitoring/components/graph.vue b/app/assets/javascripts/monitoring/components/graph.vue index 815063237fc..64a1df80a8e 100644 --- a/app/assets/javascripts/monitoring/components/graph.vue +++ b/app/assets/javascripts/monitoring/components/graph.vue @@ -105,6 +105,9 @@ export default { deploymentFlagData() { return this.reducedDeploymentData.find(deployment => deployment.showDeploymentFlag); }, + shouldRenderData() { + return this.graphData.queries.filter(s => s.result.length > 0).length > 0; + }, }, watch: { hoverData() { @@ -120,17 +123,17 @@ export default { }, draw() { const breakpointSize = bp.getBreakpointSize(); - const query = this.graphData.queries[0]; const svgWidth = this.$refs.baseSvg.getBoundingClientRect().width; + 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 = svgWidth - this.margin.left - this.margin.right; this.graphHeight = this.graphHeight - this.margin.top - this.margin.bottom; this.baseGraphHeight = this.graphHeight - 50; @@ -139,8 +142,15 @@ export default { // pixel offsets inside the svg and outside are not 1:1 this.realPixelRatio = svgWidth / this.baseGraphWidth; - this.renderAxesPaths(); - this.formatDeployments(); + // set the legends on the axes + const [query] = this.graphData.queries; + this.legendTitle = query ? query.label : 'Average'; + this.unitOfDisplay = query ? query.unit : ''; + + if (this.shouldRenderData) { + this.renderAxesPaths(); + this.formatDeployments(); + } }, handleMouseOverGraph(e) { let point = this.$refs.graphData.createSVGPoint(); @@ -266,7 +276,7 @@ export default { :y-axis-label="yAxisLabel" :unit-of-display="unitOfDisplay" /> - <svg ref="graphData" :viewBox="innerViewBox" class="graph-data"> + <svg v-if="shouldRenderData" ref="graphData" :viewBox="innerViewBox" class="graph-data"> <slot name="additionalSvgContent" :graphDrawData="graphDrawData" /> <graph-path v-for="(path, index) in timeSeries" @@ -293,8 +303,14 @@ export default { @mousemove="handleMouseOverGraph($event);" /> </svg> + <svg v-else :viewBox="innerViewBox" class="js-no-data-to-display"> + <text x="50%" y="50%" alignment-baseline="middle" text-anchor="middle"> + {{ s__('Metrics|No data to display') }} + </text> + </svg> </svg> <graph-flag + v-if="shouldRenderData" :real-pixel-ratio="realPixelRatio" :current-x-coordinate="currentXCoordinate" :current-data="currentData" diff --git a/app/assets/javascripts/monitoring/stores/monitoring_store.js b/app/assets/javascripts/monitoring/stores/monitoring_store.js index 176f7d9eef2..8692c873a41 100644 --- a/app/assets/javascripts/monitoring/stores/monitoring_store.js +++ b/app/assets/javascripts/monitoring/stores/monitoring_store.js @@ -7,10 +7,29 @@ function sortMetrics(metrics) { .value(); } +function checkQueryEmptyData(query) { + return { + ...query, + result: query.result.filter(timeSeries => { + const newTimeSeries = timeSeries; + const hasValue = series => + !Number.isNaN(series.value) && (series.value !== null || series.value !== undefined); + const hasNonNullValue = timeSeries.values.find(hasValue); + + newTimeSeries.values = hasNonNullValue ? newTimeSeries.values : []; + + return newTimeSeries.values.length > 0; + }), + }; +} + +function removeTimeSeriesNoData(queries) { + return queries.reduce((series, query) => series.concat(checkQueryEmptyData(query)), []); +} + function normalizeMetrics(metrics) { - return metrics.map(metric => ({ - ...metric, - queries: metric.queries.map(query => ({ + return metrics.map(metric => { + const queries = metric.queries.map(query => ({ ...query, result: query.result.map(result => ({ ...result, @@ -19,8 +38,13 @@ function normalizeMetrics(metrics) { value: Number(value), })), })), - })), - })); + })); + + return { + ...metric, + queries: removeTimeSeriesNoData(queries), + }; + }); } export default class MonitoringStore { diff --git a/app/assets/javascripts/notes/components/comment_form.vue b/app/assets/javascripts/notes/components/comment_form.vue index 884ccca7bde..841fcec96e8 100644 --- a/app/assets/javascripts/notes/components/comment_form.vue +++ b/app/assets/javascripts/notes/components/comment_form.vue @@ -4,6 +4,7 @@ import { mapActions, mapGetters, mapState } from 'vuex'; import _ from 'underscore'; import Autosize from 'autosize'; import { __, sprintf } from '~/locale'; +import TimelineEntryItem from '~/vue_shared/components/notes/timeline_entry_item.vue'; import Flash from '../../flash'; import Autosave from '../../autosave'; import { @@ -30,6 +31,7 @@ export default { markdownField, userAvatarLink, loadingButton, + TimelineEntryItem, }, mixins: [issuableStateMixin], props: { @@ -309,137 +311,135 @@ Please check your network connection and try again.`; <div> <note-signed-out-widget v-if="!isLoggedIn" /> <discussion-locked-widget v-else-if="!canCreateNote" :issuable-type="issuableTypeTitle" /> - <div v-else-if="canCreateNote" class="notes notes-form timeline"> - <div class="timeline-entry note-form"> - <div class="timeline-entry-inner"> - <div class="flash-container error-alert timeline-content"></div> - <div class="timeline-icon d-none d-sm-none d-md-block"> - <user-avatar-link - v-if="author" - :link-href="author.path" - :img-src="author.avatar_url" - :img-alt="author.name" - :img-size="40" - /> - </div> - <div class="timeline-content timeline-content-form"> - <form ref="commentForm" class="new-note common-note-form gfm-form js-main-target-form"> - <div class="error-alert"></div> + <ul v-else-if="canCreateNote" class="notes notes-form timeline"> + <timeline-entry-item class="note-form"> + <div class="flash-container error-alert timeline-content"></div> + <div class="timeline-icon d-none d-sm-none d-md-block"> + <user-avatar-link + v-if="author" + :link-href="author.path" + :img-src="author.avatar_url" + :img-alt="author.name" + :img-size="40" + /> + </div> + <div class="timeline-content timeline-content-form"> + <form ref="commentForm" class="new-note common-note-form gfm-form js-main-target-form"> + <div class="error-alert"></div> - <issue-warning - v-if="hasWarning(getNoteableData)" - :is-locked="isLocked(getNoteableData)" - :is-confidential="isConfidential(getNoteableData)" - /> + <issue-warning + v-if="hasWarning(getNoteableData)" + :is-locked="isLocked(getNoteableData)" + :is-confidential="isConfidential(getNoteableData)" + /> - <markdown-field - ref="markdownField" - :markdown-preview-path="markdownPreviewPath" - :markdown-docs-path="markdownDocsPath" - :quick-actions-docs-path="quickActionsDocsPath" - :markdown-version="markdownVersion" - :add-spacing-classes="false" - > - <textarea - id="note-body" - ref="textarea" - slot="textarea" - v-model="note" - :disabled="isSubmitting" - name="note[note]" - class="note-textarea js-vue-comment-form js-note-text + <markdown-field + ref="markdownField" + :markdown-preview-path="markdownPreviewPath" + :markdown-docs-path="markdownDocsPath" + :quick-actions-docs-path="quickActionsDocsPath" + :markdown-version="markdownVersion" + :add-spacing-classes="false" + > + <textarea + id="note-body" + ref="textarea" + slot="textarea" + v-model="note" + :disabled="isSubmitting" + name="note[note]" + class="note-textarea js-vue-comment-form js-note-text js-gfm-input js-autosize markdown-area js-vue-textarea qa-comment-input" - data-supports-quick-actions="true" - aria-label="Description" - placeholder="Write a comment or drag your files here…" - @keydown.up="editCurrentUserLastNote();" - @keydown.meta.enter="handleSave();" - @keydown.ctrl.enter="handleSave();" - > - </textarea> - </markdown-field> - <div class="note-form-actions"> - <div - class="float-left btn-group + data-supports-quick-actions="true" + aria-label="Description" + placeholder="Write a comment or drag your files here…" + @keydown.up="editCurrentUserLastNote();" + @keydown.meta.enter="handleSave();" + @keydown.ctrl.enter="handleSave();" + > + </textarea> + </markdown-field> + <div class="note-form-actions"> + <div + class="float-left btn-group append-right-10 comment-type-dropdown js-comment-type-dropdown droplab-dropdown" - > - <button - :disabled="isSubmitButtonDisabled" - class="btn btn-create comment-btn js-comment-button js-comment-submit-button + > + <button + :disabled="isSubmitButtonDisabled" + class="btn btn-create comment-btn js-comment-button js-comment-submit-button qa-comment-button" - type="submit" - @click.prevent="handleSave();" - > - {{ __(commentButtonTitle) }} - </button> - <button - :disabled="isSubmitButtonDisabled" - name="button" - type="button" - class="btn comment-btn note-type-toggle js-note-new-discussion dropdown-toggle qa-note-dropdown" - data-display="static" - data-toggle="dropdown" - aria-label="Open comment type dropdown" - > - <i aria-hidden="true" class="fa fa-caret-down toggle-icon"> </i> - </button> - - <ul class="note-type-dropdown dropdown-open-top dropdown-menu"> - <li :class="{ 'droplab-item-selected': noteType === 'comment' }"> - <button - type="button" - class="btn btn-transparent" - @click.prevent="setNoteType('comment');" - > - <i aria-hidden="true" class="fa fa-check icon"> </i> - <div class="description"> - <strong>Comment</strong> - <p>Add a general comment to this {{ noteableDisplayName }}.</p> - </div> - </button> - </li> - <li class="divider droplab-item-ignore"></li> - <li :class="{ 'droplab-item-selected': noteType === 'discussion' }"> - <button - type="button" - class="btn btn-transparent qa-discussion-option" - @click.prevent="setNoteType('discussion');" - > - <i aria-hidden="true" class="fa fa-check icon"> </i> - <div class="description"> - <strong>Start discussion</strong> - <p>{{ startDiscussionDescription }}</p> - </div> - </button> - </li> - </ul> - </div> - - <loading-button - v-if="canUpdateIssue" - :loading="isToggleStateButtonLoading" - :container-class="[ - actionButtonClassNames, - 'btn btn-comment btn-comment-and-close js-action-button', - ]" - :disabled="isToggleStateButtonLoading || isSubmitting" - :label="issueActionButtonTitle" - @click="handleSave(true);" - /> - + type="submit" + @click.prevent="handleSave();" + > + {{ __(commentButtonTitle) }} + </button> <button - v-if="note.length" + :disabled="isSubmitButtonDisabled" + name="button" type="button" - class="btn btn-cancel js-note-discard" - @click="discard" + class="btn comment-btn note-type-toggle js-note-new-discussion dropdown-toggle qa-note-dropdown" + data-display="static" + data-toggle="dropdown" + aria-label="Open comment type dropdown" > - Discard draft + <i aria-hidden="true" class="fa fa-caret-down toggle-icon"> </i> </button> + + <ul class="note-type-dropdown dropdown-open-top dropdown-menu"> + <li :class="{ 'droplab-item-selected': noteType === 'comment' }"> + <button + type="button" + class="btn btn-transparent" + @click.prevent="setNoteType('comment');" + > + <i aria-hidden="true" class="fa fa-check icon"> </i> + <div class="description"> + <strong>Comment</strong> + <p>Add a general comment to this {{ noteableDisplayName }}.</p> + </div> + </button> + </li> + <li class="divider droplab-item-ignore"></li> + <li :class="{ 'droplab-item-selected': noteType === 'discussion' }"> + <button + type="button" + class="btn btn-transparent qa-discussion-option" + @click.prevent="setNoteType('discussion');" + > + <i aria-hidden="true" class="fa fa-check icon"> </i> + <div class="description"> + <strong>Start discussion</strong> + <p>{{ startDiscussionDescription }}</p> + </div> + </button> + </li> + </ul> </div> - </form> - </div> + + <loading-button + v-if="canUpdateIssue" + :loading="isToggleStateButtonLoading" + :container-class="[ + actionButtonClassNames, + 'btn btn-comment btn-comment-and-close js-action-button', + ]" + :disabled="isToggleStateButtonLoading || isSubmitting" + :label="issueActionButtonTitle" + @click="handleSave(true);" + /> + + <button + v-if="note.length" + type="button" + class="btn btn-cancel js-note-discard" + @click="discard" + > + Discard draft + </button> + </div> + </form> </div> - </div> - </div> + </timeline-entry-item> + </ul> </div> </template> diff --git a/app/assets/javascripts/notes/components/note_form.vue b/app/assets/javascripts/notes/components/note_form.vue index ad58267b533..95164183ccb 100644 --- a/app/assets/javascripts/notes/components/note_form.vue +++ b/app/assets/javascripts/notes/components/note_form.vue @@ -48,13 +48,19 @@ export default { required: false, default: '', }, + resolveDiscussion: { + type: Boolean, + required: false, + default: false, + }, }, data() { return { updatedNoteBody: this.noteBody, conflictWhileEditing: false, isSubmitting: false, - isResolving: false, + isResolving: this.resolveDiscussion, + isUnresolving: !this.resolveDiscussion, resolveAsThread: true, }; }, @@ -149,7 +155,7 @@ export default { <div ref="editNoteForm" class="note-edit-form current-note-edit-form js-discussion-note-form"> <div v-if="conflictWhileEditing" class="js-conflict-edit-warning alert alert-danger"> This comment has changed since you started editing, please review the - <a :href="noteHash" target="_blank" rel="noopener noreferrer"> updated comment </a> to ensure + <a :href="noteHash" target="_blank" rel="noopener noreferrer">updated comment</a> to ensure information is not lost. </div> <div class="flash-container timeline-content"></div> @@ -174,22 +180,20 @@ export default { v-model="updatedNoteBody" :data-supports-quick-actions="!isEditing" name="note[note]" - class="note-textarea js-gfm-input js-note-text -js-autosize markdown-area js-vue-issue-note-form js-vue-textarea qa-reply-input" + class="note-textarea js-gfm-input js-note-text js-autosize markdown-area js-vue-issue-note-form js-vue-textarea qa-reply-input" aria-label="Description" placeholder="Write a comment or drag your files here…" @keydown.meta.enter="handleUpdate();" @keydown.ctrl.enter="handleUpdate();" @keydown.up="editMyLastNote();" @keydown.esc="cancelHandler(true);" - > - </textarea> + ></textarea> </markdown-field> <div class="note-form-actions clearfix"> <button :disabled="isDisabled" type="button" - class="js-vue-issue-save btn btn-success js-comment-button " + class="js-vue-issue-save btn btn-success js-comment-button" @click="handleUpdate();" > {{ saveButtonTitle }} diff --git a/app/assets/javascripts/notes/components/noteable_discussion.vue b/app/assets/javascripts/notes/components/noteable_discussion.vue index 36388e4b3cf..4eb3b49392c 100644 --- a/app/assets/javascripts/notes/components/noteable_discussion.vue +++ b/app/assets/javascripts/notes/components/noteable_discussion.vue @@ -1,10 +1,12 @@ <script> +import _ from 'underscore'; import { mapActions, mapGetters } from 'vuex'; import { GlTooltipDirective } from '@gitlab/ui'; import { truncateSha } from '~/lib/utils/text_utility'; -import { s__, __ } from '~/locale'; +import { s__, __, sprintf } from '~/locale'; import systemNote from '~/vue_shared/components/notes/system_note.vue'; import icon from '~/vue_shared/components/icon.vue'; +import TimelineEntryItem from '~/vue_shared/components/notes/timeline_entry_item.vue'; import Flash from '../../flash'; import { SYSTEM_NOTE } from '../constants'; import userAvatarLink from '../../vue_shared/components/user_avatar/user_avatar_link.vue'; @@ -36,6 +38,7 @@ export default { placeholderNote, placeholderSystemNote, systemNote, + TimelineEntryItem, }, directives: { GlTooltip: GlTooltipDirective, @@ -156,6 +159,37 @@ export default { (!discussion.diff_discussion && resolved && hasReplies && !isRepliesToggledByUser) || false ); }, + actionText() { + const commitId = this.discussion.commit_id ? truncateSha(this.discussion.commit_id) : ''; + const linkStart = `<a href="${_.escape(this.discussion.discussion_path)}">`; + const linkEnd = '</a>'; + + let text = s__('MergeRequests|started a discussion'); + + if (this.discussion.for_commit) { + text = s__( + 'MergeRequests|started a discussion on commit %{linkStart}%{commitId}%{linkEnd}', + ); + } else if (this.discussion.diff_discussion) { + if (this.discussion.active) { + text = s__('MergeRequests|started a discussion on %{linkStart}the diff%{linkEnd}'); + } else { + text = s__( + 'MergeRequests|started a discussion on %{linkStart}an old version of the diff%{linkEnd}', + ); + } + } + + return sprintf( + text, + { + commitId, + linkStart, + linkEnd, + }, + false, + ); + }, }, watch: { isReplying() { @@ -269,179 +303,156 @@ Please check your network connection and try again.`; </script> <template> - <li class="note note-discussion timeline-entry" :class="componentClassName"> - <div class="timeline-entry-inner"> - <div class="timeline-content"> - <div :data-discussion-id="discussion.id" class="discussion js-discussion-container"> - <div v-if="shouldRenderDiffs" class="discussion-header note-wrapper"> - <div v-once class="timeline-icon"> - <user-avatar-link - v-if="author" - :link-href="author.path" - :img-src="author.avatar_url" - :img-alt="author.name" - :img-size="40" - /> - </div> - <note-header - :author="author" - :created-at="initialDiscussion.created_at" - :note-id="initialDiscussion.id" - :include-toggle="true" - :expanded="discussion.expanded" - @toggleHandler="toggleDiscussionHandler" - > - <template v-if="discussion.diff_discussion"> - started a discussion on - <a :href="discussion.discussion_path"> - <template v-if="discussion.active" - >the diff</template - > - <template v-else - >an old version of the diff</template - > - </a> - </template> - <template v-else-if="discussion.for_commit"> - started a discussion on commit - <a :href="discussion.discussion_path">{{ truncateSha(discussion.commit_id) }}</a> - </template> - <template v-else - >started a discussion</template - > - </note-header> - <note-edited-text - v-if="discussion.resolved" - :edited-at="discussion.resolved_at" - :edited-by="discussion.resolved_by" - :action-text="resolvedText" - class-name="discussion-headline-light js-discussion-headline" - /> - <note-edited-text - v-else-if="lastUpdatedAt" - :edited-at="lastUpdatedAt" - :edited-by="lastUpdatedBy" - action-text="Last updated" - class-name="discussion-headline-light js-discussion-headline" + <timeline-entry-item class="note note-discussion" :class="componentClassName"> + <div class="timeline-content"> + <div :data-discussion-id="discussion.id" class="discussion js-discussion-container"> + <div v-if="shouldRenderDiffs" class="discussion-header note-wrapper"> + <div v-once class="timeline-icon"> + <user-avatar-link + v-if="author" + :link-href="author.path" + :img-src="author.avatar_url" + :img-alt="author.name" + :img-size="40" /> </div> - <div v-if="shouldShowDiscussions" class="discussion-body"> - <component - :is="wrapperComponent" - v-bind="wrapperComponentProps" - class="card discussion-wrapper" - > - <div class="discussion-notes"> - <ul class="notes"> - <template v-if="shouldGroupReplies"> - <component - :is="componentName(initialDiscussion)" - :note="componentData(initialDiscussion)" - @handleDeleteNote="deleteNoteHandler" - > - <slot slot="avatar-badge" name="avatar-badge"></slot> - </component> - <toggle-replies-widget - v-if="hasReplies" - :collapsed="isRepliesCollapsed" - :replies="replies" - @toggle="toggleReplies" - /> - <template v-if="!isRepliesCollapsed"> - <component - :is="componentName(note)" - v-for="note in replies" - :key="note.id" - :note="componentData(note)" - @handleDeleteNote="deleteNoteHandler" - /> - </template> - </template> - <template v-else> + <note-header + :author="author" + :created-at="initialDiscussion.created_at" + :note-id="initialDiscussion.id" + :include-toggle="true" + :expanded="discussion.expanded" + @toggleHandler="toggleDiscussionHandler" + > + <span v-html="actionText"></span> + </note-header> + <note-edited-text + v-if="discussion.resolved" + :edited-at="discussion.resolved_at" + :edited-by="discussion.resolved_by" + :action-text="resolvedText" + class-name="discussion-headline-light js-discussion-headline" + /> + <note-edited-text + v-else-if="lastUpdatedAt" + :edited-at="lastUpdatedAt" + :edited-by="lastUpdatedBy" + action-text="Last updated" + class-name="discussion-headline-light js-discussion-headline" + /> + </div> + <div v-if="shouldShowDiscussions" class="discussion-body"> + <component + :is="wrapperComponent" + v-bind="wrapperComponentProps" + class="card discussion-wrapper" + > + <div class="discussion-notes"> + <ul class="notes"> + <template v-if="shouldGroupReplies"> + <component + :is="componentName(initialDiscussion)" + :note="componentData(initialDiscussion)" + @handleDeleteNote="deleteNoteHandler" + > + <slot slot="avatar-badge" name="avatar-badge"></slot> + </component> + <toggle-replies-widget + v-if="hasReplies" + :collapsed="isRepliesCollapsed" + :replies="replies" + @toggle="toggleReplies" + /> + <template v-if="!isRepliesCollapsed"> <component :is="componentName(note)" - v-for="(note, index) in discussion.notes" + v-for="note in replies" :key="note.id" :note="componentData(note)" @handleDeleteNote="deleteNoteHandler" - > - <slot v-if="index === 0" slot="avatar-badge" name="avatar-badge"></slot> - </component> + /> </template> - </ul> - <div - v-if="!isRepliesCollapsed" - :class="{ 'is-replying': isReplying }" - class="discussion-reply-holder" - > - <template v-if="!isReplying && canReply"> - <div class="discussion-with-resolve-btn"> + </template> + <template v-else> + <component + :is="componentName(note)" + v-for="(note, index) in discussion.notes" + :key="note.id" + :note="componentData(note)" + @handleDeleteNote="deleteNoteHandler" + > + <slot v-if="index === 0" slot="avatar-badge" name="avatar-badge"></slot> + </component> + </template> + </ul> + <div + v-if="!isRepliesCollapsed" + :class="{ 'is-replying': isReplying }" + class="discussion-reply-holder" + > + <template v-if="!isReplying && canReply"> + <div class="discussion-with-resolve-btn"> + <button + type="button" + class="js-vue-discussion-reply btn btn-text-field mr-sm-2 qa-discussion-reply" + title="Add a reply" + @click="showReplyForm" + > + Reply... + </button> + <div v-if="discussion.resolvable"> <button type="button" - class="js-vue-discussion-reply btn btn-text-field mr-sm-2 qa-discussion-reply" - title="Add a reply" - @click="showReplyForm" + class="btn btn-default mr-sm-2" + @click="resolveHandler();" > - Reply... + <i v-if="isResolving" aria-hidden="true" class="fa fa-spinner fa-spin"></i> + {{ resolveButtonTitle }} </button> - <div v-if="discussion.resolvable"> + </div> + <div + v-if="discussion.resolvable" + class="btn-group discussion-actions ml-sm-2" + role="group" + > + <div v-if="!discussionResolved" class="btn-group" role="group"> + <a + v-gl-tooltip + :href="discussion.resolve_with_issue_path" + :title="s__('MergeRequests|Resolve this discussion in a new issue')" + class="new-issue-for-discussion btn btn-default discussion-create-issue-btn" + > + <icon name="issue-new" /> + </a> + </div> + <div v-if="hasUnresolvedDiscussions" class="btn-group" role="group"> <button - type="button" - class="btn btn-default mr-sm-2" - @click="resolveHandler();" + v-gl-tooltip + class="btn btn-default discussion-next-btn" + title="Jump to next unresolved discussion" + @click="jumpToNextDiscussion" > - <i - v-if="isResolving" - aria-hidden="true" - class="fa fa-spinner fa-spin" - ></i> - {{ resolveButtonTitle }} + <icon name="comment-next" /> </button> </div> - <div - v-if="discussion.resolvable" - class="btn-group discussion-actions ml-sm-2" - role="group" - > - <div v-if="!discussionResolved" class="btn-group" role="group"> - <a - v-gl-tooltip - :href="discussion.resolve_with_issue_path" - :title="s__('MergeRequests|Resolve this discussion in a new issue')" - class="new-issue-for-discussion btn btn-default discussion-create-issue-btn" - > - <icon name="issue-new" /> - </a> - </div> - <div v-if="hasUnresolvedDiscussions" class="btn-group" role="group"> - <button - v-gl-tooltip - class="btn btn-default discussion-next-btn" - title="Jump to next unresolved discussion" - @click="jumpToNextDiscussion" - > - <icon name="comment-next" /> - </button> - </div> - </div> </div> - </template> - <note-form - v-if="isReplying" - ref="noteForm" - :discussion="discussion" - :is-editing="false" - save-button-title="Comment" - @handleFormUpdate="saveReply" - @cancelForm="cancelReplyForm" - /> - <note-signed-out-widget v-if="!canReply" /> - </div> + </div> + </template> + <note-form + v-if="isReplying" + ref="noteForm" + :discussion="discussion" + :is-editing="false" + save-button-title="Comment" + @handleFormUpdate="saveReply" + @cancelForm="cancelReplyForm" + /> + <note-signed-out-widget v-if="!canReply" /> </div> - </component> - </div> + </div> + </component> </div> </div> </div> - </li> + </timeline-entry-item> </template> diff --git a/app/assets/javascripts/notes/components/noteable_note.vue b/app/assets/javascripts/notes/components/noteable_note.vue index fc37fe1c7af..a17be51353e 100644 --- a/app/assets/javascripts/notes/components/noteable_note.vue +++ b/app/assets/javascripts/notes/components/noteable_note.vue @@ -2,6 +2,7 @@ import $ from 'jquery'; import { mapGetters, mapActions } from 'vuex'; import { escape } from 'underscore'; +import TimelineEntryItem from '~/vue_shared/components/notes/timeline_entry_item.vue'; import Flash from '../../flash'; import userAvatarLink from '../../vue_shared/components/user_avatar/user_avatar_link.vue'; import noteHeader from './note_header.vue'; @@ -18,6 +19,7 @@ export default { noteHeader, noteActions, noteBody, + TimelineEntryItem, }, mixins: [noteable, resolvable], props: { @@ -169,62 +171,60 @@ export default { </script> <template> - <li + <timeline-entry-item :id="noteAnchorId" :class="classNameBindings" :data-award-url="note.toggle_award_path" :data-note-id="note.id" - class="note timeline-entry note-wrapper" + class="note note-wrapper" > - <div class="timeline-entry-inner"> - <div v-once class="timeline-icon"> - <user-avatar-link - :link-href="author.path" - :img-src="author.avatar_url" - :img-alt="author.name" - :img-size="40" - > - <slot slot="avatar-badge" name="avatar-badge"> </slot> - </user-avatar-link> - </div> - <div class="timeline-content"> - <div class="note-header"> - <note-header - v-once - :author="author" - :created-at="note.created_at" - :note-id="note.id" - action-text="commented" - /> - <note-actions - :author-id="author.id" - :note-id="note.id" - :note-url="note.noteable_note_url" - :access-level="note.human_access" - :can-edit="note.current_user.can_edit" - :can-award-emoji="note.current_user.can_award_emoji" - :can-delete="note.current_user.can_edit" - :can-report-as-abuse="canReportAsAbuse" - :can-resolve="note.current_user.can_resolve" - :report-abuse-path="note.report_abuse_path" - :resolvable="note.resolvable" - :is-resolved="note.resolved" - :is-resolving="isResolving" - :resolved-by="note.resolved_by" - @handleEdit="editHandler" - @handleDelete="deleteHandler" - @handleResolve="resolveHandler" - /> - </div> - <note-body - ref="noteBody" - :note="note" + <div v-once class="timeline-icon"> + <user-avatar-link + :link-href="author.path" + :img-src="author.avatar_url" + :img-alt="author.name" + :img-size="40" + > + <slot slot="avatar-badge" name="avatar-badge"> </slot> + </user-avatar-link> + </div> + <div class="timeline-content"> + <div class="note-header"> + <note-header + v-once + :author="author" + :created-at="note.created_at" + :note-id="note.id" + action-text="commented" + /> + <note-actions + :author-id="author.id" + :note-id="note.id" + :note-url="note.noteable_note_url" + :access-level="note.human_access" :can-edit="note.current_user.can_edit" - :is-editing="isEditing" - @handleFormUpdate="formUpdateHandler" - @cancelForm="formCancelHandler" + :can-award-emoji="note.current_user.can_award_emoji" + :can-delete="note.current_user.can_edit" + :can-report-as-abuse="canReportAsAbuse" + :can-resolve="note.current_user.can_resolve" + :report-abuse-path="note.report_abuse_path" + :resolvable="note.resolvable" + :is-resolved="note.resolved" + :is-resolving="isResolving" + :resolved-by="note.resolved_by" + @handleEdit="editHandler" + @handleDelete="deleteHandler" + @handleResolve="resolveHandler" /> </div> + <note-body + ref="noteBody" + :note="note" + :can-edit="note.current_user.can_edit" + :is-editing="isEditing" + @handleFormUpdate="formUpdateHandler" + @cancelForm="formCancelHandler" + /> </div> - </li> + </timeline-entry-item> </template> diff --git a/app/assets/javascripts/notes/mixins/resolvable.js b/app/assets/javascripts/notes/mixins/resolvable.js index cd8394e0619..8edf3d088bb 100644 --- a/app/assets/javascripts/notes/mixins/resolvable.js +++ b/app/assets/javascripts/notes/mixins/resolvable.js @@ -36,7 +36,7 @@ export default { const discussion = this.resolveAsThread; const endpoint = discussion ? this.discussion.resolve_path : `${this.note.path}/resolve`; - this.toggleResolveNote({ endpoint, isResolved, discussion }) + return this.toggleResolveNote({ endpoint, isResolved, discussion }) .then(() => { this.isResolving = false; }) diff --git a/app/assets/javascripts/pages/groups/edit/index.js b/app/assets/javascripts/pages/groups/edit/index.js index 32b55575f95..01ef445c901 100644 --- a/app/assets/javascripts/pages/groups/edit/index.js +++ b/app/assets/javascripts/pages/groups/edit/index.js @@ -5,6 +5,7 @@ import initSettingsPanels from '~/settings_panels'; import dirtySubmitFactory from '~/dirty_submit/dirty_submit_factory'; import mountBadgeSettings from '~/pages/shared/mount_badge_settings'; import { GROUP_BADGE } from '~/badges/constants'; +import groupsSelect from '~/groups_select'; import projectSelect from '~/project_select'; document.addEventListener('DOMContentLoaded', () => { @@ -17,5 +18,8 @@ document.addEventListener('DOMContentLoaded', () => { ); mountBadgeSettings(GROUP_BADGE); + // Initialize Subgroups selector + groupsSelect(); + projectSelect(); }); diff --git a/app/assets/javascripts/pipelines/components/graph/graph_component.vue b/app/assets/javascripts/pipelines/components/graph/graph_component.vue index 6f008528db4..59cebaba717 100644 --- a/app/assets/javascripts/pipelines/components/graph/graph_component.vue +++ b/app/assets/javascripts/pipelines/components/graph/graph_component.vue @@ -18,23 +18,19 @@ export default { required: true, }, }, - computed: { graph() { return this.pipeline.details && this.pipeline.details.stages; }, }, - methods: { capitalizeStageName(name) { const escapedName = _.escape(name); return escapedName.charAt(0).toUpperCase() + escapedName.slice(1); }, - isFirstColumn(index) { return index === 0; }, - stageConnectorClass(index, stage) { let className; @@ -48,7 +44,6 @@ export default { return className; }, - refreshPipelineGraph() { this.$emit('refreshPipelineGraph'); }, diff --git a/app/assets/javascripts/pipelines/components/graph/job_item.vue b/app/assets/javascripts/pipelines/components/graph/job_item.vue index 782494f72e4..cf9db89e32b 100644 --- a/app/assets/javascripts/pipelines/components/graph/job_item.vue +++ b/app/assets/javascripts/pipelines/components/graph/job_item.vue @@ -84,10 +84,6 @@ export default { return textBuilder.join(' '); }, - - tooltipBoundary() { - return this.dropdownLength < 5 ? 'viewport' : null; - }, /** * Verifies if the provided job has an action path * @@ -108,7 +104,7 @@ export default { <div class="ci-job-component"> <gl-link v-if="status.has_details" - v-gl-tooltip="{ boundary: tooltipBoundary }" + v-gl-tooltip :href="status.details_path" :title="tooltipText" :class="cssClassJobName" diff --git a/app/assets/javascripts/vue_merge_request_widget/components/deployment.vue b/app/assets/javascripts/vue_merge_request_widget/components/deployment.vue index 950347d8863..2f2a37347af 100644 --- a/app/assets/javascripts/vue_merge_request_widget/components/deployment.vue +++ b/app/assets/javascripts/vue_merge_request_widget/components/deployment.vue @@ -112,7 +112,7 @@ export default { </script> <template> - <div class="mr-widget-heading deploy-heading append-bottom-default"> + <div class="deploy-heading"> <div class="ci-widget media"> <div class="media-body"> <div class="deploy-body"> diff --git a/app/assets/javascripts/vue_merge_request_widget/components/mr_widget_container.vue b/app/assets/javascripts/vue_merge_request_widget/components/mr_widget_container.vue new file mode 100644 index 00000000000..5967ca026e5 --- /dev/null +++ b/app/assets/javascripts/vue_merge_request_widget/components/mr_widget_container.vue @@ -0,0 +1,6 @@ +<template> + <div class="mr-widget-heading"> + <div class="mr-widget-content"><slot name="default"></slot></div> + <slot name="footer"></slot> + </div> +</template> diff --git a/app/assets/javascripts/vue_merge_request_widget/components/mr_widget_header.vue b/app/assets/javascripts/vue_merge_request_widget/components/mr_widget_header.vue index 6f422ea3f27..3b9fc2661ef 100644 --- a/app/assets/javascripts/vue_merge_request_widget/components/mr_widget_header.vue +++ b/app/assets/javascripts/vue_merge_request_widget/components/mr_widget_header.vue @@ -6,6 +6,7 @@ import Icon from '~/vue_shared/components/icon.vue'; import clipboardButton from '~/vue_shared/components/clipboard_button.vue'; import tooltip from '~/vue_shared/directives/tooltip'; import TooltipOnTruncate from '~/vue_shared/components/tooltip_on_truncate.vue'; +import MrWidgetIcon from './mr_widget_icon.vue'; export default { name: 'MRWidgetHeader', @@ -13,6 +14,7 @@ export default { Icon, clipboardButton, TooltipOnTruncate, + MrWidgetIcon, }, directives: { tooltip, @@ -76,7 +78,7 @@ export default { </script> <template> <div class="mr-source-target append-bottom-default"> - <div class="git-merge-icon-container append-right-default"><icon name="git-merge" /></div> + <mr-widget-icon name="git-merge" /> <div class="git-merge-container d-flex"> <div class="normal"> <strong> diff --git a/app/assets/javascripts/vue_merge_request_widget/components/mr_widget_icon.vue b/app/assets/javascripts/vue_merge_request_widget/components/mr_widget_icon.vue new file mode 100644 index 00000000000..e3adc7f7af5 --- /dev/null +++ b/app/assets/javascripts/vue_merge_request_widget/components/mr_widget_icon.vue @@ -0,0 +1,17 @@ +<script> +import Icon from '~/vue_shared/components/icon.vue'; + +export default { + components: { Icon }, + props: { + name: { + type: String, + required: true, + }, + }, +}; +</script> + +<template> + <div class="circle-icon-container append-right-default"><icon :name="name" /></div> +</template> diff --git a/app/assets/javascripts/vue_merge_request_widget/components/mr_widget_pipeline.vue b/app/assets/javascripts/vue_merge_request_widget/components/mr_widget_pipeline.vue index 71571ba9cab..f11cf21b0ca 100644 --- a/app/assets/javascripts/vue_merge_request_widget/components/mr_widget_pipeline.vue +++ b/app/assets/javascripts/vue_merge_request_widget/components/mr_widget_pipeline.vue @@ -79,67 +79,65 @@ export default { </script> <template> - <div v-if="hasPipeline || hasCIError" class="mr-widget-heading append-bottom-default"> - <div class="ci-widget media"> - <template v-if="hasCIError"> - <div - class="add-border ci-status-icon ci-status-icon-failed ci-error - js-ci-error append-right-default" - > - <icon :size="32" name="status_failed_borderless" /> - </div> - <div class="media-body" v-html="errorText"></div> - </template> - <template v-else-if="hasPipeline"> - <a :href="status.details_path" class="align-self-start append-right-default"> - <ci-icon :status="status" :size="32" :borderless="true" class="add-border" /> - </a> - <div class="ci-widget-container d-flex"> - <div class="ci-widget-content"> - <div class="media-body"> - <div class="font-weight-bold"> - Pipeline - <a :href="pipeline.path" class="pipeline-id font-weight-normal pipeline-number" - >#{{ pipeline.id }}</a - > + <div v-if="hasPipeline || hasCIError" class="ci-widget media"> + <template v-if="hasCIError"> + <div + class="add-border ci-status-icon ci-status-icon-failed ci-error + js-ci-error append-right-default" + > + <icon :size="32" name="status_failed_borderless" /> + </div> + <div class="media-body" v-html="errorText"></div> + </template> + <template v-else-if="hasPipeline"> + <a :href="status.details_path" class="align-self-start append-right-default"> + <ci-icon :status="status" :size="32" :borderless="true" class="add-border" /> + </a> + <div class="ci-widget-container d-flex"> + <div class="ci-widget-content"> + <div class="media-body"> + <div class="font-weight-bold"> + Pipeline + <a :href="pipeline.path" class="pipeline-id font-weight-normal pipeline-number" + >#{{ pipeline.id }}</a + > - {{ pipeline.details.status.label }} + {{ pipeline.details.status.label }} - <template v-if="hasCommitInfo"> - for - <a - :href="pipeline.commit.commit_path" - class="commit-sha js-commit-link font-weight-normal" - > - {{ pipeline.commit.short_id }}</a - > - on - <tooltip-on-truncate - :title="sourceBranch" - truncate-target="child" - class="label-branch label-truncate" - v-html="sourceBranchLink" - /> - </template> - </div> - <div v-if="pipeline.coverage" class="coverage">Coverage {{ pipeline.coverage }}%</div> + <template v-if="hasCommitInfo"> + for + <a + :href="pipeline.commit.commit_path" + class="commit-sha js-commit-link font-weight-normal" + > + {{ pipeline.commit.short_id }}</a + > + on + <tooltip-on-truncate + :title="sourceBranch" + truncate-target="child" + class="label-branch label-truncate" + v-html="sourceBranchLink" + /> + </template> </div> + <div v-if="pipeline.coverage" class="coverage">Coverage {{ pipeline.coverage }}%</div> </div> - <div> - <span class="mr-widget-pipeline-graph"> - <span v-if="hasStages" class="stage-cell"> - <div - v-for="(stage, i) in pipeline.details.stages" - :key="i" - class="stage-container dropdown js-mini-pipeline-graph mr-widget-pipeline-stages" - > - <pipeline-stage :stage="stage" /> - </div> - </span> + </div> + <div> + <span class="mr-widget-pipeline-graph"> + <span v-if="hasStages" class="stage-cell"> + <div + v-for="(stage, i) in pipeline.details.stages" + :key="i" + class="stage-container dropdown js-mini-pipeline-graph mr-widget-pipeline-stages" + > + <pipeline-stage :stage="stage" /> + </div> </span> - </div> + </span> </div> - </template> - </div> + </div> + </template> </div> </template> diff --git a/app/assets/javascripts/vue_merge_request_widget/components/mr_widget_pipeline_container.vue b/app/assets/javascripts/vue_merge_request_widget/components/mr_widget_pipeline_container.vue new file mode 100644 index 00000000000..5f5fe67b3c1 --- /dev/null +++ b/app/assets/javascripts/vue_merge_request_widget/components/mr_widget_pipeline_container.vue @@ -0,0 +1,74 @@ +<script> +import Deployment from './deployment.vue'; +import MrWidgetContainer from './mr_widget_container.vue'; +import MrWidgetPipeline from './mr_widget_pipeline.vue'; + +/** + * Renders the pipeline and related deployments from the store. + * + * | Props | Description + * |---------------|------------- + * | `mr` | This is the mr_widget store + * | `isPostMerge` | If true, show the "post merge" pipeline and deployments + */ +export default { + name: 'MrWidgetPipelineContainer', + components: { + Deployment, + MrWidgetContainer, + MrWidgetPipeline, + }, + props: { + mr: { + type: Object, + required: true, + }, + isPostMerge: { + type: Boolean, + required: false, + default: false, + }, + }, + computed: { + pipeline() { + return this.isPostMerge ? this.mr.mergePipeline : this.mr.pipeline; + }, + branch() { + return this.isPostMerge ? this.mr.targetBranch : this.mr.sourceBranch; + }, + branchLink() { + return this.isPostMerge ? this.mr.targetBranch : this.mr.sourceBranchLink; + }, + deployments() { + return this.isPostMerge ? this.mr.postMergeDeployments : this.mr.deployments; + }, + deploymentClass() { + return this.isPostMerge ? 'js-post-deployment' : 'js-pre-deployment'; + }, + hasDeploymentMetrics() { + return this.isPostMerge; + }, + }, +}; +</script> +<template> + <mr-widget-container> + <mr-widget-pipeline + :pipeline="pipeline" + :ci-status="mr.ciStatus" + :has-ci="mr.hasCI" + :source-branch="branch" + :source-branch-link="branchLink" + :troubleshooting-docs-path="mr.troubleshootingDocsPath" + /> + <div v-if="deployments.length" slot="footer" class="mr-widget-extension"> + <deployment + v-for="deployment in deployments" + :key="deployment.id" + :class="deploymentClass" + :deployment="deployment" + :show-metrics="hasDeploymentMetrics" + /> + </div> + </mr-widget-container> +</template> diff --git a/app/assets/javascripts/vue_merge_request_widget/mr_widget_options.vue b/app/assets/javascripts/vue_merge_request_widget/mr_widget_options.vue index a269c0a4e87..3c3e3efcc36 100644 --- a/app/assets/javascripts/vue_merge_request_widget/mr_widget_options.vue +++ b/app/assets/javascripts/vue_merge_request_widget/mr_widget_options.vue @@ -6,7 +6,7 @@ import SmartInterval from '~/smart_interval'; import createFlash from '../flash'; import WidgetHeader from './components/mr_widget_header.vue'; import WidgetMergeHelp from './components/mr_widget_merge_help.vue'; -import WidgetPipeline from './components/mr_widget_pipeline.vue'; +import MrWidgetPipelineContainer from './components/mr_widget_pipeline_container.vue'; import Deployment from './components/deployment.vue'; import WidgetRelatedLinks from './components/mr_widget_related_links.vue'; import MergedState from './components/states/mr_widget_merged.vue'; @@ -44,7 +44,7 @@ export default { components: { 'mr-widget-header': WidgetHeader, 'mr-widget-merge-help': WidgetMergeHelp, - 'mr-widget-pipeline': WidgetPipeline, + MrWidgetPipelineContainer, Deployment, 'mr-widget-related-links': WidgetRelatedLinks, 'mr-widget-merged': MergedState, @@ -296,23 +296,12 @@ export default { <template> <div class="mr-state-widget prepend-top-default"> <mr-widget-header :mr="mr" /> - <mr-widget-pipeline + <mr-widget-pipeline-container v-if="shouldRenderPipelines" - :pipeline="mr.pipeline" - :ci-status="mr.ciStatus" - :has-ci="mr.hasCI" - :source-branch="mr.sourceBranch" - :source-branch-link="mr.sourceBranchLink" - :troubleshooting-docs-path="mr.troubleshootingDocsPath" + class="mr-widget-workflow" + :mr="mr" /> - <deployment - v-for="deployment in mr.deployments" - :key="`pre-merge-deploy-${deployment.id}`" - class="js-pre-merge-deploy" - :deployment="deployment" - :show-metrics="false" - /> - <div class="mr-section-container"> + <div class="mr-section-container mr-widget-workflow"> <grouped-test-reports-app v-if="mr.testResultsPath" class="js-reports-container" @@ -336,24 +325,11 @@ export default { </div> <div v-if="shouldRenderMergeHelp" class="mr-widget-footer"><mr-widget-merge-help /></div> </div> - - <template v-if="shouldRenderMergedPipeline"> - <mr-widget-pipeline - class="js-post-merge-pipeline prepend-top-default" - :pipeline="mr.mergePipeline" - :ci-status="mr.ciStatus" - :has-ci="mr.hasCI" - :source-branch="mr.targetBranch" - :source-branch-link="mr.targetBranch" - :troubleshooting-docs-path="mr.troubleshootingDocsPath" - /> - <deployment - v-for="postMergeDeployment in mr.postMergeDeployments" - :key="`post-merge-deploy-${postMergeDeployment.id}`" - :deployment="postMergeDeployment" - :show-metrics="true" - class="js-post-deployment" - /> - </template> + <mr-widget-pipeline-container + v-if="shouldRenderMergedPipeline" + class="js-post-merge-pipeline mr-widget-workflow" + :mr="mr" + :is-post-merge="true" + /> </div> </template> diff --git a/app/assets/javascripts/vue_shared/components/notes/placeholder_note.vue b/app/assets/javascripts/vue_shared/components/notes/placeholder_note.vue index b1faebf409b..8d3a3009c55 100644 --- a/app/assets/javascripts/vue_shared/components/notes/placeholder_note.vue +++ b/app/assets/javascripts/vue_shared/components/notes/placeholder_note.vue @@ -17,12 +17,14 @@ * /> */ import { mapGetters } from 'vuex'; +import TimelineEntryItem from '~/vue_shared/components/notes/timeline_entry_item.vue'; import userAvatarLink from '../user_avatar/user_avatar_link.vue'; export default { name: 'PlaceholderNote', components: { userAvatarLink, + TimelineEntryItem, }, props: { note: { @@ -37,30 +39,28 @@ export default { </script> <template> - <li class="note being-posted fade-in-half timeline-entry"> - <div class="timeline-entry-inner"> - <div class="timeline-icon"> - <user-avatar-link - :link-href="getUserData.path" - :img-src="getUserData.avatar_url" - :img-size="40" - /> - </div> - <div :class="{ discussion: !note.individual_note }" class="timeline-content"> - <div class="note-header"> - <div class="note-header-info"> - <a :href="getUserData.path"> - <span class="d-none d-sm-inline-block">{{ getUserData.name }}</span> - <span class="note-headline-light">@{{ getUserData.username }}</span> - </a> - </div> + <timeline-entry-item class="note being-posted fade-in-half"> + <div class="timeline-icon"> + <user-avatar-link + :link-href="getUserData.path" + :img-src="getUserData.avatar_url" + :img-size="40" + /> + </div> + <div :class="{ discussion: !note.individual_note }" class="timeline-content"> + <div class="note-header"> + <div class="note-header-info"> + <a :href="getUserData.path"> + <span class="d-none d-sm-inline-block">{{ getUserData.name }}</span> + <span class="note-headline-light">@{{ getUserData.username }}</span> + </a> </div> - <div class="note-body"> - <div class="note-text"> - <p>{{ note.body }}</p> - </div> + </div> + <div class="note-body"> + <div class="note-text"> + <p>{{ note.body }}</p> </div> </div> </div> - </li> + </timeline-entry-item> </template> diff --git a/app/assets/javascripts/vue_shared/components/notes/placeholder_system_note.vue b/app/assets/javascripts/vue_shared/components/notes/placeholder_system_note.vue index 674f923478d..7689425eb52 100644 --- a/app/assets/javascripts/vue_shared/components/notes/placeholder_system_note.vue +++ b/app/assets/javascripts/vue_shared/components/notes/placeholder_system_note.vue @@ -1,4 +1,6 @@ <script> +import TimelineEntryItem from '~/vue_shared/components/notes/timeline_entry_item.vue'; + /** * Common component to render a placeholder system note. * @@ -9,6 +11,9 @@ */ export default { name: 'PlaceholderSystemNote', + components: { + TimelineEntryItem, + }, props: { note: { type: Object, @@ -19,11 +24,9 @@ export default { </script> <template> - <li class="note system-note timeline-entry being-posted fade-in-half"> - <div class="timeline-entry-inner"> - <div class="timeline-content"> - <em>{{ note.body }}</em> - </div> + <timeline-entry-item class="note system-note being-posted fade-in-half"> + <div class="timeline-content"> + <em>{{ note.body }}</em> </div> - </li> + </timeline-entry-item> </template> diff --git a/app/assets/javascripts/vue_shared/components/notes/skeleton_note.vue b/app/assets/javascripts/vue_shared/components/notes/skeleton_note.vue index c6cf4661222..e61d1fd2031 100644 --- a/app/assets/javascripts/vue_shared/components/notes/skeleton_note.vue +++ b/app/assets/javascripts/vue_shared/components/notes/skeleton_note.vue @@ -1,22 +1,22 @@ <script> import { GlSkeletonLoading } from '@gitlab/ui'; +import TimelineEntryItem from '~/vue_shared/components/notes/timeline_entry_item.vue'; export default { name: 'SkeletonNote', components: { GlSkeletonLoading, + TimelineEntryItem, }, }; </script> <template> - <li class="timeline-entry note note-wrapper"> - <div class="timeline-entry-inner"> - <div class="timeline-icon"></div> - <div class="timeline-content"> - <div class="note-header"></div> - <div class="note-body"><gl-skeleton-loading /></div> - </div> + <timeline-entry-item class="note note-wrapper"> + <div class="timeline-icon"></div> + <div class="timeline-content"> + <div class="note-header"></div> + <div class="note-body"><gl-skeleton-loading /></div> </div> - </li> + </timeline-entry-item> </template> diff --git a/app/assets/javascripts/vue_shared/components/notes/system_note.vue b/app/assets/javascripts/vue_shared/components/notes/system_note.vue index fb86262d0b4..31df26f7b05 100644 --- a/app/assets/javascripts/vue_shared/components/notes/system_note.vue +++ b/app/assets/javascripts/vue_shared/components/notes/system_note.vue @@ -20,6 +20,7 @@ import $ from 'jquery'; import { mapGetters } from 'vuex'; import noteHeader from '~/notes/components/note_header.vue'; import Icon from '~/vue_shared/components/icon.vue'; +import TimelineEntryItem from './timeline_entry_item.vue'; import { spriteIcon } from '../../../lib/utils/common_utils'; const MAX_VISIBLE_COMMIT_LIST_COUNT = 3; @@ -29,6 +30,7 @@ export default { components: { Icon, noteHeader, + TimelineEntryItem, }, props: { note: { @@ -73,36 +75,34 @@ export default { </script> <template> - <li + <timeline-entry-item :id="noteAnchorId" :class="{ target: isTargetNote }" - class="note system-note timeline-entry note-wrapper" + class="note system-note note-wrapper" > - <div class="timeline-entry-inner"> - <div class="timeline-icon" v-html="iconHtml"></div> - <div class="timeline-content"> - <div class="note-header"> - <note-header :author="note.author" :created-at="note.created_at" :note-id="note.id"> - <span v-html="actionTextHtml"></span> - </note-header> - </div> - <div class="note-body"> - <div - :class="{ - 'system-note-commit-list': hasMoreCommits, - 'hide-shade': expanded, - }" - class="note-text" - v-html="note.note_html" - ></div> - <div v-if="hasMoreCommits" class="flex-list"> - <div class="system-note-commit-list-toggler flex-row" @click="expanded = !expanded;"> - <icon :name="toggleIcon" :size="8" class="append-right-5" /> - <span>Toggle commit list</span> - </div> + <div class="timeline-icon" v-html="iconHtml"></div> + <div class="timeline-content"> + <div class="note-header"> + <note-header :author="note.author" :created-at="note.created_at" :note-id="note.id"> + <span v-html="actionTextHtml"></span> + </note-header> + </div> + <div class="note-body"> + <div + :class="{ + 'system-note-commit-list': hasMoreCommits, + 'hide-shade': expanded, + }" + class="note-text" + v-html="note.note_html" + ></div> + <div v-if="hasMoreCommits" class="flex-list"> + <div class="system-note-commit-list-toggler flex-row" @click="expanded = !expanded;"> + <icon :name="toggleIcon" :size="8" class="append-right-5" /> + <span>Toggle commit list</span> </div> </div> </div> </div> - </li> + </timeline-entry-item> </template> diff --git a/app/assets/javascripts/vue_shared/components/notes/timeline_entry_item.vue b/app/assets/javascripts/vue_shared/components/notes/timeline_entry_item.vue new file mode 100644 index 00000000000..06974a12aed --- /dev/null +++ b/app/assets/javascripts/vue_shared/components/notes/timeline_entry_item.vue @@ -0,0 +1,11 @@ +<script> +export default { + name: 'TimelineEntryItem', +}; +</script> + +<template> + <li class="timeline-entry"> + <div class="timeline-entry-inner"><slot></slot></div> + </li> +</template> diff --git a/app/assets/stylesheets/framework.scss b/app/assets/stylesheets/framework.scss index 4041f2b4479..d320fa5f595 100644 --- a/app/assets/stylesheets/framework.scss +++ b/app/assets/stylesheets/framework.scss @@ -6,6 +6,7 @@ @import 'bootstrap_migration'; @import 'framework/layout'; +@import 'framework/alerts'; @import 'framework/animations'; @import 'framework/vue_transitions'; @import 'framework/avatar'; diff --git a/app/assets/stylesheets/framework/alerts.scss b/app/assets/stylesheets/framework/alerts.scss new file mode 100644 index 00000000000..866792a6a1b --- /dev/null +++ b/app/assets/stylesheets/framework/alerts.scss @@ -0,0 +1,4 @@ +.alert-tip { + background-color: $theme-gray-100; + color: $theme-gray-900; +} diff --git a/app/assets/stylesheets/framework/callout.scss b/app/assets/stylesheets/framework/callout.scss index bdd7f09d926..0d8e4afa76f 100644 --- a/app/assets/stylesheets/framework/callout.scss +++ b/app/assets/stylesheets/framework/callout.scss @@ -33,7 +33,11 @@ .bs-callout-warning { background-color: $orange-100; border-color: $orange-200; - color: $orange-700; + color: $orange-900; + + a { + color: $orange-900; + } } .bs-callout-info { diff --git a/app/assets/stylesheets/framework/filters.scss b/app/assets/stylesheets/framework/filters.scss index d5693a5d1a1..f48b3ddc912 100644 --- a/app/assets/stylesheets/framework/filters.scss +++ b/app/assets/stylesheets/framework/filters.scss @@ -363,6 +363,12 @@ background-color: $white-light; border-top: 0; } + + .filter-dropdown-container { + .dropdown { + margin-left: 0; + } + } } @include media-breakpoint-down(sm) { @@ -372,16 +378,6 @@ .dropdown-menu { width: 100%; } - - .dropdown { - margin-left: 0; - } - - .fa-chevron-down { - position: absolute; - right: 10px; - top: 10px; - } } } diff --git a/app/assets/stylesheets/framework/icons.scss b/app/assets/stylesheets/framework/icons.scss index abd26e38d18..8db7d63266e 100644 --- a/app/assets/stylesheets/framework/icons.scss +++ b/app/assets/stylesheets/framework/icons.scss @@ -80,3 +80,15 @@ .user-avatar-link { text-decoration: none; } + +.circle-icon-container { + $border-size: 1px; + + display: flex; + align-items: center; + justify-content: center; + border: $border-size solid $theme-gray-400; + border-radius: 50%; + padding: $gl-padding-8 - $border-size; + color: $theme-gray-700; +} diff --git a/app/assets/stylesheets/framework/secondary_navigation_elements.scss b/app/assets/stylesheets/framework/secondary_navigation_elements.scss index de9e7c37695..19640ab5986 100644 --- a/app/assets/stylesheets/framework/secondary_navigation_elements.scss +++ b/app/assets/stylesheets/framework/secondary_navigation_elements.scss @@ -158,6 +158,10 @@ width: 100%; } + .dropdown-menu-toggle { + margin-bottom: 0; + } + form { display: block; height: auto; diff --git a/app/assets/stylesheets/framework/timeline.scss b/app/assets/stylesheets/framework/timeline.scss index 4a311da1675..3d5208c3db5 100644 --- a/app/assets/stylesheets/framework/timeline.scss +++ b/app/assets/stylesheets/framework/timeline.scss @@ -31,16 +31,6 @@ .timeline-entry-inner { position: relative; - - @include notes-media('max', map-get($grid-breakpoints, sm)) { - .timeline-icon { - display: none; - } - - .timeline-content { - margin-left: 0; - } - } } &:target, diff --git a/app/assets/stylesheets/framework/variables.scss b/app/assets/stylesheets/framework/variables.scss index b43bb3feef5..bf2868710eb 100644 --- a/app/assets/stylesheets/framework/variables.scss +++ b/app/assets/stylesheets/framework/variables.scss @@ -243,6 +243,7 @@ $gl-padding-top: 10px; $gl-sidebar-padding: 22px; $gl-bar-padding: 3px; $input-horizontal-padding: 12px; +$browserScrollbarSize: 10px; /* * Misc diff --git a/app/assets/stylesheets/pages/merge_requests.scss b/app/assets/stylesheets/pages/merge_requests.scss index b075009b57c..221b4e934ff 100644 --- a/app/assets/stylesheets/pages/merge_requests.scss +++ b/app/assets/stylesheets/pages/merge_requests.scss @@ -50,9 +50,19 @@ .mr-widget-heading { position: relative; border: 1px solid $border-color; - border-radius: 4px; + border-radius: $border-radius-default; +} - &:not(.deploy-heading)::before { +.mr-widget-extension { + border-top: 1px solid $border-color; + background-color: $gray-light; +} + +.mr-widget-workflow { + margin-top: $gl-padding; + position: relative; + + &::before { content: ''; border-left: 1px solid $theme-gray-200; position: absolute; @@ -68,8 +78,8 @@ border-top: 0; } -.mr-widget-heading, .mr-widget-section, +.mr-widget-content, .mr-widget-footer { padding: $gl-padding; } @@ -560,19 +570,6 @@ color: $gl-text-color; } - .git-merge-icon-container { - border: 1px solid $theme-gray-400; - border-radius: 50%; - height: 32px; - width: 32px; - color: $theme-gray-700; - line-height: 28px; - - .ic-git-merge { - vertical-align: middle; - width: 31px; - } - } .git-merge-container { justify-content: space-between; @@ -854,11 +851,6 @@ } .deploy-heading { - margin-top: -19px; - border-top-left-radius: 0; - border-top-right-radius: 0; - background-color: $gray-light; - @include media-breakpoint-up(md) { padding: $gl-padding-8 $gl-padding; } @@ -868,6 +860,10 @@ font-size: 12px; margin-left: 48px; } + + &:not(:last-child) { + border-bottom: 1px solid $border-color; + } } .deploy-body { diff --git a/app/assets/stylesheets/pages/notes.scss b/app/assets/stylesheets/pages/notes.scss index 4fda2964fd5..0d1d0b4d2d6 100644 --- a/app/assets/stylesheets/pages/notes.scss +++ b/app/assets/stylesheets/pages/notes.scss @@ -589,12 +589,6 @@ $note-form-margin-left: 72px; padding-bottom: 0; } -.note-header-author-name { - @include notes-media('max', map-get($grid-breakpoints, sm) - 1) { - display: none; - } -} - .note-headline-light { display: inline; diff --git a/app/assets/stylesheets/pages/projects.scss b/app/assets/stylesheets/pages/projects.scss index 80ec390d18e..6cc21072acd 100644 --- a/app/assets/stylesheets/pages/projects.scss +++ b/app/assets/stylesheets/pages/projects.scss @@ -723,7 +723,8 @@ .scrolling-tabs-container { .scrolling-tabs { margin-top: $gl-padding-8; - margin-bottom: $gl-padding-8; + margin-bottom: $gl-padding-8 - $browserScrollbarSize; + padding-bottom: $browserScrollbarSize; flex-wrap: wrap; border-bottom: 0; } @@ -731,7 +732,7 @@ .fade-left, .fade-right { top: 0; - height: 100%; + height: calc(100% - #{$browserScrollbarSize}); .fa { top: 50%; diff --git a/app/assets/stylesheets/pages/tree.scss b/app/assets/stylesheets/pages/tree.scss index dc5ca78ff58..a46b8679a42 100644 --- a/app/assets/stylesheets/pages/tree.scss +++ b/app/assets/stylesheets/pages/tree.scss @@ -104,11 +104,23 @@ border-bottom: 1px solid $white-normal; border-top: 1px solid $white-normal; + &:last-of-type { + border-bottom-color: $white-light; + } + td, th { line-height: 21px; } + th { + border-top-color: $gray-light; + } + + td { + border-color: $border-color; + } + &:hover:not(.tree-truncated-warning) { td { background-color: $blue-50; diff --git a/app/models/ci/pipeline.rb b/app/models/ci/pipeline.rb index 9512ba42f67..1487b9d3bca 100644 --- a/app/models/ci/pipeline.rb +++ b/app/models/ci/pipeline.rb @@ -26,6 +26,8 @@ module Ci has_many :builds, foreign_key: :commit_id, inverse_of: :pipeline has_many :trigger_requests, dependent: :destroy, foreign_key: :commit_id # rubocop:disable Cop/ActiveRecordDependent has_many :variables, class_name: 'Ci::PipelineVariable' + has_many :deployments, through: :builds + has_many :environments, -> { distinct }, through: :deployments # Merge requests for which the current pipeline is running against # the merge request's latest commit. @@ -523,10 +525,6 @@ module Ci yaml_errors.present? end - def environments - builds.where.not(environment: nil).success.pluck(:environment).uniq - end - # Manually set the notes for a Ci::Pipeline # There is no ActiveRecord relation between Ci::Pipeline and notes # as they are related to a commit sha. This method helps importing diff --git a/app/models/clusters/applications/jupyter.rb b/app/models/clusters/applications/jupyter.rb index e43a0fd1786..421a923d386 100644 --- a/app/models/clusters/applications/jupyter.rb +++ b/app/models/clusters/applications/jupyter.rb @@ -56,7 +56,11 @@ module Clusters def specification { "ingress" => { - "hosts" => [hostname] + "hosts" => [hostname], + "tls" => [{ + "hosts" => [hostname], + "secretName" => "jupyter-cert" + }] }, "hub" => { "extraEnv" => { diff --git a/app/models/concerns/shardable.rb b/app/models/concerns/shardable.rb new file mode 100644 index 00000000000..57cd77b44b4 --- /dev/null +++ b/app/models/concerns/shardable.rb @@ -0,0 +1,18 @@ +# frozen_string_literal: true + +module Shardable + extend ActiveSupport::Concern + + included do + belongs_to :shard + validates :shard, presence: true + end + + def shard_name + shard&.name + end + + def shard_name=(name) + self.shard = Shard.by_name(name) + end +end diff --git a/app/models/environment_status.rb b/app/models/environment_status.rb index 4a128dde5cd..2fb6cadc8cd 100644 --- a/app/models/environment_status.rb +++ b/app/models/environment_status.rb @@ -12,13 +12,13 @@ class EnvironmentStatus delegate :deployed_at, to: :deployment, allow_nil: true def self.for_merge_request(mr, user) - build_environments_status(mr, user, mr.diff_head_sha) + build_environments_status(mr, user, mr.actual_head_pipeline) end def self.after_merge_request(mr, user) return [] unless mr.merged? - build_environments_status(mr, user, mr.merge_commit_sha) + build_environments_status(mr, user, mr.merge_pipeline) end def initialize(environment, merge_request, sha) @@ -61,13 +61,13 @@ class EnvironmentStatus } end - def self.build_environments_status(mr, user, sha) - Environment.where(project_id: [mr.source_project_id, mr.target_project_id]) - .available - .with_deployment(sha).map do |environment| + def self.build_environments_status(mr, user, pipeline) + return [] unless pipeline + + pipeline.environments.available.map do |environment| next unless Ability.allowed?(user, :read_environment, environment) - EnvironmentStatus.new(environment, mr, sha) + EnvironmentStatus.new(environment, mr, pipeline.sha) end.compact end private_class_method :build_environments_status diff --git a/app/models/notification_setting.rb b/app/models/notification_setting.rb index 1600acfc575..e82eaf4e069 100644 --- a/app/models/notification_setting.rb +++ b/app/models/notification_setting.rb @@ -5,7 +5,7 @@ class NotificationSetting < ActiveRecord::Base ignore_column :events - enum level: { global: 3, watch: 2, mention: 4, participating: 1, disabled: 0, custom: 5 } + enum level: { global: 3, watch: 2, participating: 1, mention: 4, disabled: 0, custom: 5 } default_value_for :level, NotificationSetting.levels[:global] diff --git a/app/models/pool_repository.rb b/app/models/pool_repository.rb index 7351674201e..bad0e30ceb5 100644 --- a/app/models/pool_repository.rb +++ b/app/models/pool_repository.rb @@ -1,21 +1,12 @@ # frozen_string_literal: true class PoolRepository < ActiveRecord::Base - belongs_to :shard - validates :shard, presence: true + include Shardable has_many :member_projects, class_name: 'Project' after_create :correct_disk_path - def shard_name - shard&.name - end - - def shard_name=(name) - self.shard = Shard.by_name(name) - end - private def correct_disk_path diff --git a/app/models/project.rb b/app/models/project.rb index 185fd76cbbc..ade20cc8948 100644 --- a/app/models/project.rb +++ b/app/models/project.rb @@ -186,6 +186,7 @@ class Project < ActiveRecord::Base has_one :import_state, autosave: true, class_name: 'ProjectImportState', inverse_of: :project has_one :import_export_upload, dependent: :destroy # rubocop:disable Cop/ActiveRecordDependent + has_one :project_repository, inverse_of: :project # Merge Requests for target project should be removed with it has_many :merge_requests, foreign_key: 'target_project_id', inverse_of: :target_project @@ -1206,6 +1207,13 @@ class Project < ActiveRecord::Base false end + def track_project_repository + return unless hashed_storage?(:repository) + + project_repo = project_repository || build_project_repository + project_repo.update!(shard_name: repository_storage, disk_path: disk_path) + end + def create_repository(force: false) # Forked import is handled asynchronously return if forked? && !force diff --git a/app/models/project_repository.rb b/app/models/project_repository.rb new file mode 100644 index 00000000000..38913f3f2f5 --- /dev/null +++ b/app/models/project_repository.rb @@ -0,0 +1,13 @@ +# frozen_string_literal: true + +class ProjectRepository < ActiveRecord::Base + include Shardable + + belongs_to :project, inverse_of: :project_repository + + class << self + def find_project(disk_path) + find_by(disk_path: disk_path)&.project + end + end +end diff --git a/app/services/ci/create_pipeline_service.rb b/app/services/ci/create_pipeline_service.rb index 92a8438ab2f..46a82377c10 100644 --- a/app/services/ci/create_pipeline_service.rb +++ b/app/services/ci/create_pipeline_service.rb @@ -4,6 +4,8 @@ module Ci class CreatePipelineService < BaseService attr_reader :pipeline + CreateError = Class.new(StandardError) + SEQUENCE = [Gitlab::Ci::Pipeline::Chain::Build, Gitlab::Ci::Pipeline::Chain::Validate::Abilities, Gitlab::Ci::Pipeline::Chain::Validate::Repository, @@ -47,6 +49,14 @@ module Ci pipeline end + def execute!(*args, &block) + execute(*args, &block).tap do |pipeline| + unless pipeline.persisted? + raise CreateError, pipeline.errors.full_messages.join(',') + end + end + end + private def commit diff --git a/app/services/projects/create_service.rb b/app/services/projects/create_service.rb index 481de34b977..9e77a3237e3 100644 --- a/app/services/projects/create_service.rb +++ b/app/services/projects/create_service.rb @@ -9,7 +9,7 @@ module Projects end def execute - if @params[:template_name]&.present? + if @params[:template_name].present? return ::Projects::CreateFromTemplateService.new(current_user, params).execute end @@ -86,6 +86,8 @@ module Projects @project.create_wiki unless skip_wiki? end + @project.track_project_repository + event_service.create_project(@project, current_user) system_hook_service.execute_hooks_for(@project, :create) diff --git a/app/services/projects/hashed_storage/migrate_repository_service.rb b/app/services/projects/hashed_storage/migrate_repository_service.rb index 4462d504071..f3e026ba38c 100644 --- a/app/services/projects/hashed_storage/migrate_repository_service.rb +++ b/app/services/projects/hashed_storage/migrate_repository_service.rb @@ -30,6 +30,7 @@ module Projects if result project.write_repository_config + project.track_project_repository else rollback_folder_move project.storage_version = nil diff --git a/app/views/admin/runners/_sort_dropdown.html.haml b/app/views/admin/runners/_sort_dropdown.html.haml index b201e6bf10e..19c2a50ebd9 100644 --- a/app/views/admin/runners/_sort_dropdown.html.haml +++ b/app/views/admin/runners/_sort_dropdown.html.haml @@ -1,7 +1,7 @@ - sorted_by = sort_options_hash[@sort] .dropdown.inline.prepend-left-10 - %button.dropdown-toggle{ type: 'button', data: { toggle: 'dropdown', display: 'static' } } + %button.dropdown-menu-toggle{ type: 'button', data: { toggle: 'dropdown', display: 'static' } } = sorted_by = icon('chevron-down') %ul.dropdown-menu.dropdown-menu-right.dropdown-menu-selectable.dropdown-menu-sort diff --git a/app/views/explore/projects/_filter.html.haml b/app/views/explore/projects/_filter.html.haml index b694103ccaf..f518205f14c 100644 --- a/app/views/explore/projects/_filter.html.haml +++ b/app/views/explore/projects/_filter.html.haml @@ -1,8 +1,8 @@ - if current_user .dropdown - %button.dropdown-toggle{ href: '#', "data-toggle" => "dropdown", 'data-display' => 'static' } - = icon('globe') - %span.light= _("Visibility:") + %button.dropdown-menu-toggle{ href: '#', "data-toggle" => "dropdown", 'data-display' => 'static' } + = icon('globe', class: 'mt-1') + %span.light.ml-3= _("Visibility:") - if params[:visibility_level].present? = visibility_level_label(params[:visibility_level].to_i) - else diff --git a/app/views/groups/edit.html.haml b/app/views/groups/edit.html.haml index 869c54d89ea..39d0f620283 100644 --- a/app/views/groups/edit.html.haml +++ b/app/views/groups/edit.html.haml @@ -37,6 +37,7 @@ .settings-content = render 'shared/badges/badge_settings' += render_if_exists 'groups/custom_project_templates_setting' = render_if_exists 'groups/templates_setting', expanded: expanded %section.settings.gs-advanced.no-animate#js-advanced-settings{ class: ('expanded' if expanded) } diff --git a/app/views/invites/show.html.haml b/app/views/invites/show.html.haml index c2bb1216c5f..30ab5781014 100644 --- a/app/views/invites/show.html.haml +++ b/app/views/invites/show.html.haml @@ -1,5 +1,5 @@ -- page_title "Invitation" -%h3.page-title Invitation +- page_title _("Invitation") +%h3.page-title= _("Invitation") %p You have been invited @@ -24,14 +24,17 @@ - if is_member %p - However, you are already a member of this #{@member.source.is_a?(Group) ? "group" : "project"}. - Sign in using a different account to accept the invitation. + - member_source = @member.source.is_a?(Group) ? _("group") : _("project") + = _("However, you are already a member of this %{member_source}. Sign in using a different account to accept the invitation.") % { member_source: member_source } - if @member.invite_email != current_user.email %p - Note that this invitation was sent to #{mail_to @member.invite_email}, but you are signed in as #{link_to current_user.to_reference, user_url(current_user)} with email #{mail_to current_user.email}. + - mail_to_invite_email = mail_to(@member.invite_email) + - mail_to_current_user = mail_to(current_user.email) + - link_to_current_user = link_to(current_user.to_reference, user_url(current_user)) + = _("Note that this invitation was sent to %{mail_to_invite_email}, but you are signed in as %{link_to_current_user} with email %{mail_to_current_user}.").html_safe % { mail_to_invite_email: mail_to_invite_email, mail_to_current_user: mail_to_current_user, link_to_current_user: link_to_current_user } - unless is_member .actions - = link_to "Accept invitation", accept_invite_url(@token), method: :post, class: "btn btn-success" - = link_to "Decline", decline_invite_url(@token), method: :post, class: "btn btn-danger prepend-left-10" + = link_to _("Accept invitation"), accept_invite_url(@token), method: :post, class: "btn btn-success" + = link_to _("Decline"), decline_invite_url(@token), method: :post, class: "btn btn-danger prepend-left-10" diff --git a/app/views/projects/forks/index.html.haml b/app/views/projects/forks/index.html.haml index b44ea89510b..c63c34c4ebb 100644 --- a/app/views/projects/forks/index.html.haml +++ b/app/views/projects/forks/index.html.haml @@ -9,7 +9,7 @@ spellcheck: false, data: { 'filter-selector' => 'span.namespace-name' } .dropdown - %button.dropdown-toggle{ type: 'button', 'data-toggle' => 'dropdown' } + %button.dropdown-menu-toggle{ type: 'button', 'data-toggle' => 'dropdown' } %span.light sort: - if @sort.present? = sort_options_hash[@sort] diff --git a/app/views/projects/new.html.haml b/app/views/projects/new.html.haml index 10e3b01096a..a760d02c4c3 100644 --- a/app/views/projects/new.html.haml +++ b/app/views/projects/new.html.haml @@ -50,7 +50,7 @@ .project-template .form-group %div - = render 'project_templates', f: f + = render 'project_templates', f: f, project: @project .tab-pane.import-project-pane.js-toggle-container{ id: 'import-project-pane', class: active_when(active_tab == 'import'), role: 'tabpanel' } - if import_sources_enabled? diff --git a/app/views/projects/project_templates/_built_in_templates.html.haml b/app/views/projects/project_templates/_built_in_templates.html.haml index 233c3adba0e..5b4d8927045 100644 --- a/app/views/projects/project_templates/_built_in_templates.html.haml +++ b/app/views/projects/project_templates/_built_in_templates.html.haml @@ -9,9 +9,9 @@ .text-muted = template.description .controls.d-flex.align-items-center - %label.btn.btn-success.template-button.choose-template.append-right-10.append-bottom-0{ for: template.name } + %a.btn.btn-default.append-right-10{ href: template.preview, rel: 'noopener noreferrer', target: '_blank', data: { track_label: "create_from_template", track_property: "template_preview", track_event: "click_button", track_value: template.name } } + = _("Preview") + %label.btn.btn-success.template-button.choose-template.append-bottom-0{ for: template.name } %input{ type: "radio", autocomplete: "off", name: "project[template_name]", id: template.name, value: template.name, data: { track_label: "create_from_template", track_property: "template_use", track_event: "click_button" } } %span = _("Use template") - %a.btn.btn-default{ href: template.preview, rel: 'noopener noreferrer', target: '_blank', data: { track_label: "create_from_template", track_property: "template_preview", track_event: "click_button", track_value: template.name } } - = _("Preview") diff --git a/app/views/projects/tags/index.html.haml b/app/views/projects/tags/index.html.haml index 37535370940..026bc44a05f 100644 --- a/app/views/projects/tags/index.html.haml +++ b/app/views/projects/tags/index.html.haml @@ -14,7 +14,7 @@ = search_field_tag :search, params[:search], { placeholder: s_('TagsPage|Filter by tag name'), id: 'tag-search', class: 'form-control search-text-input input-short', spellcheck: false } .dropdown - %button.dropdown-toggle{ type: 'button', data: { toggle: 'dropdown'} } + %button.dropdown-menu-toggle{ type: 'button', data: { toggle: 'dropdown'} } %span.light = tags_sort_options_hash[@sort] = icon('chevron-down') diff --git a/app/views/projects/tree/_tree_content.html.haml b/app/views/projects/tree/_tree_content.html.haml index 5e0523f0b96..889a13339fd 100644 --- a/app/views/projects/tree/_tree_content.html.haml +++ b/app/views/projects/tree/_tree_content.html.haml @@ -1,5 +1,5 @@ .tree-content-holder.js-tree-content{ 'data-logs-path': @logs_path } - .table-holder + .table-holder.bordered-box %table.table#tree-slider{ class: "table_#{@hex_path} tree-table qa-file-tree" } %thead %tr diff --git a/app/views/shared/_auto_devops_implicitly_enabled_banner.html.haml b/app/views/shared/_auto_devops_implicitly_enabled_banner.html.haml index 6c4607b2f16..0d0a3c1aa64 100644 --- a/app/views/shared/_auto_devops_implicitly_enabled_banner.html.haml +++ b/app/views/shared/_auto_devops_implicitly_enabled_banner.html.haml @@ -1,6 +1,6 @@ - if show_auto_devops_implicitly_enabled_banner?(project) .auto-devops-implicitly-enabled-banner.alert.alert-warning - - more_information_link = link_to _('More information'), 'https://docs.gitlab.com/ee/topics/autodevops/', class: 'alert-link' + - more_information_link = link_to _('More information'), help_page_path('topics/autodevops/index.md'), target: '_blank', class: 'alert-link' - auto_devops_message = s_("AutoDevOps|The Auto DevOps pipeline has been enabled and will be used if no alternative CI configuration file is found. %{more_information_link}") % { more_information_link: more_information_link } = auto_devops_message.html_safe .alert-link-group diff --git a/app/views/shared/_milestones_sort_dropdown.html.haml b/app/views/shared/_milestones_sort_dropdown.html.haml index a6ba3b59365..bd68a3e4c84 100644 --- a/app/views/shared/_milestones_sort_dropdown.html.haml +++ b/app/views/shared/_milestones_sort_dropdown.html.haml @@ -1,5 +1,5 @@ .dropdown.inline.prepend-left-10 - %button.dropdown-toggle{ type: 'button', data: { toggle: 'dropdown' } } + %button.dropdown-menu-toggle{ type: 'button', data: { toggle: 'dropdown' } } %span.light - if @sort.present? = milestone_sort_options_hash[@sort] diff --git a/app/views/shared/_sort_dropdown.html.haml b/app/views/shared/_sort_dropdown.html.haml index be6d4f1c32b..e4463c1e0d8 100644 --- a/app/views/shared/_sort_dropdown.html.haml +++ b/app/views/shared/_sort_dropdown.html.haml @@ -2,7 +2,7 @@ - viewing_issues = controller.controller_name == 'issues' || controller.action_name == 'issues' .dropdown.inline.prepend-left-10 - %button.dropdown-toggle{ type: 'button', data: { toggle: 'dropdown', display: 'static' } } + %button.dropdown-menu-toggle{ type: 'button', data: { toggle: 'dropdown', display: 'static' } } = sorted_by = icon('chevron-down') %ul.dropdown-menu.dropdown-menu-right.dropdown-menu-selectable.dropdown-menu-sort diff --git a/app/views/shared/groups/_dropdown.html.haml b/app/views/shared/groups/_dropdown.html.haml index 2237b93a10b..1ae6d1f5ee3 100644 --- a/app/views/shared/groups/_dropdown.html.haml +++ b/app/views/shared/groups/_dropdown.html.haml @@ -9,7 +9,7 @@ - default_sort_by = sort_value_recently_created .dropdown.inline.js-group-filter-dropdown-wrap.append-right-10 - %button.dropdown-toggle{ type: 'button', 'data-toggle' => 'dropdown' } + %button.dropdown-menu-toggle{ type: 'button', 'data-toggle' => 'dropdown' } %span.dropdown-label = options_hash[default_sort_by] = icon('chevron-down') diff --git a/app/views/shared/labels/_sort_dropdown.html.haml b/app/views/shared/labels/_sort_dropdown.html.haml index 8a7d037e15b..d664ef1cc2f 100644 --- a/app/views/shared/labels/_sort_dropdown.html.haml +++ b/app/views/shared/labels/_sort_dropdown.html.haml @@ -1,6 +1,6 @@ - sort_title = label_sort_options_hash[@sort] || sort_title_name_desc .dropdown.inline - %button.dropdown-toggle{ type: 'button', data: { toggle: 'dropdown' } } + %button.dropdown-menu-toggle{ type: 'button', data: { toggle: 'dropdown' } } = sort_title = icon('chevron-down') %ul.dropdown-menu.dropdown-menu-right.dropdown-menu-sort diff --git a/app/workers/pipeline_schedule_worker.rb b/app/workers/pipeline_schedule_worker.rb index 85d1ffe0fa9..ac4e9710f33 100644 --- a/app/workers/pipeline_schedule_worker.rb +++ b/app/workers/pipeline_schedule_worker.rb @@ -9,18 +9,36 @@ class PipelineScheduleWorker Ci::PipelineSchedule.active.where("next_run_at < ?", Time.now) .preload(:owner, :project).find_each do |schedule| begin - pipeline = Ci::CreatePipelineService.new(schedule.project, - schedule.owner, - ref: schedule.ref) - .execute(:schedule, ignore_skip_ci: true, save_on_errors: false, schedule: schedule) - - schedule.deactivate! unless pipeline.persisted? + Ci::CreatePipelineService.new(schedule.project, + schedule.owner, + ref: schedule.ref) + .execute!(:schedule, ignore_skip_ci: true, save_on_errors: true, schedule: schedule) rescue => e - Rails.logger.error "#{schedule.id}: Failed to create a scheduled pipeline: #{e.message}" + error(schedule, e) ensure schedule.schedule_next_run! end end end # rubocop: enable CodeReuse/ActiveRecord + + private + + def error(schedule, error) + failed_creation_counter.increment + + Rails.logger.error "Failed to create a scheduled pipeline. " \ + "schedule_id: #{schedule.id} message: #{error.message}" + + Gitlab::Sentry + .track_exception(error, + issue_url: 'https://gitlab.com/gitlab-org/gitlab-ce/issues/41231', + extra: { schedule_id: schedule.id }) + end + + def failed_creation_counter + @failed_creation_counter ||= + Gitlab::Metrics.counter(:pipeline_schedule_creation_failed_total, + "Counter of failed attempts of pipeline schedule creation") + end end diff --git a/changelogs/unreleased/1979-redesign-mr-widget-approvals-ce.yml b/changelogs/unreleased/1979-redesign-mr-widget-approvals-ce.yml new file mode 100644 index 00000000000..d05b6054b22 --- /dev/null +++ b/changelogs/unreleased/1979-redesign-mr-widget-approvals-ce.yml @@ -0,0 +1,5 @@ +--- +title: Redesign of MR header sections (CE) +merge_request: 23465 +author: +type: changed diff --git a/changelogs/unreleased/48496-merge-request-refactor-does-not-highlight-selected-line.yml b/changelogs/unreleased/48496-merge-request-refactor-does-not-highlight-selected-line.yml new file mode 100644 index 00000000000..cfc74bef638 --- /dev/null +++ b/changelogs/unreleased/48496-merge-request-refactor-does-not-highlight-selected-line.yml @@ -0,0 +1,5 @@ +--- +title: When user clicks linenumber in MR changes, highlight that line +merge_request: +author: +type: fixed diff --git a/changelogs/unreleased/50264-add-border-around-the-repository-file-tree.yml b/changelogs/unreleased/50264-add-border-around-the-repository-file-tree.yml new file mode 100644 index 00000000000..6315c3e7f36 --- /dev/null +++ b/changelogs/unreleased/50264-add-border-around-the-repository-file-tree.yml @@ -0,0 +1,5 @@ +--- +title: Resolve Add border around the repository file tree +merge_request: 23018 +author: +type: changed diff --git a/changelogs/unreleased/51029-status-emoji-currently-replaces-avatar-on-mobile.yml b/changelogs/unreleased/51029-status-emoji-currently-replaces-avatar-on-mobile.yml new file mode 100644 index 00000000000..dc11ede5c8d --- /dev/null +++ b/changelogs/unreleased/51029-status-emoji-currently-replaces-avatar-on-mobile.yml @@ -0,0 +1,5 @@ +--- +title: Resolve status emoji being replaced by avatar on mobile +merge_request: 23408 +author: +type: other diff --git a/changelogs/unreleased/53763-fix-encrypt-columns-data-loss.yml b/changelogs/unreleased/53763-fix-encrypt-columns-data-loss.yml deleted file mode 100644 index 44362a8622e..00000000000 --- a/changelogs/unreleased/53763-fix-encrypt-columns-data-loss.yml +++ /dev/null @@ -1,5 +0,0 @@ ---- -title: Correctly handle data-loss scenarios when encrypting columns -merge_request: 23306 -author: -type: fixed diff --git a/changelogs/unreleased/54218-fix-mergeUrlParams.yml b/changelogs/unreleased/54218-fix-mergeUrlParams.yml new file mode 100644 index 00000000000..dae06b66e8e --- /dev/null +++ b/changelogs/unreleased/54218-fix-mergeUrlParams.yml @@ -0,0 +1,5 @@ +--- +title: "Fix mergeUrlParams with fragment URL" +merge_request: 54218 +author: Thomas Holder +type: fixed diff --git a/changelogs/unreleased/54648-fix-order-by-dropdown-tablet-screens.yml b/changelogs/unreleased/54648-fix-order-by-dropdown-tablet-screens.yml new file mode 100644 index 00000000000..671d1590991 --- /dev/null +++ b/changelogs/unreleased/54648-fix-order-by-dropdown-tablet-screens.yml @@ -0,0 +1,5 @@ +--- +title: Fix Order By dropdown menu styling in tablet and mobile screens +merge_request: 23446 +author: +type: fixed diff --git a/changelogs/unreleased/bump_gpgme_gem.yml b/changelogs/unreleased/bump_gpgme_gem.yml new file mode 100644 index 00000000000..4c0067cb824 --- /dev/null +++ b/changelogs/unreleased/bump_gpgme_gem.yml @@ -0,0 +1,5 @@ +--- +title: Bump gpgme gem version from 2.0.13 to 2.0.18 +merge_request: +author: asaparov +type: other diff --git a/changelogs/unreleased/ce-52811-fix_namespaces_api_routing.yml b/changelogs/unreleased/ce-52811-fix_namespaces_api_routing.yml new file mode 100644 index 00000000000..b5fd99c304f --- /dev/null +++ b/changelogs/unreleased/ce-52811-fix_namespaces_api_routing.yml @@ -0,0 +1,5 @@ +--- +title: Fix API::Namespaces routing to accept namepaces with dots +merge_request: 22912 +author: +type: fixed diff --git a/changelogs/unreleased/ce-53347_fix_impersonation_tokens.yml b/changelogs/unreleased/ce-53347_fix_impersonation_tokens.yml deleted file mode 100644 index 6cc743d6f3a..00000000000 --- a/changelogs/unreleased/ce-53347_fix_impersonation_tokens.yml +++ /dev/null @@ -1,5 +0,0 @@ ---- -title: Display impersonation token value only after creation -merge_request: 22916 -author: -type: fixed diff --git a/changelogs/unreleased/fix-mr-widget-unrelated-deployment-status.yml b/changelogs/unreleased/fix-mr-widget-unrelated-deployment-status.yml new file mode 100644 index 00000000000..ab926fbd43b --- /dev/null +++ b/changelogs/unreleased/fix-mr-widget-unrelated-deployment-status.yml @@ -0,0 +1,5 @@ +--- +title: Fix unrelated deployment status in MR widget +merge_request: 23175 +author: +type: fixed diff --git a/changelogs/unreleased/gt-externalize-app-views-invites.yml b/changelogs/unreleased/gt-externalize-app-views-invites.yml new file mode 100644 index 00000000000..b5a22177f9b --- /dev/null +++ b/changelogs/unreleased/gt-externalize-app-views-invites.yml @@ -0,0 +1,5 @@ +--- +title: Externalize strings from `/app/views/invites` +merge_request: 23205 +author: Tao Wang +type: other diff --git a/changelogs/unreleased/ignore-failed-pipeline-creation-on-pipeline-schedule.yml b/changelogs/unreleased/ignore-failed-pipeline-creation-on-pipeline-schedule.yml new file mode 100644 index 00000000000..90f47aa12db --- /dev/null +++ b/changelogs/unreleased/ignore-failed-pipeline-creation-on-pipeline-schedule.yml @@ -0,0 +1,5 @@ +--- +title: Remove auto deactivation when failed to create a pipeline via pipeline schedules +merge_request: 22243 +author: +type: changed diff --git a/changelogs/unreleased/improve_auto_devops_migration_debug.yml b/changelogs/unreleased/improve_auto_devops_migration_debug.yml new file mode 100644 index 00000000000..96a78808361 --- /dev/null +++ b/changelogs/unreleased/improve_auto_devops_migration_debug.yml @@ -0,0 +1,6 @@ +--- +title: 'Auto DevOps: Add echo for each branch of the deploy() function where we run + helm upgrade' +merge_request: 23499 +author: +type: changed diff --git a/changelogs/unreleased/jivl-add-empty-state-graphs-null-values.yml b/changelogs/unreleased/jivl-add-empty-state-graphs-null-values.yml new file mode 100644 index 00000000000..d21254b16d0 --- /dev/null +++ b/changelogs/unreleased/jivl-add-empty-state-graphs-null-values.yml @@ -0,0 +1,5 @@ +--- +title: Add empty state for graphs with no values +merge_request: 22630 +author: +type: fixed diff --git a/changelogs/unreleased/jupyter-tls.yml b/changelogs/unreleased/jupyter-tls.yml new file mode 100644 index 00000000000..4111edd34ff --- /dev/null +++ b/changelogs/unreleased/jupyter-tls.yml @@ -0,0 +1,5 @@ +--- +title: "#52753: HTTPS for JupyterHub installation" +merge_request: 23479 +author: Amit Rathi +type: added diff --git a/changelogs/unreleased/non-webkit-scrollbar-fixing.yml b/changelogs/unreleased/non-webkit-scrollbar-fixing.yml new file mode 100644 index 00000000000..526a9f25486 --- /dev/null +++ b/changelogs/unreleased/non-webkit-scrollbar-fixing.yml @@ -0,0 +1,5 @@ +--- +title: Fix horizontal scrollbar overlapping on horizontal scrolling-tabs +merge_request: 23167 +author: Harry Kiselev +type: other diff --git a/changelogs/unreleased/order-of-notification-settings.yml b/changelogs/unreleased/order-of-notification-settings.yml new file mode 100644 index 00000000000..0f0243bcb40 --- /dev/null +++ b/changelogs/unreleased/order-of-notification-settings.yml @@ -0,0 +1,5 @@ +--- +title: reorder notification settings by noisy-ness +merge_request: +author: C.J. Jameson +type: changed diff --git a/changelogs/unreleased/tc-repo-full-path-in-db.yml b/changelogs/unreleased/tc-repo-full-path-in-db.yml new file mode 100644 index 00000000000..ead8feabeb9 --- /dev/null +++ b/changelogs/unreleased/tc-repo-full-path-in-db.yml @@ -0,0 +1,5 @@ +--- +title: Add model and relation to store repo full path in database +merge_request: 23143 +author: +type: added diff --git a/changelogs/unreleased/winh-merge-request-commit-discussion.yml b/changelogs/unreleased/winh-merge-request-commit-discussion.yml new file mode 100644 index 00000000000..b0c6264369b --- /dev/null +++ b/changelogs/unreleased/winh-merge-request-commit-discussion.yml @@ -0,0 +1,5 @@ +--- +title: Display commit ID for commit diff discussion on merge request +merge_request: 23370 +author: +type: fixed diff --git a/changelogs/unreleased/workhorse-7-3-0.yml b/changelogs/unreleased/workhorse-7-3-0.yml new file mode 100644 index 00000000000..6708b8a3cbb --- /dev/null +++ b/changelogs/unreleased/workhorse-7-3-0.yml @@ -0,0 +1,5 @@ +--- +title: Upgrade GitLab Workhorse to v7.3.0 +merge_request: 23489 +author: +type: other diff --git a/db/migrate/20181122160027_create_project_repositories.rb b/db/migrate/20181122160027_create_project_repositories.rb new file mode 100644 index 00000000000..e42cef9b1c6 --- /dev/null +++ b/db/migrate/20181122160027_create_project_repositories.rb @@ -0,0 +1,18 @@ +# frozen_string_literal: true + +# See http://doc.gitlab.com/ce/development/migration_style_guide.html +# for more information on how to write migrations for GitLab. + +class CreateProjectRepositories < ActiveRecord::Migration[5.0] + include Gitlab::Database::MigrationHelpers + + DOWNTIME = false + + def change + create_table :project_repositories, id: :bigserial do |t| + t.references :shard, null: false, index: true, foreign_key: { on_delete: :restrict } + t.string :disk_path, null: false, index: { unique: true } + t.references :project, null: false, index: { unique: true }, foreign_key: { on_delete: :cascade } + end + end +end diff --git a/db/migrate/20181123042307_drop_site_statistics.rb b/db/post_migrate/20181123042307_drop_site_statistics.rb index 8986374ef65..8986374ef65 100644 --- a/db/migrate/20181123042307_drop_site_statistics.rb +++ b/db/post_migrate/20181123042307_drop_site_statistics.rb diff --git a/db/schema.rb b/db/schema.rb index 4f9588fd86b..995619bdc69 100644 --- a/db/schema.rb +++ b/db/schema.rb @@ -1601,6 +1601,15 @@ ActiveRecord::Schema.define(version: 20181126153547) do t.index ["status"], name: "index_project_mirror_data_on_status", using: :btree end + create_table "project_repositories", id: :bigserial, force: :cascade do |t| + t.integer "shard_id", null: false + t.string "disk_path", null: false + t.integer "project_id", null: false + t.index ["disk_path"], name: "index_project_repositories_on_disk_path", unique: true, using: :btree + t.index ["project_id"], name: "index_project_repositories_on_project_id", unique: true, using: :btree + t.index ["shard_id"], name: "index_project_repositories_on_shard_id", using: :btree + end + create_table "project_statistics", force: :cascade do |t| t.integer "project_id", null: false t.integer "namespace_id", null: false @@ -2385,6 +2394,8 @@ ActiveRecord::Schema.define(version: 20181126153547) do add_foreign_key "project_group_links", "projects", name: "fk_daa8cee94c", on_delete: :cascade add_foreign_key "project_import_data", "projects", name: "fk_ffb9ee3a10", on_delete: :cascade add_foreign_key "project_mirror_data", "projects", on_delete: :cascade + add_foreign_key "project_repositories", "projects", on_delete: :cascade + add_foreign_key "project_repositories", "shards", on_delete: :restrict add_foreign_key "project_statistics", "projects", on_delete: :cascade add_foreign_key "projects", "pool_repositories", name: "fk_6e5c14658a", on_delete: :nullify add_foreign_key "prometheus_metrics", "projects", on_delete: :cascade diff --git a/doc/administration/gitaly/index.md b/doc/administration/gitaly/index.md index 3395c503ced..dc6a71e2ebd 100644 --- a/doc/administration/gitaly/index.md +++ b/doc/administration/gitaly/index.md @@ -237,3 +237,14 @@ gitaly_enabled=false When you run `service gitlab restart` Gitaly will be disabled on this particular machine. + +## Troubleshooting Gitaly in production + +Since GitLab 11.6, Gitaly comes with a command-line tool called +`gitaly-debug` that can be run on a Gitaly server to aid in +troubleshooting. In GitLab 11.6 its only sub-command is +`simulate-http-clone` which allows you to measure the maximum possible +Git clone speed for a specific repository on the server. + +For an up to date list of sub-commands see [the gitaly-debug +README](https://gitlab.com/gitlab-org/gitaly/blob/master/cmd/gitaly-debug/README.md). diff --git a/doc/administration/monitoring/performance/request_profiling.md b/doc/administration/monitoring/performance/request_profiling.md index c358dfbead2..dfd9be3d04c 100644 --- a/doc/administration/monitoring/performance/request_profiling.md +++ b/doc/administration/monitoring/performance/request_profiling.md @@ -1,16 +1,17 @@ # Request Profiling ## Procedure + 1. Grab the profiling token from `Monitoring > Requests Profiles` admin page -(highlighted in a blue in the image below). -![Profile token](img/request_profiling_token.png) -1. Pass the header `X-Profile-Token: <token>` to the request you want to profile. You can use any of these tools - * [ModHeader](https://chrome.google.com/webstore/detail/modheader/idgpnmonknjnojddfkpgkljpfnnfcklj) Chrome extension - * [Modify Headers](https://addons.mozilla.org/en-US/firefox/addon/modify-headers/) Firefox extension - * `curl --header 'X-Profile-Token: <token>' https://gitlab.example.com/group/project` + (highlighted in a blue in the image below). + ![Profile token](img/request_profiling_token.png) +1. Pass the header `X-Profile-Token: <token>` to the request you want to profile. You can use: + - Browser extensions. For example, [ModHeader](https://chrome.google.com/webstore/detail/modheader/idgpnmonknjnojddfkpgkljpfnnfcklj) Chrome extension. + - `curl`. For example, `curl --header 'X-Profile-Token: <token>' https://gitlab.example.com/group/project`. 1. Once request is finished (which will take a little longer than usual), you can -view the profiling output from `Monitoring > Requests Profiles` admin page. -![Profiling output](img/request_profile_result.png) + view the profiling output from `Monitoring > Requests Profiles` admin page. + ![Profiling output](img/request_profile_result.png) ## Cleaning up + Profiling output will be cleared out every day via a Sidekiq worker. diff --git a/doc/api/README.md b/doc/api/README.md index 848a6e6b72b..b49c3a198f1 100644 --- a/doc/api/README.md +++ b/doc/api/README.md @@ -230,8 +230,7 @@ Impersonation tokens are used exactly like regular personal access tokens, and c > [Introduced](https://gitlab.com/gitlab-org/gitlab-ce/issues/40385) in GitLab 11.6. -By default, impersonation is enabled. To disable impersonation, GitLab must be -reconfigured: +By default, impersonation is enabled. To disable impersonation: **For Omnibus installations** @@ -577,7 +576,7 @@ When you try to access an API URL that does not exist you will receive 404 Not F ``` HTTP/1.1 404 Not Found Content-Type: application/json -{ f +{ "error": "404 Not Found" } ``` diff --git a/doc/ci/yaml/README.md b/doc/ci/yaml/README.md index e46b2bbc79c..af7e41db443 100644 --- a/doc/ci/yaml/README.md +++ b/doc/ci/yaml/README.md @@ -1590,7 +1590,7 @@ Possible values for `when` are: > [Introduced](https://gitlab.com/gitlab-org/gitlab-ce/merge_requests/22631) in GitLab 11.5. `parallel` allows you to configure how many instances of a job to run in -parallel. This value has to be greater than or equal to two (2) and less or equal than 50. +parallel. This value has to be greater than or equal to two (2) and less than or equal to 50. This creates N instances of the same job that run in parallel. They're named sequentially from `job_name 1/N` to `job_name N/N`. diff --git a/doc/development/architecture.md b/doc/development/architecture.md index 931a7a8e6d5..e65c5f05505 100644 --- a/doc/development/architecture.md +++ b/doc/development/architecture.md @@ -55,7 +55,7 @@ GitLab can be considered to have two layers from a process perspective: ### gitaly -- [Omnibus confiugration options](https://gitlab.com/gitlab-org/gitaly/tree/master/doc/configuration) +- [Omnibus configuration options](https://gitlab.com/gitlab-org/gitaly/tree/master/doc/configuration) - Layer: Core Service (Data) Gitaly is a service designed by GitLab to remove our need for NFS for Git storage in distributed deployments of GitLab. (Think GitLab.com or High Availablity Deployments) As of 11.3.0, This service handles all Git level access in GitLab. You can read more about the project [in the project's readme](https://gitlab.com/gitlab-org/gitaly). diff --git a/doc/development/code_review.md b/doc/development/code_review.md index 8a186df7f06..7788d155154 100644 --- a/doc/development/code_review.md +++ b/doc/development/code_review.md @@ -53,7 +53,7 @@ from teams other than your own. #### Security requirements - 1. If your merge request is processing, storing, or transferring any kind of [RED or ORANGE data][https://docs.google.com/document/d/15eNKGA3zyZazsJMldqTBFbYMnVUSQSpU14lo22JMZQY/edit] (this is a confidential document), it must be + 1. If your merge request is processing, storing, or transferring any kind of [RED or ORANGE data](https://docs.google.com/document/d/15eNKGA3zyZazsJMldqTBFbYMnVUSQSpU14lo22JMZQY/edit) (this is a confidential document), it must be **approved by a [Security Engineer][team]**. 1. If your merge request involves implementing, utilizing, or is otherwise related to any type of authentication, authorization, or session handling mechanism, it must be **approved by a [Security Engineer][team]**. diff --git a/doc/development/ee_features.md b/doc/development/ee_features.md index a227e2f6640..790b1bf951b 100644 --- a/doc/development/ee_features.md +++ b/doc/development/ee_features.md @@ -511,7 +511,7 @@ module EE params do requires :id, type: String, desc: 'The ID of a project' end - resource :projects, requirements: ::API::API::PROJECT_ENDPOINT_REQUIREMENTS do + resource :projects, requirements: ::API::API::NAMESPACE_OR_PROJECT_REQUIREMENTS do # ... end end diff --git a/doc/user/admin_area/custom_project_templates.md b/doc/user/admin_area/custom_project_templates.md new file mode 100644 index 00000000000..5afbf9f2934 --- /dev/null +++ b/doc/user/admin_area/custom_project_templates.md @@ -0,0 +1,25 @@ +# Custom instance-level project templates **[PREMIUM ONLY]** + +> [Introduced](https://gitlab.com/gitlab-org/gitlab-ee/issues/6860) in [GitLab Premium](https://about.gitlab.com/pricing) 11.2. + +When you create a new project, creating it based on custom project templates is +a convenient option to bootstrap from an existing project boilerplate. +The administration setting to configure a GitLab group that serves as template +source can be found under **Admin > Settings > Custom project templates**. + +Within this section, you can configure the group where all the custom project +templates are sourced. Every project directly under the group namespace will be +available to the user if they have access to them. For example, every public +project in the group will be available to every logged user. However, +private projects will be available only if the user has view [permissions](../permissions.md) +in the project: + +- Project Owner, Maintainer, Developer, Reporter or Guest +- Is a member of the Group: Owner, Maintainer, Developer, Reporter or Guest + +Projects below subgroups of the template group are **not** supported. + +Repository and database information that are copied over to each new project are +identical to the data exported with [GitLab's Project Import/Export](../project/settings/import_export.md). + +If you would like to set project templates at a group level, please see [Custom group-level project templates](../group/custom_project_templates.md).
\ No newline at end of file diff --git a/doc/user/group/custom_project_templates.md b/doc/user/group/custom_project_templates.md new file mode 100644 index 00000000000..eaf0273050b --- /dev/null +++ b/doc/user/group/custom_project_templates.md @@ -0,0 +1,23 @@ +# Custom group-level project templates **[PREMIUM ONLY]** + +> [Introduced](https://gitlab.com/gitlab-org/gitlab-ee/issues/6861) in [GitLab Premium](https://about.gitlab.com/pricing) 11.6. + +When you create a new project, creating it based on custom project templates is +a convenient option to bootstrap from an existing project boilerplate. +The group-level setting to configure a GitLab group that serves as template +source can be found under **Group > Settings > General > Custom project templates**. + +Within this section, you can configure the group where all the custom project +templates are sourced. Every project directly under the group namespace will be +available to the user if they have access to them. For example, every public +project in the group will be available to every logged in user. However, +private projects will be available only if the user has view [permissions](../permissions.md) +in the project. That is, users with Owner, Maintainer, Developer, Reporter or Guest roles for projects, +or for groups to which the project belongs. + +Projects of nested subgroups of a selected template source cannot be used. + +Repository and database information that are copied over to each new project are +identical to the data exported with [GitLab's Project Import/Export](../project/settings/import_export.md). + +If you would like to set project templates at an instance level, please see [Custom instance-level project templates](../admin_area/custom_project_templates.md).
\ No newline at end of file diff --git a/doc/user/group/index.md b/doc/user/group/index.md index d673fa4d21a..36b9318c0e0 100644 --- a/doc/user/group/index.md +++ b/doc/user/group/index.md @@ -259,6 +259,11 @@ types with every project in a group. Learn more about [Group-level file templates](https://docs.gitlab.com/ee/user/group/index.html#group-level-file-templates-premium). +#### Group-level project templates **[PREMIUM]** + +Define project templates at a group-level by setting a group as a template source. +[Learn more about group-level project templates](custom_project_templates.md). + ### Advanced settings - **Projects**: view all projects within that group, add members to each project, diff --git a/doc/user/markdown.md b/doc/user/markdown.md index 6c6119a2691..debebd4c081 100644 --- a/doc/user/markdown.md +++ b/doc/user/markdown.md @@ -1022,7 +1022,7 @@ A link starting with a `/` is relative to the wiki root. [rouge]: http://rouge.jneen.net/ "Rouge website" [redcarpet]: https://github.com/vmg/redcarpet "Redcarpet website" [katex]: https://github.com/Khan/KaTeX "KaTeX website" -[katex-subset]: https://github.com/Khan/KaTeX/wiki/Function-Support-in-KaTeX "Macros supported by KaTeX" +[katex-subset]: https://katex.org/docs/supported.html "Macros supported by KaTeX" [asciidoctor-manual]: http://asciidoctor.org/docs/user-manual/#activating-stem-support "Asciidoctor user manual" [commonmarker]: https://github.com/gjtorikian/commonmarker [commonmark-spec]: https://spec.commonmark.org/current/ diff --git a/doc/user/project/clusters/index.md b/doc/user/project/clusters/index.md index 2aa7c7ef815..79b36e5263e 100644 --- a/doc/user/project/clusters/index.md +++ b/doc/user/project/clusters/index.md @@ -92,13 +92,47 @@ To add an existing Kubernetes cluster to your project: the `ca.crt` contents here. - **Token** - GitLab authenticates against Kubernetes using service tokens, which are - scoped to a particular `namespace`. If you don't have a service token yet, - you can follow the - [Kubernetes documentation](https://kubernetes.io/docs/tasks/configure-pod-container/configure-service-account/) - to create one. You can also view or create service tokens in the - [Kubernetes dashboard](https://kubernetes.io/docs/tasks/access-application-cluster/web-ui-dashboard/) - (under **Config > Secrets**). **The account that will issue the service token - must have admin privileges on the cluster.** + scoped to a particular `namespace`. + **The token used should belong to a service account with + [`cluster-admin`](https://kubernetes.io/docs/reference/access-authn-authz/rbac/#user-facing-roles) + privileges.** To create this service account: + + 1. Create a `gitlab` service account in the `default` namespace: + + ```bash + kubectl create -f - <<EOF + apiVersion: v1 + kind: ServiceAccount + metadata: + name: gitlab + namespace: default + EOF + ``` + 1. Create a cluster role binding to give the `gitlab` service account + `cluster-admin` privileges: + + ```bash + kubectl create -f - <<EOF + kind: ClusterRoleBinding + apiVersion: rbac.authorization.k8s.io/v1 + metadata: + name: gitlab-cluster-admin + subjects: + - kind: ServiceAccount + name: gitlab + namespace: default + roleRef: + kind: ClusterRole + name: cluster-admin + apiGroup: rbac.authorization.k8s.io + EOF + ``` + NOTE: **Note:** + For GKE clusters, you will need the + `container.clusterRoleBindings.create` permission to create a cluster + role binding. You can follow the [Google Cloud + documentation](https://cloud.google.com/iam/docs/granting-changing-revoking-access) + to grant access. - **Project namespace** (optional) - You don't have to fill it in; by leaving it blank, GitLab will create one for you. Also: - Each project should have a unique namespace. @@ -142,8 +176,9 @@ Whether ABAC or RBAC is enabled, GitLab will create the necessary service accounts and privileges in order to install and run [GitLab managed applications](#installing-applications): -- A `gitlab` service account with `cluster-admin` privileges will be created in the - `default` namespace, which will be used by GitLab to manage the newly created cluster. +- If GitLab is creating the cluster, a `gitlab` service account with + `cluster-admin` privileges will be created in the `default` namespace, + which will be used by GitLab to manage the newly created cluster. - A project service account with [`edit` privileges](https://kubernetes.io/docs/reference/access-authn-authz/rbac/#user-facing-roles) diff --git a/doc/user/project/pipelines/schedules.md b/doc/user/project/pipelines/schedules.md index 9daacc37994..051277dfe02 100644 --- a/doc/user/project/pipelines/schedules.md +++ b/doc/user/project/pipelines/schedules.md @@ -83,12 +83,12 @@ The next time a pipeline is scheduled, your credentials will be used. ![Schedules list](img/pipeline_schedules_ownership.png) -> **Note:** -When the owner of the schedule doesn't have the ability to create pipelines -anymore, due to e.g., being blocked or removed from the project, or lacking -the permission to run on protected branches or tags. When this happened, the -schedule is deactivated. Another user can take ownership and activate it, so -the schedule can be run again. +NOTE: **Note:** +If the owner of a pipeline schedule doesn't have the ability to create pipelines +on the target branch, the schedule will stop creating new pipelines. This can +happen if, for example, the owner is blocked or removed from the project, or +the target branch or tag is protected. In this case, someone with sufficient +privileges must take ownership of the schedule. ## Advanced admin configuration diff --git a/lib/api/access_requests.rb b/lib/api/access_requests.rb index cecff6d3b81..ee8dc822098 100644 --- a/lib/api/access_requests.rb +++ b/lib/api/access_requests.rb @@ -12,7 +12,7 @@ module API params do requires :id, type: String, desc: "The #{source_type} ID" end - resource source_type.pluralize, requirements: API::PROJECT_ENDPOINT_REQUIREMENTS do + resource source_type.pluralize, requirements: API::NAMESPACE_OR_PROJECT_REQUIREMENTS do desc "Gets a list of access requests for a #{source_type}." do detail 'This feature was introduced in GitLab 8.11.' success Entities::AccessRequester diff --git a/lib/api/api.rb b/lib/api/api.rb index 449faf5f8da..a4bf0d77eb1 100644 --- a/lib/api/api.rb +++ b/lib/api/api.rb @@ -7,8 +7,8 @@ module API LOG_FILENAME = Rails.root.join("log", "api_json.log") NO_SLASH_URL_PART_REGEX = %r{[^/]+} - PROJECT_ENDPOINT_REQUIREMENTS = { id: NO_SLASH_URL_PART_REGEX }.freeze - COMMIT_ENDPOINT_REQUIREMENTS = PROJECT_ENDPOINT_REQUIREMENTS.merge(sha: NO_SLASH_URL_PART_REGEX).freeze + NAMESPACE_OR_PROJECT_REQUIREMENTS = { id: NO_SLASH_URL_PART_REGEX }.freeze + COMMIT_ENDPOINT_REQUIREMENTS = NAMESPACE_OR_PROJECT_REQUIREMENTS.merge(sha: NO_SLASH_URL_PART_REGEX).freeze insert_before Grape::Middleware::Error, GrapeLogging::Middleware::RequestLogger, diff --git a/lib/api/award_emoji.rb b/lib/api/award_emoji.rb index c2abf9155f3..a1851ba3627 100644 --- a/lib/api/award_emoji.rb +++ b/lib/api/award_emoji.rb @@ -14,7 +14,7 @@ module API params do requires :id, type: String, desc: 'The ID of a project' end - resource :projects, requirements: API::PROJECT_ENDPOINT_REQUIREMENTS do + resource :projects, requirements: API::NAMESPACE_OR_PROJECT_REQUIREMENTS do AWARDABLES.each do |awardable_params| awardable_string = awardable_params[:type].pluralize awardable_id_string = "#{awardable_params[:type]}_#{awardable_params[:find_by]}" diff --git a/lib/api/badges.rb b/lib/api/badges.rb index ab670988f47..ba554e00a16 100644 --- a/lib/api/badges.rb +++ b/lib/api/badges.rb @@ -22,7 +22,7 @@ module API params do requires :id, type: String, desc: "The ID of a #{source_type}" end - resource source_type.pluralize, requirements: API::PROJECT_ENDPOINT_REQUIREMENTS do + resource source_type.pluralize, requirements: API::NAMESPACE_OR_PROJECT_REQUIREMENTS do desc "Gets a list of #{source_type} badges viewable by the authenticated user." do detail 'This feature was introduced in GitLab 10.6.' success Entities::Badge diff --git a/lib/api/boards.rb b/lib/api/boards.rb index c80e1c57864..b7c77730afb 100644 --- a/lib/api/boards.rb +++ b/lib/api/boards.rb @@ -16,7 +16,7 @@ module API params do requires :id, type: String, desc: 'The ID of a project' end - resource :projects, requirements: API::PROJECT_ENDPOINT_REQUIREMENTS do + resource :projects, requirements: API::NAMESPACE_OR_PROJECT_REQUIREMENTS do segment ':id/boards' do desc 'Get all project boards' do detail 'This feature was introduced in 8.13' diff --git a/lib/api/branches.rb b/lib/api/branches.rb index 2735d410c8e..e7e58ad0e66 100644 --- a/lib/api/branches.rb +++ b/lib/api/branches.rb @@ -6,7 +6,7 @@ module API class Branches < Grape::API include PaginationParams - BRANCH_ENDPOINT_REQUIREMENTS = API::PROJECT_ENDPOINT_REQUIREMENTS.merge(branch: API::NO_SLASH_URL_PART_REGEX) + BRANCH_ENDPOINT_REQUIREMENTS = API::NAMESPACE_OR_PROJECT_REQUIREMENTS.merge(branch: API::NO_SLASH_URL_PART_REGEX) before { authorize! :download_code, user_project } @@ -20,7 +20,7 @@ module API params do requires :id, type: String, desc: 'The ID of a project' end - resource :projects, requirements: API::PROJECT_ENDPOINT_REQUIREMENTS do + resource :projects, requirements: API::NAMESPACE_OR_PROJECT_REQUIREMENTS do desc 'Get a project repository branches' do success Entities::Branch end diff --git a/lib/api/commit_statuses.rb b/lib/api/commit_statuses.rb index 99553d993ca..1ba2c150cb4 100644 --- a/lib/api/commit_statuses.rb +++ b/lib/api/commit_statuses.rb @@ -7,7 +7,7 @@ module API params do requires :id, type: String, desc: 'The ID of a project' end - resource :projects, requirements: API::PROJECT_ENDPOINT_REQUIREMENTS do + resource :projects, requirements: API::NAMESPACE_OR_PROJECT_REQUIREMENTS do include PaginationParams before { authenticate! } diff --git a/lib/api/commits.rb b/lib/api/commits.rb index 337b92a6183..9d23daafe95 100644 --- a/lib/api/commits.rb +++ b/lib/api/commits.rb @@ -23,7 +23,7 @@ module API params do requires :id, type: String, desc: 'The ID of a project' end - resource :projects, requirements: API::PROJECT_ENDPOINT_REQUIREMENTS do + resource :projects, requirements: API::NAMESPACE_OR_PROJECT_REQUIREMENTS do desc 'Get a project repository commits' do success Entities::Commit end diff --git a/lib/api/deploy_keys.rb b/lib/api/deploy_keys.rb index ce35720d408..df6d2721977 100644 --- a/lib/api/deploy_keys.rb +++ b/lib/api/deploy_keys.rb @@ -31,7 +31,7 @@ module API params do requires :id, type: String, desc: 'The ID of the project' end - resource :projects, requirements: API::PROJECT_ENDPOINT_REQUIREMENTS do + resource :projects, requirements: API::NAMESPACE_OR_PROJECT_REQUIREMENTS do before { authorize_admin_project } desc "Get a specific project's deploy keys" do diff --git a/lib/api/deployments.rb b/lib/api/deployments.rb index 6747e2e5005..8706a971a1a 100644 --- a/lib/api/deployments.rb +++ b/lib/api/deployments.rb @@ -10,7 +10,7 @@ module API params do requires :id, type: String, desc: 'The project ID' end - resource :projects, requirements: API::PROJECT_ENDPOINT_REQUIREMENTS do + resource :projects, requirements: API::NAMESPACE_OR_PROJECT_REQUIREMENTS do desc 'Get all deployments of the project' do detail 'This feature was introduced in GitLab 8.11.' success Entities::Deployment diff --git a/lib/api/discussions.rb b/lib/api/discussions.rb index 39c6d28391d..91eb6a23701 100644 --- a/lib/api/discussions.rb +++ b/lib/api/discussions.rb @@ -17,7 +17,7 @@ module API params do requires :id, type: String, desc: "The ID of a #{parent_type}" end - resource parent_type.pluralize.to_sym, requirements: API::PROJECT_ENDPOINT_REQUIREMENTS do + resource parent_type.pluralize.to_sym, requirements: API::NAMESPACE_OR_PROJECT_REQUIREMENTS do desc "Get a list of #{noteable_type.to_s.downcase} discussions" do success Entities::Discussion end diff --git a/lib/api/environments.rb b/lib/api/environments.rb index c64217a6977..633f24d3c9a 100644 --- a/lib/api/environments.rb +++ b/lib/api/environments.rb @@ -11,7 +11,7 @@ module API params do requires :id, type: String, desc: 'The project ID' end - resource :projects, requirements: API::PROJECT_ENDPOINT_REQUIREMENTS do + resource :projects, requirements: API::NAMESPACE_OR_PROJECT_REQUIREMENTS do desc 'Get all environments of the project' do detail 'This feature was introduced in GitLab 8.11.' success Entities::Environment diff --git a/lib/api/events.rb b/lib/api/events.rb index 6e0b508be19..44dae57770d 100644 --- a/lib/api/events.rb +++ b/lib/api/events.rb @@ -97,7 +97,7 @@ module API params do requires :id, type: String, desc: 'The ID of a project' end - resource :projects, requirements: API::PROJECT_ENDPOINT_REQUIREMENTS do + resource :projects, requirements: API::NAMESPACE_OR_PROJECT_REQUIREMENTS do desc "List a Project's visible events" do success Entities::Event end diff --git a/lib/api/files.rb b/lib/api/files.rb index bcd2cd48a45..becf66d1467 100644 --- a/lib/api/files.rb +++ b/lib/api/files.rb @@ -2,7 +2,7 @@ module API class Files < Grape::API - FILE_ENDPOINT_REQUIREMENTS = API::PROJECT_ENDPOINT_REQUIREMENTS.merge(file_path: API::NO_SLASH_URL_PART_REGEX) + FILE_ENDPOINT_REQUIREMENTS = API::NAMESPACE_OR_PROJECT_REQUIREMENTS.merge(file_path: API::NO_SLASH_URL_PART_REGEX) # Prevents returning plain/text responses for files with .txt extension after_validation { content_type "application/json" } diff --git a/lib/api/group_boards.rb b/lib/api/group_boards.rb index dc30e868e2e..9a20ee8c8b9 100644 --- a/lib/api/group_boards.rb +++ b/lib/api/group_boards.rb @@ -19,7 +19,7 @@ module API requires :id, type: String, desc: 'The ID of a group' end - resource :groups, requirements: API::PROJECT_ENDPOINT_REQUIREMENTS do + resource :groups, requirements: API::NAMESPACE_OR_PROJECT_REQUIREMENTS do segment ':id/boards' do desc 'Find a group board' do detail 'This feature was introduced in 10.6' diff --git a/lib/api/group_milestones.rb b/lib/api/group_milestones.rb index b36436dbf43..d4287e4a7c4 100644 --- a/lib/api/group_milestones.rb +++ b/lib/api/group_milestones.rb @@ -12,7 +12,7 @@ module API params do requires :id, type: String, desc: 'The ID of a group' end - resource :groups, requirements: API::PROJECT_ENDPOINT_REQUIREMENTS do + resource :groups, requirements: API::NAMESPACE_OR_PROJECT_REQUIREMENTS do desc 'Get a list of group milestones' do success Entities::Milestone end diff --git a/lib/api/group_variables.rb b/lib/api/group_variables.rb index ae7241e9a30..3f048e0dc56 100644 --- a/lib/api/group_variables.rb +++ b/lib/api/group_variables.rb @@ -11,7 +11,7 @@ module API requires :id, type: String, desc: 'The ID of a group' end - resource :groups, requirements: API::PROJECT_ENDPOINT_REQUIREMENTS do + resource :groups, requirements: API::NAMESPACE_OR_PROJECT_REQUIREMENTS do desc 'Get group-level variables' do success Entities::Variable end diff --git a/lib/api/groups.rb b/lib/api/groups.rb index b3d10721692..626a2008dee 100644 --- a/lib/api/groups.rb +++ b/lib/api/groups.rb @@ -140,7 +140,7 @@ module API params do requires :id, type: String, desc: 'The ID of a group' end - resource :groups, requirements: API::PROJECT_ENDPOINT_REQUIREMENTS do + resource :groups, requirements: API::NAMESPACE_OR_PROJECT_REQUIREMENTS do desc 'Update a group. Available only for users who can administrate groups.' do success Entities::Group end diff --git a/lib/api/issues.rb b/lib/api/issues.rb index 491b5085bb8..dac700482b4 100644 --- a/lib/api/issues.rb +++ b/lib/api/issues.rb @@ -101,7 +101,7 @@ module API params do requires :id, type: String, desc: 'The ID of a group' end - resource :groups, requirements: API::PROJECT_ENDPOINT_REQUIREMENTS do + resource :groups, requirements: API::NAMESPACE_OR_PROJECT_REQUIREMENTS do desc 'Get a list of group issues' do success Entities::IssueBasic end @@ -128,7 +128,7 @@ module API params do requires :id, type: String, desc: 'The ID of a project' end - resource :projects, requirements: API::PROJECT_ENDPOINT_REQUIREMENTS do + resource :projects, requirements: API::NAMESPACE_OR_PROJECT_REQUIREMENTS do include TimeTrackingEndpoints desc 'Get a list of project issues' do diff --git a/lib/api/job_artifacts.rb b/lib/api/job_artifacts.rb index 2229cbcd9d4..7c2d8ff11bf 100644 --- a/lib/api/job_artifacts.rb +++ b/lib/api/job_artifacts.rb @@ -14,7 +14,7 @@ module API params do requires :id, type: String, desc: 'The ID of a project' end - resource :projects, requirements: API::PROJECT_ENDPOINT_REQUIREMENTS do + resource :projects, requirements: API::NAMESPACE_OR_PROJECT_REQUIREMENTS do desc 'Download the artifacts archive from a job' do detail 'This feature was introduced in GitLab 8.10' end diff --git a/lib/api/jobs.rb b/lib/api/jobs.rb index 697555c9605..c3f8a84d742 100644 --- a/lib/api/jobs.rb +++ b/lib/api/jobs.rb @@ -9,7 +9,7 @@ module API params do requires :id, type: String, desc: 'The ID of a project' end - resource :projects, requirements: API::PROJECT_ENDPOINT_REQUIREMENTS do + resource :projects, requirements: API::NAMESPACE_OR_PROJECT_REQUIREMENTS do helpers do params :optional_scope do optional :scope, types: [String, Array[String]], desc: 'The scope of builds to show', diff --git a/lib/api/labels.rb b/lib/api/labels.rb index 28555454307..2e676b0aa6b 100644 --- a/lib/api/labels.rb +++ b/lib/api/labels.rb @@ -9,7 +9,7 @@ module API params do requires :id, type: String, desc: 'The ID of a project' end - resource :projects, requirements: API::PROJECT_ENDPOINT_REQUIREMENTS do + resource :projects, requirements: API::NAMESPACE_OR_PROJECT_REQUIREMENTS do desc 'Get all labels of the project' do success Entities::Label end diff --git a/lib/api/members.rb b/lib/api/members.rb index a8f67be3463..461ffe71a62 100644 --- a/lib/api/members.rb +++ b/lib/api/members.rb @@ -12,7 +12,7 @@ module API params do requires :id, type: String, desc: "The #{source_type} ID" end - resource source_type.pluralize, requirements: API::PROJECT_ENDPOINT_REQUIREMENTS do + resource source_type.pluralize, requirements: API::NAMESPACE_OR_PROJECT_REQUIREMENTS do desc 'Gets a list of group or project members viewable by the authenticated user.' do success Entities::Member end diff --git a/lib/api/merge_request_diffs.rb b/lib/api/merge_request_diffs.rb index e4fb890960a..6ad30aa56e0 100644 --- a/lib/api/merge_request_diffs.rb +++ b/lib/api/merge_request_diffs.rb @@ -10,7 +10,7 @@ module API params do requires :id, type: String, desc: 'The ID of a project' end - resource :projects, requirements: API::PROJECT_ENDPOINT_REQUIREMENTS do + resource :projects, requirements: API::NAMESPACE_OR_PROJECT_REQUIREMENTS do desc 'Get a list of merge request diff versions' do detail 'This feature was introduced in GitLab 8.12.' success Entities::MergeRequestDiff diff --git a/lib/api/merge_requests.rb b/lib/api/merge_requests.rb index 595b3641c52..8c1951cc535 100644 --- a/lib/api/merge_requests.rb +++ b/lib/api/merge_requests.rb @@ -135,7 +135,7 @@ module API params do requires :id, type: String, desc: 'The ID of a group' end - resource :groups, requirements: API::PROJECT_ENDPOINT_REQUIREMENTS do + resource :groups, requirements: API::NAMESPACE_OR_PROJECT_REQUIREMENTS do desc 'Get a list of group merge requests' do success Entities::MergeRequestBasic end @@ -154,7 +154,7 @@ module API params do requires :id, type: String, desc: 'The ID of a project' end - resource :projects, requirements: API::PROJECT_ENDPOINT_REQUIREMENTS do + resource :projects, requirements: API::NAMESPACE_OR_PROJECT_REQUIREMENTS do include TimeTrackingEndpoints helpers do diff --git a/lib/api/namespaces.rb b/lib/api/namespaces.rb index 76639fbb031..06a57e3cd6f 100644 --- a/lib/api/namespaces.rb +++ b/lib/api/namespaces.rb @@ -28,7 +28,7 @@ module API params do requires :id, type: String, desc: "Namespace's ID or path" end - get ':id' do + get ':id', requirements: API::NAMESPACE_OR_PROJECT_REQUIREMENTS do present user_namespace, with: Entities::Namespace, current_user: current_user end end diff --git a/lib/api/notes.rb b/lib/api/notes.rb index 9f323b87baf..1bdf7aeb119 100644 --- a/lib/api/notes.rb +++ b/lib/api/notes.rb @@ -16,7 +16,7 @@ module API params do requires :id, type: String, desc: "The ID of a #{parent_type}" end - resource parent_type.pluralize.to_sym, requirements: API::PROJECT_ENDPOINT_REQUIREMENTS do + resource parent_type.pluralize.to_sym, requirements: API::NAMESPACE_OR_PROJECT_REQUIREMENTS do noteables_str = noteable_type.to_s.underscore.pluralize desc "Get a list of #{noteable_type.to_s.downcase} notes" do diff --git a/lib/api/notification_settings.rb b/lib/api/notification_settings.rb index 4d9a4629268..8cb46bd3ad6 100644 --- a/lib/api/notification_settings.rb +++ b/lib/api/notification_settings.rb @@ -58,7 +58,7 @@ module API params do requires :id, type: String, desc: "The #{source_type} ID" end - resource source_type.pluralize, requirements: API::PROJECT_ENDPOINT_REQUIREMENTS do + resource source_type.pluralize, requirements: API::NAMESPACE_OR_PROJECT_REQUIREMENTS do desc "Get #{source_type} level notification level settings, defaults to Global" do detail 'This feature was introduced in GitLab 8.12' success Entities::NotificationSetting diff --git a/lib/api/pages_domains.rb b/lib/api/pages_domains.rb index c9ad47e0f0d..78442f465bd 100644 --- a/lib/api/pages_domains.rb +++ b/lib/api/pages_domains.rb @@ -4,7 +4,7 @@ module API class PagesDomains < Grape::API include PaginationParams - PAGES_DOMAINS_ENDPOINT_REQUIREMENTS = API::PROJECT_ENDPOINT_REQUIREMENTS.merge(domain: API::NO_SLASH_URL_PART_REGEX) + PAGES_DOMAINS_ENDPOINT_REQUIREMENTS = API::NAMESPACE_OR_PROJECT_REQUIREMENTS.merge(domain: API::NO_SLASH_URL_PART_REGEX) before do authenticate! @@ -54,7 +54,7 @@ module API params do requires :id, type: String, desc: 'The ID of a project' end - resource :projects, requirements: API::PROJECT_ENDPOINT_REQUIREMENTS do + resource :projects, requirements: API::NAMESPACE_OR_PROJECT_REQUIREMENTS do before do require_pages_enabled! end diff --git a/lib/api/pipeline_schedules.rb b/lib/api/pipeline_schedules.rb index ed0a38b9d70..47b711917e2 100644 --- a/lib/api/pipeline_schedules.rb +++ b/lib/api/pipeline_schedules.rb @@ -9,7 +9,7 @@ module API params do requires :id, type: String, desc: 'The ID of a project' end - resource :projects, requirements: API::PROJECT_ENDPOINT_REQUIREMENTS do + resource :projects, requirements: API::NAMESPACE_OR_PROJECT_REQUIREMENTS do desc 'Get all pipeline schedules' do success Entities::PipelineSchedule end diff --git a/lib/api/pipelines.rb b/lib/api/pipelines.rb index cba1e3a6684..62d07d945e2 100644 --- a/lib/api/pipelines.rb +++ b/lib/api/pipelines.rb @@ -9,7 +9,7 @@ module API params do requires :id, type: String, desc: 'The project ID' end - resource :projects, requirements: API::PROJECT_ENDPOINT_REQUIREMENTS do + resource :projects, requirements: API::NAMESPACE_OR_PROJECT_REQUIREMENTS do desc 'Get all Pipelines of the project' do detail 'This feature was introduced in GitLab 8.11.' success Entities::PipelineBasic diff --git a/lib/api/project_hooks.rb b/lib/api/project_hooks.rb index 4af4c6ac593..0e7576c9243 100644 --- a/lib/api/project_hooks.rb +++ b/lib/api/project_hooks.rb @@ -29,7 +29,7 @@ module API params do requires :id, type: String, desc: 'The ID of a project' end - resource :projects, requirements: API::PROJECT_ENDPOINT_REQUIREMENTS do + resource :projects, requirements: API::NAMESPACE_OR_PROJECT_REQUIREMENTS do desc 'Get project hooks' do success Entities::ProjectHook end diff --git a/lib/api/project_import.rb b/lib/api/project_import.rb index cbfa0c5bc1c..c64ec2fcc95 100644 --- a/lib/api/project_import.rb +++ b/lib/api/project_import.rb @@ -23,7 +23,7 @@ module API forbidden! unless Gitlab::CurrentSettings.import_sources.include?('gitlab_project') end - resource :projects, requirements: API::PROJECT_ENDPOINT_REQUIREMENTS do + resource :projects, requirements: API::NAMESPACE_OR_PROJECT_REQUIREMENTS do params do requires :path, type: String, desc: 'The new project path and name' requires :file, type: File, desc: 'The project export file to be imported' diff --git a/lib/api/project_milestones.rb b/lib/api/project_milestones.rb index c7137ba5217..da31bcb8dac 100644 --- a/lib/api/project_milestones.rb +++ b/lib/api/project_milestones.rb @@ -12,7 +12,7 @@ module API params do requires :id, type: String, desc: 'The ID of a project' end - resource :projects, requirements: API::PROJECT_ENDPOINT_REQUIREMENTS do + resource :projects, requirements: API::NAMESPACE_OR_PROJECT_REQUIREMENTS do desc 'Get a list of project milestones' do success Entities::Milestone end diff --git a/lib/api/project_snippets.rb b/lib/api/project_snippets.rb index f3a1b73b153..a607df411a6 100644 --- a/lib/api/project_snippets.rb +++ b/lib/api/project_snippets.rb @@ -9,7 +9,7 @@ module API params do requires :id, type: String, desc: 'The ID of a project' end - resource :projects, requirements: API::PROJECT_ENDPOINT_REQUIREMENTS do + resource :projects, requirements: API::NAMESPACE_OR_PROJECT_REQUIREMENTS do helpers do def handle_project_member_errors(errors) if errors[:project_access].any? diff --git a/lib/api/projects.rb b/lib/api/projects.rb index 0a914f9012e..f5d21d8923f 100644 --- a/lib/api/projects.rb +++ b/lib/api/projects.rb @@ -128,7 +128,7 @@ module API end end - resource :users, requirements: API::PROJECT_ENDPOINT_REQUIREMENTS do + resource :users, requirements: API::NAMESPACE_OR_PROJECT_REQUIREMENTS do desc 'Get a user projects' do success Entities::BasicProjectDetails end @@ -224,7 +224,7 @@ module API params do requires :id, type: String, desc: 'The ID of a project' end - resource :projects, requirements: API::PROJECT_ENDPOINT_REQUIREMENTS do + resource :projects, requirements: API::NAMESPACE_OR_PROJECT_REQUIREMENTS do desc 'Get a single project' do success Entities::ProjectWithAccess end diff --git a/lib/api/protected_branches.rb b/lib/api/protected_branches.rb index 47752f40e58..5af43448727 100644 --- a/lib/api/protected_branches.rb +++ b/lib/api/protected_branches.rb @@ -4,14 +4,14 @@ module API class ProtectedBranches < Grape::API include PaginationParams - BRANCH_ENDPOINT_REQUIREMENTS = API::PROJECT_ENDPOINT_REQUIREMENTS.merge(name: API::NO_SLASH_URL_PART_REGEX) + BRANCH_ENDPOINT_REQUIREMENTS = API::NAMESPACE_OR_PROJECT_REQUIREMENTS.merge(name: API::NO_SLASH_URL_PART_REGEX) before { authorize_admin_project } params do requires :id, type: String, desc: 'The ID of a project' end - resource :projects, requirements: API::PROJECT_ENDPOINT_REQUIREMENTS do + resource :projects, requirements: API::NAMESPACE_OR_PROJECT_REQUIREMENTS do desc "Get a project's protected branches" do success Entities::ProtectedBranch end diff --git a/lib/api/protected_tags.rb b/lib/api/protected_tags.rb index ed1c5f0cc05..ee13473c848 100644 --- a/lib/api/protected_tags.rb +++ b/lib/api/protected_tags.rb @@ -4,14 +4,14 @@ module API class ProtectedTags < Grape::API include PaginationParams - TAG_ENDPOINT_REQUIREMENTS = API::PROJECT_ENDPOINT_REQUIREMENTS.merge(name: API::NO_SLASH_URL_PART_REGEX) + TAG_ENDPOINT_REQUIREMENTS = API::NAMESPACE_OR_PROJECT_REQUIREMENTS.merge(name: API::NO_SLASH_URL_PART_REGEX) before { authorize_admin_project } params do requires :id, type: String, desc: 'The ID of a project' end - resource :projects, requirements: API::PROJECT_ENDPOINT_REQUIREMENTS do + resource :projects, requirements: API::NAMESPACE_OR_PROJECT_REQUIREMENTS do desc "Get a project's protected tags" do detail 'This feature was introduced in GitLab 11.3.' success Entities::ProtectedTag diff --git a/lib/api/repositories.rb b/lib/api/repositories.rb index dc844c0bd27..32e05d84491 100644 --- a/lib/api/repositories.rb +++ b/lib/api/repositories.rb @@ -11,7 +11,7 @@ module API params do requires :id, type: String, desc: 'The ID of a project' end - resource :projects, requirements: API::PROJECT_ENDPOINT_REQUIREMENTS do + resource :projects, requirements: API::NAMESPACE_OR_PROJECT_REQUIREMENTS do helpers do def handle_project_member_errors(errors) if errors[:project_access].any? diff --git a/lib/api/resource_label_events.rb b/lib/api/resource_label_events.rb index b6fbe8c0235..0c328f7268e 100644 --- a/lib/api/resource_label_events.rb +++ b/lib/api/resource_label_events.rb @@ -16,7 +16,7 @@ module API params do requires :id, type: String, desc: "The ID of a #{parent_type}" end - resource parent_type.pluralize.to_sym, requirements: API::PROJECT_ENDPOINT_REQUIREMENTS do + resource parent_type.pluralize.to_sym, requirements: API::NAMESPACE_OR_PROJECT_REQUIREMENTS do desc "Get a list of #{eventable_type.to_s.downcase} resource label events" do success Entities::ResourceLabelEvent detail 'This feature was introduced in 11.3' diff --git a/lib/api/runners.rb b/lib/api/runners.rb index ce70460af11..f72b33605a7 100644 --- a/lib/api/runners.rb +++ b/lib/api/runners.rb @@ -126,7 +126,7 @@ module API params do requires :id, type: String, desc: 'The ID of a project' end - resource :projects, requirements: API::PROJECT_ENDPOINT_REQUIREMENTS do + resource :projects, requirements: API::NAMESPACE_OR_PROJECT_REQUIREMENTS do before { authorize_admin_project } desc 'Get runners available for project' do diff --git a/lib/api/search.rb b/lib/api/search.rb index 12d97dcfe7f..5900e1cccc2 100644 --- a/lib/api/search.rb +++ b/lib/api/search.rb @@ -70,7 +70,7 @@ module API end end - resource :groups, requirements: API::PROJECT_ENDPOINT_REQUIREMENTS do + resource :groups, requirements: API::NAMESPACE_OR_PROJECT_REQUIREMENTS do desc 'Search on GitLab' do detail 'This feature was introduced in GitLab 10.5.' end @@ -89,7 +89,7 @@ module API end end - resource :projects, requirements: API::PROJECT_ENDPOINT_REQUIREMENTS do + resource :projects, requirements: API::NAMESPACE_OR_PROJECT_REQUIREMENTS do desc 'Search on GitLab' do detail 'This feature was introduced in GitLab 10.5.' end diff --git a/lib/api/services.rb b/lib/api/services.rb index 1cb3b8a7277..d60f0f5f08d 100644 --- a/lib/api/services.rb +++ b/lib/api/services.rb @@ -763,7 +763,7 @@ module API params do requires :id, type: String, desc: 'The ID of a project' end - resource :projects, requirements: API::PROJECT_ENDPOINT_REQUIREMENTS do + resource :projects, requirements: API::NAMESPACE_OR_PROJECT_REQUIREMENTS do before { authenticate! } before { authorize_admin_project } @@ -842,7 +842,7 @@ module API params do requires :id, type: String, desc: 'The ID of a project' end - resource :projects, requirements: API::PROJECT_ENDPOINT_REQUIREMENTS do + resource :projects, requirements: API::NAMESPACE_OR_PROJECT_REQUIREMENTS do desc "Trigger a slash command for #{service_slug}" do detail 'Added in GitLab 8.13' end diff --git a/lib/api/subscriptions.rb b/lib/api/subscriptions.rb index 077e9373ac4..74ad3c35a61 100644 --- a/lib/api/subscriptions.rb +++ b/lib/api/subscriptions.rb @@ -14,7 +14,7 @@ module API requires :id, type: String, desc: 'The ID of a project' requires :subscribable_id, type: String, desc: 'The ID of a resource' end - resource :projects, requirements: API::PROJECT_ENDPOINT_REQUIREMENTS do + resource :projects, requirements: API::NAMESPACE_OR_PROJECT_REQUIREMENTS do subscribable_types.each do |type, finder| type_singularized = type.singularize entity_class = Entities.const_get(type_singularized.camelcase) diff --git a/lib/api/tags.rb b/lib/api/tags.rb index f739eacf9ba..b18eec7d796 100644 --- a/lib/api/tags.rb +++ b/lib/api/tags.rb @@ -4,14 +4,14 @@ module API class Tags < Grape::API include PaginationParams - TAG_ENDPOINT_REQUIREMENTS = API::PROJECT_ENDPOINT_REQUIREMENTS.merge(tag_name: API::NO_SLASH_URL_PART_REGEX) + TAG_ENDPOINT_REQUIREMENTS = API::NAMESPACE_OR_PROJECT_REQUIREMENTS.merge(tag_name: API::NO_SLASH_URL_PART_REGEX) before { authorize! :download_code, user_project } params do requires :id, type: String, desc: 'The ID of a project' end - resource :projects, requirements: API::PROJECT_ENDPOINT_REQUIREMENTS do + resource :projects, requirements: API::NAMESPACE_OR_PROJECT_REQUIREMENTS do desc 'Get a project repository tags' do success Entities::Tag end diff --git a/lib/api/todos.rb b/lib/api/todos.rb index ed2cf2cc31b..d2c8cf7c1aa 100644 --- a/lib/api/todos.rb +++ b/lib/api/todos.rb @@ -14,7 +14,7 @@ module API params do requires :id, type: String, desc: 'The ID of a project' end - resource :projects, requirements: API::PROJECT_ENDPOINT_REQUIREMENTS do + resource :projects, requirements: API::NAMESPACE_OR_PROJECT_REQUIREMENTS do ISSUABLE_TYPES.each do |type, finder| type_id_str = "#{type.singularize}_iid".to_sym diff --git a/lib/api/triggers.rb b/lib/api/triggers.rb index f784c857883..3ce1529f259 100644 --- a/lib/api/triggers.rb +++ b/lib/api/triggers.rb @@ -7,7 +7,7 @@ module API params do requires :id, type: String, desc: 'The ID of a project' end - resource :projects, requirements: API::PROJECT_ENDPOINT_REQUIREMENTS do + resource :projects, requirements: API::NAMESPACE_OR_PROJECT_REQUIREMENTS do desc 'Trigger a GitLab project pipeline' do success Entities::Pipeline end diff --git a/lib/api/variables.rb b/lib/api/variables.rb index c844ba321ed..f7cae2251c2 100644 --- a/lib/api/variables.rb +++ b/lib/api/variables.rb @@ -11,7 +11,7 @@ module API requires :id, type: String, desc: 'The ID of a project' end - resource :projects, requirements: API::PROJECT_ENDPOINT_REQUIREMENTS do + resource :projects, requirements: API::NAMESPACE_OR_PROJECT_REQUIREMENTS do desc 'Get project variables' do success Entities::Variable end diff --git a/lib/api/wikis.rb b/lib/api/wikis.rb index 24746f4efc6..302b2797a34 100644 --- a/lib/api/wikis.rb +++ b/lib/api/wikis.rb @@ -22,7 +22,7 @@ module API end end - resource :projects, requirements: API::PROJECT_ENDPOINT_REQUIREMENTS do + resource :projects, requirements: API::NAMESPACE_OR_PROJECT_REQUIREMENTS do desc 'Get a list of wiki pages' do success Entities::WikiPageBasic end @@ -103,7 +103,7 @@ module API requires :file, type: ::API::Validations::Types::SafeFile, desc: 'The attachment file to be uploaded' optional :branch, type: String, desc: 'The name of the branch' end - post ":id/wikis/attachments", requirements: API::PROJECT_ENDPOINT_REQUIREMENTS do + post ":id/wikis/attachments", requirements: API::NAMESPACE_OR_PROJECT_REQUIREMENTS do authorize! :create_wiki, user_project result = ::Wikis::CreateAttachmentService.new(user_project, diff --git a/lib/gitlab/ci/config.rb b/lib/gitlab/ci/config.rb index 2fb3c4582e7..6333799a491 100644 --- a/lib/gitlab/ci/config.rb +++ b/lib/gitlab/ci/config.rb @@ -15,7 +15,7 @@ module Gitlab @global = Entry::Global.new(@config) @global.compose! - rescue Loader::FormatError, + rescue Gitlab::Config::Loader::FormatError, Extendable::ExtensionError, External::Processor::IncludeError => e raise Config::ConfigError, e.message @@ -71,7 +71,7 @@ module Gitlab private def build_config(config, opts = {}) - initial_config = Loader.new(config).load! + initial_config = Gitlab::Config::Loader::Yaml.new(config).load! project = opts.fetch(:project, nil) if project diff --git a/lib/gitlab/ci/config/entry/artifacts.rb b/lib/gitlab/ci/config/entry/artifacts.rb index ef5f25b42c0..41613369ca2 100644 --- a/lib/gitlab/ci/config/entry/artifacts.rb +++ b/lib/gitlab/ci/config/entry/artifacts.rb @@ -7,10 +7,10 @@ module Gitlab ## # Entry that represents a configuration of job artifacts. # - class Artifacts < Node - include Configurable - include Validatable - include Attributable + class Artifacts < ::Gitlab::Config::Entry::Node + include ::Gitlab::Config::Entry::Configurable + include ::Gitlab::Config::Entry::Validatable + include ::Gitlab::Config::Entry::Attributable ALLOWED_KEYS = %i[name untracked paths reports when expire_in].freeze diff --git a/lib/gitlab/ci/config/entry/attributable.rb b/lib/gitlab/ci/config/entry/attributable.rb deleted file mode 100644 index 3c2e1df9b83..00000000000 --- a/lib/gitlab/ci/config/entry/attributable.rb +++ /dev/null @@ -1,29 +0,0 @@ -# frozen_string_literal: true - -module Gitlab - module Ci - class Config - module Entry - module Attributable - extend ActiveSupport::Concern - - class_methods do - def attributes(*attributes) - attributes.flatten.each do |attribute| - if method_defined?(attribute) - raise ArgumentError, 'Method already defined!' - end - - define_method(attribute) do - return unless config.is_a?(Hash) - - config[attribute] - end - end - end - end - end - end - end - end -end diff --git a/lib/gitlab/ci/config/entry/boolean.rb b/lib/gitlab/ci/config/entry/boolean.rb deleted file mode 100644 index b9639c83075..00000000000 --- a/lib/gitlab/ci/config/entry/boolean.rb +++ /dev/null @@ -1,20 +0,0 @@ -# frozen_string_literal: true - -module Gitlab - module Ci - class Config - module Entry - ## - # Entry that represents a boolean value. - # - class Boolean < Node - include Validatable - - validations do - validates :config, boolean: true - end - end - end - end - end -end diff --git a/lib/gitlab/ci/config/entry/cache.rb b/lib/gitlab/ci/config/entry/cache.rb index 0a25057f482..7b94af24c09 100644 --- a/lib/gitlab/ci/config/entry/cache.rb +++ b/lib/gitlab/ci/config/entry/cache.rb @@ -7,9 +7,9 @@ module Gitlab ## # Entry that represents a cache configuration # - class Cache < Node - include Configurable - include Attributable + class Cache < ::Gitlab::Config::Entry::Node + include ::Gitlab::Config::Entry::Configurable + include ::Gitlab::Config::Entry::Attributable ALLOWED_KEYS = %i[key untracked paths policy].freeze DEFAULT_POLICY = 'pull-push'.freeze @@ -22,7 +22,7 @@ module Gitlab entry :key, Entry::Key, description: 'Cache key used to define a cache affinity.' - entry :untracked, Entry::Boolean, + entry :untracked, ::Gitlab::Config::Entry::Boolean, description: 'Cache all untracked files.' entry :paths, Entry::Paths, diff --git a/lib/gitlab/ci/config/entry/commands.rb b/lib/gitlab/ci/config/entry/commands.rb index d9658291ebe..02e368c1813 100644 --- a/lib/gitlab/ci/config/entry/commands.rb +++ b/lib/gitlab/ci/config/entry/commands.rb @@ -7,8 +7,8 @@ module Gitlab ## # Entry that represents a job script. # - class Commands < Node - include Validatable + class Commands < ::Gitlab::Config::Entry::Node + include ::Gitlab::Config::Entry::Validatable validations do validates :config, array_of_strings_or_string: true diff --git a/lib/gitlab/ci/config/entry/configurable.rb b/lib/gitlab/ci/config/entry/configurable.rb deleted file mode 100644 index 4aabf0cfa31..00000000000 --- a/lib/gitlab/ci/config/entry/configurable.rb +++ /dev/null @@ -1,83 +0,0 @@ -# frozen_string_literal: true - -module Gitlab - module Ci - class Config - module Entry - ## - # This mixin is responsible for adding DSL, which purpose is to - # simplifly process of adding child nodes. - # - # This can be used only if parent node is a configuration entry that - # holds a hash as a configuration value, for example: - # - # job: - # script: ... - # artifacts: ... - # - module Configurable - extend ActiveSupport::Concern - - included do - include Validatable - - validations do - validates :config, type: Hash - end - end - - # rubocop: disable CodeReuse/ActiveRecord - def compose!(deps = nil) - return unless valid? - - self.class.nodes.each do |key, factory| - factory - .value(config[key]) - .with(key: key, parent: self) - - entries[key] = factory.create! - end - - yield if block_given? - - entries.each_value do |entry| - entry.compose!(deps) - end - end - # rubocop: enable CodeReuse/ActiveRecord - - class_methods do - def nodes - Hash[(@nodes || {}).map { |key, factory| [key, factory.dup] }] - end - - private - - # rubocop: disable CodeReuse/ActiveRecord - def entry(key, entry, metadata) - factory = Entry::Factory.new(entry) - .with(description: metadata[:description]) - - (@nodes ||= {}).merge!(key.to_sym => factory) - end - # rubocop: enable CodeReuse/ActiveRecord - - def helpers(*nodes) - nodes.each do |symbol| - define_method("#{symbol}_defined?") do - entries[symbol]&.specified? - end - - define_method("#{symbol}_value") do - return unless entries[symbol] && entries[symbol].valid? - - entries[symbol].value - end - end - end - end - end - end - end - end -end diff --git a/lib/gitlab/ci/config/entry/coverage.rb b/lib/gitlab/ci/config/entry/coverage.rb index 690409ccf77..89545158bed 100644 --- a/lib/gitlab/ci/config/entry/coverage.rb +++ b/lib/gitlab/ci/config/entry/coverage.rb @@ -7,8 +7,8 @@ module Gitlab ## # Entry that represents Coverage settings. # - class Coverage < Node - include Validatable + class Coverage < ::Gitlab::Config::Entry::Node + include ::Gitlab::Config::Entry::Validatable validations do validates :config, regexp: true diff --git a/lib/gitlab/ci/config/entry/environment.rb b/lib/gitlab/ci/config/entry/environment.rb index 07e9e1d3f67..69a3a1aedef 100644 --- a/lib/gitlab/ci/config/entry/environment.rb +++ b/lib/gitlab/ci/config/entry/environment.rb @@ -7,8 +7,8 @@ module Gitlab ## # Entry that represents an environment. # - class Environment < Node - include Validatable + class Environment < ::Gitlab::Config::Entry::Node + include ::Gitlab::Config::Entry::Validatable ALLOWED_KEYS = %i[name url action on_stop].freeze diff --git a/lib/gitlab/ci/config/entry/factory.rb b/lib/gitlab/ci/config/entry/factory.rb deleted file mode 100644 index 85c9c3511a4..00000000000 --- a/lib/gitlab/ci/config/entry/factory.rb +++ /dev/null @@ -1,75 +0,0 @@ -# frozen_string_literal: true - -module Gitlab - module Ci - class Config - module Entry - ## - # Factory class responsible for fabricating entry objects. - # - class Factory - InvalidFactory = Class.new(StandardError) - - def initialize(entry) - @entry = entry - @metadata = {} - @attributes = {} - end - - def value(value) - @value = value - self - end - - def metadata(metadata) - @metadata.merge!(metadata) - self - end - - def with(attributes) - @attributes.merge!(attributes) - self - end - - def create! - raise InvalidFactory unless defined?(@value) - - ## - # We assume that unspecified entry is undefined. - # See issue #18775. - # - if @value.nil? - Entry::Unspecified.new( - fabricate_unspecified - ) - else - fabricate(@entry, @value) - end - end - - private - - def fabricate_unspecified - ## - # If entry has a default value we fabricate concrete node - # with default value. - # - if @entry.default.nil? - fabricate(Entry::Undefined) - else - fabricate(@entry, @entry.default) - end - end - - def fabricate(entry, value = nil) - entry.new(value, @metadata).tap do |node| - node.key = @attributes[:key] - node.parent = @attributes[:parent] - node.description = @attributes[:description] - end - end - end - end - end - end -end diff --git a/lib/gitlab/ci/config/entry/global.rb b/lib/gitlab/ci/config/entry/global.rb index eba203d9d06..09ecb5fdb99 100644 --- a/lib/gitlab/ci/config/entry/global.rb +++ b/lib/gitlab/ci/config/entry/global.rb @@ -8,8 +8,8 @@ module Gitlab # This class represents a global entry - root Entry for entire # GitLab CI Configuration file. # - class Global < Node - include Configurable + class Global < ::Gitlab::Config::Entry::Node + include ::Gitlab::Config::Entry::Configurable entry :before_script, Entry::Script, description: 'Script that will be executed before each job.' @@ -49,7 +49,7 @@ module Gitlab # rubocop: disable CodeReuse/ActiveRecord def compose_jobs! - factory = Entry::Factory.new(Entry::Jobs) + factory = ::Gitlab::Config::Entry::Factory.new(Entry::Jobs) .value(@config.except(*self.class.nodes.keys)) .with(key: :jobs, parent: self, description: 'Jobs definition for this pipeline') diff --git a/lib/gitlab/ci/config/entry/hidden.rb b/lib/gitlab/ci/config/entry/hidden.rb index dc0ede2a25f..76e5d05639f 100644 --- a/lib/gitlab/ci/config/entry/hidden.rb +++ b/lib/gitlab/ci/config/entry/hidden.rb @@ -7,8 +7,8 @@ module Gitlab ## # Entry that represents a hidden CI/CD key. # - class Hidden < Node - include Validatable + class Hidden < ::Gitlab::Config::Entry::Node + include ::Gitlab::Config::Entry::Validatable validations do validates :config, presence: true diff --git a/lib/gitlab/ci/config/entry/image.rb b/lib/gitlab/ci/config/entry/image.rb index fc453b72fa5..a13a0625e90 100644 --- a/lib/gitlab/ci/config/entry/image.rb +++ b/lib/gitlab/ci/config/entry/image.rb @@ -7,8 +7,8 @@ module Gitlab ## # Entry that represents a Docker image. # - class Image < Node - include Validatable + class Image < ::Gitlab::Config::Entry::Node + include ::Gitlab::Config::Entry::Validatable ALLOWED_KEYS = %i[name entrypoint].freeze diff --git a/lib/gitlab/ci/config/entry/job.rb b/lib/gitlab/ci/config/entry/job.rb index c8cb3248fa7..4f2ac94b6c3 100644 --- a/lib/gitlab/ci/config/entry/job.rb +++ b/lib/gitlab/ci/config/entry/job.rb @@ -7,9 +7,9 @@ module Gitlab ## # Entry that represents a concrete CI/CD job. # - class Job < Node - include Configurable - include Attributable + class Job < ::Gitlab::Config::Entry::Node + include ::Gitlab::Config::Entry::Configurable + include ::Gitlab::Config::Entry::Attributable ALLOWED_KEYS = %i[tags script only except type image services allow_failure type stage when start_in artifacts cache diff --git a/lib/gitlab/ci/config/entry/jobs.rb b/lib/gitlab/ci/config/entry/jobs.rb index 1535b108000..82b72e40404 100644 --- a/lib/gitlab/ci/config/entry/jobs.rb +++ b/lib/gitlab/ci/config/entry/jobs.rb @@ -7,8 +7,8 @@ module Gitlab ## # Entry that represents a set of jobs. # - class Jobs < Node - include Validatable + class Jobs < ::Gitlab::Config::Entry::Node + include ::Gitlab::Config::Entry::Validatable validations do validates :config, type: Hash @@ -34,7 +34,7 @@ module Gitlab @config.each do |name, config| node = hidden?(name) ? Entry::Hidden : Entry::Job - factory = Entry::Factory.new(node) + factory = ::Gitlab::Config::Entry::Factory.new(node) .value(config || {}) .metadata(name: name) .with(key: name, parent: self, diff --git a/lib/gitlab/ci/config/entry/key.rb b/lib/gitlab/ci/config/entry/key.rb index 963b200c7bb..0c10967e629 100644 --- a/lib/gitlab/ci/config/entry/key.rb +++ b/lib/gitlab/ci/config/entry/key.rb @@ -7,8 +7,8 @@ module Gitlab ## # Entry that represents a key. # - class Key < Node - include Validatable + class Key < ::Gitlab::Config::Entry::Node + include ::Gitlab::Config::Entry::Validatable validations do validates :config, key: true diff --git a/lib/gitlab/ci/config/entry/legacy_validation_helpers.rb b/lib/gitlab/ci/config/entry/legacy_validation_helpers.rb deleted file mode 100644 index 4043629dea9..00000000000 --- a/lib/gitlab/ci/config/entry/legacy_validation_helpers.rb +++ /dev/null @@ -1,72 +0,0 @@ -# frozen_string_literal: true - -module Gitlab - module Ci - class Config - module Entry - module LegacyValidationHelpers - private - - def validate_duration(value) - value.is_a?(String) && ChronicDuration.parse(value) - rescue ChronicDuration::DurationParseError - false - end - - def validate_duration_limit(value, limit) - return false unless value.is_a?(String) - - ChronicDuration.parse(value).second.from_now < - ChronicDuration.parse(limit).second.from_now - rescue ChronicDuration::DurationParseError - false - end - - def validate_array_of_strings(values) - values.is_a?(Array) && values.all? { |value| validate_string(value) } - end - - def validate_array_of_strings_or_regexps(values) - values.is_a?(Array) && values.all? { |value| validate_string_or_regexp(value) } - end - - def validate_variables(variables) - variables.is_a?(Hash) && - variables.flatten.all? do |value| - validate_string(value) || validate_integer(value) - end - end - - def validate_integer(value) - value.is_a?(Integer) - end - - def validate_string(value) - value.is_a?(String) || value.is_a?(Symbol) - end - - def validate_regexp(value) - !value.nil? && Regexp.new(value.to_s) && true - rescue RegexpError, TypeError - false - end - - def validate_string_or_regexp(value) - return true if value.is_a?(Symbol) - return false unless value.is_a?(String) - - if value.first == '/' && value.last == '/' - validate_regexp(value[1...-1]) - else - true - end - end - - def validate_boolean(value) - value.in?([true, false]) - end - end - end - end - end -end diff --git a/lib/gitlab/ci/config/entry/node.rb b/lib/gitlab/ci/config/entry/node.rb deleted file mode 100644 index 347089722e4..00000000000 --- a/lib/gitlab/ci/config/entry/node.rb +++ /dev/null @@ -1,103 +0,0 @@ -# frozen_string_literal: true - -module Gitlab - module Ci - class Config - module Entry - ## - # Base abstract class for each configuration entry node. - # - class Node - InvalidError = Class.new(StandardError) - - attr_reader :config, :metadata - attr_accessor :key, :parent, :description - - def initialize(config, **metadata) - @config = config - @metadata = metadata - @entries = {} - - self.class.aspects.to_a.each do |aspect| - instance_exec(&aspect) - end - end - - def [](key) - @entries[key] || Entry::Undefined.new - end - - def compose!(deps = nil) - return unless valid? - - yield if block_given? - end - - def leaf? - @entries.none? - end - - def descendants - @entries.values - end - - def ancestors - @parent ? @parent.ancestors + [@parent] : [] - end - - def valid? - errors.none? - end - - def errors - [] - end - - def value - if leaf? - @config - else - meaningful = @entries.select do |_key, value| - value.specified? && value.relevant? - end - - Hash[meaningful.map { |key, entry| [key, entry.value] }] - end - end - - def specified? - true - end - - def relevant? - true - end - - def location - name = @key.presence || self.class.name.to_s.demodulize - .underscore.humanize.downcase - - ancestors.map(&:key).append(name).compact.join(':') - end - - def inspect - val = leaf? ? config : descendants - unspecified = specified? ? '' : '(unspecified) ' - "#<#{self.class.name} #{unspecified}{#{key}: #{val.inspect}}>" - end - - def self.default - end - - def self.aspects - @aspects ||= [] - end - - private - - attr_reader :entries - end - end - end - end -end diff --git a/lib/gitlab/ci/config/entry/paths.rb b/lib/gitlab/ci/config/entry/paths.rb index 9580b5e2e7f..d6f287c6552 100644 --- a/lib/gitlab/ci/config/entry/paths.rb +++ b/lib/gitlab/ci/config/entry/paths.rb @@ -7,8 +7,8 @@ module Gitlab ## # Entry that represents an array of paths. # - class Paths < Node - include Validatable + class Paths < ::Gitlab::Config::Entry::Node + include ::Gitlab::Config::Entry::Validatable validations do validates :config, array_of_strings: true diff --git a/lib/gitlab/ci/config/entry/policy.rb b/lib/gitlab/ci/config/entry/policy.rb index 0535d7c1a1a..998da1f6837 100644 --- a/lib/gitlab/ci/config/entry/policy.rb +++ b/lib/gitlab/ci/config/entry/policy.rb @@ -7,12 +7,12 @@ module Gitlab ## # Entry that represents an only/except trigger policy for the job. # - class Policy < Simplifiable + class Policy < ::Gitlab::Config::Entry::Simplifiable strategy :RefsPolicy, if: -> (config) { config.is_a?(Array) } strategy :ComplexPolicy, if: -> (config) { config.is_a?(Hash) } - class RefsPolicy < Entry::Node - include Entry::Validatable + class RefsPolicy < ::Gitlab::Config::Entry::Node + include ::Gitlab::Config::Entry::Validatable validations do validates :config, array_of_strings_or_regexps: true @@ -23,9 +23,9 @@ module Gitlab end end - class ComplexPolicy < Entry::Node - include Entry::Validatable - include Entry::Attributable + class ComplexPolicy < ::Gitlab::Config::Entry::Node + include ::Gitlab::Config::Entry::Validatable + include ::Gitlab::Config::Entry::Attributable ALLOWED_KEYS = %i[refs kubernetes variables changes].freeze attributes :refs, :kubernetes, :variables, :changes @@ -58,7 +58,7 @@ module Gitlab end end - class UnknownStrategy < Entry::Node + class UnknownStrategy < ::Gitlab::Config::Entry::Node def errors ["#{location} has to be either an array of conditions or a hash"] end diff --git a/lib/gitlab/ci/config/entry/reports.rb b/lib/gitlab/ci/config/entry/reports.rb index 3ac2a6fa777..a3f6cc31321 100644 --- a/lib/gitlab/ci/config/entry/reports.rb +++ b/lib/gitlab/ci/config/entry/reports.rb @@ -7,9 +7,9 @@ module Gitlab ## # Entry that represents a configuration of job artifacts. # - class Reports < Node - include Validatable - include Attributable + class Reports < ::Gitlab::Config::Entry::Node + include ::Gitlab::Config::Entry::Validatable + include ::Gitlab::Config::Entry::Attributable ALLOWED_KEYS = %i[junit codequality sast dependency_scanning container_scanning dast performance license_management].freeze diff --git a/lib/gitlab/ci/config/entry/retry.rb b/lib/gitlab/ci/config/entry/retry.rb index ee82ab10f9c..eaf8b38aa3c 100644 --- a/lib/gitlab/ci/config/entry/retry.rb +++ b/lib/gitlab/ci/config/entry/retry.rb @@ -7,12 +7,12 @@ module Gitlab ## # Entry that represents a retry config for a job. # - class Retry < Simplifiable + class Retry < ::Gitlab::Config::Entry::Simplifiable strategy :SimpleRetry, if: -> (config) { config.is_a?(Integer) } strategy :FullRetry, if: -> (config) { config.is_a?(Hash) } - class SimpleRetry < Entry::Node - include Entry::Validatable + class SimpleRetry < ::Gitlab::Config::Entry::Node + include ::Gitlab::Config::Entry::Validatable validations do validates :config, numericality: { only_integer: true, @@ -31,9 +31,9 @@ module Gitlab end end - class FullRetry < Entry::Node - include Entry::Validatable - include Entry::Attributable + class FullRetry < ::Gitlab::Config::Entry::Node + include ::Gitlab::Config::Entry::Validatable + include ::Gitlab::Config::Entry::Attributable ALLOWED_KEYS = %i[max when].freeze attributes :max, :when @@ -73,7 +73,7 @@ module Gitlab end end - class UnknownStrategy < Entry::Node + class UnknownStrategy < ::Gitlab::Config::Entry::Node def errors ["#{location} has to be either an integer or a hash"] end diff --git a/lib/gitlab/ci/config/entry/script.rb b/lib/gitlab/ci/config/entry/script.rb index f7d39e5cf55..9d25a82b521 100644 --- a/lib/gitlab/ci/config/entry/script.rb +++ b/lib/gitlab/ci/config/entry/script.rb @@ -7,8 +7,8 @@ module Gitlab ## # Entry that represents a script. # - class Script < Node - include Validatable + class Script < ::Gitlab::Config::Entry::Node + include ::Gitlab::Config::Entry::Validatable validations do validates :config, array_of_strings: true diff --git a/lib/gitlab/ci/config/entry/service.rb b/lib/gitlab/ci/config/entry/service.rb index 47bf9205147..6df67083310 100644 --- a/lib/gitlab/ci/config/entry/service.rb +++ b/lib/gitlab/ci/config/entry/service.rb @@ -8,7 +8,7 @@ module Gitlab # Entry that represents a configuration of Docker service. # class Service < Image - include Validatable + include ::Gitlab::Config::Entry::Validatable ALLOWED_KEYS = %i[name entrypoint command alias].freeze diff --git a/lib/gitlab/ci/config/entry/services.rb b/lib/gitlab/ci/config/entry/services.rb index bdf7f80f382..71475f69218 100644 --- a/lib/gitlab/ci/config/entry/services.rb +++ b/lib/gitlab/ci/config/entry/services.rb @@ -7,8 +7,8 @@ module Gitlab ## # Entry that represents a configuration of Docker services. # - class Services < Node - include Validatable + class Services < ::Gitlab::Config::Entry::Node + include ::Gitlab::Config::Entry::Validatable validations do validates :config, type: Array @@ -18,7 +18,7 @@ module Gitlab super do @entries = [] @config.each do |config| - @entries << Entry::Factory.new(Entry::Service) + @entries << ::Gitlab::Config::Entry::Factory.new(Entry::Service) .value(config || {}) .create! end diff --git a/lib/gitlab/ci/config/entry/simplifiable.rb b/lib/gitlab/ci/config/entry/simplifiable.rb deleted file mode 100644 index 9961bbfaa40..00000000000 --- a/lib/gitlab/ci/config/entry/simplifiable.rb +++ /dev/null @@ -1,45 +0,0 @@ -# frozen_string_literal: true - -module Gitlab - module Ci - class Config - module Entry - class Simplifiable < SimpleDelegator - EntryStrategy = Struct.new(:name, :condition) - - def initialize(config, **metadata) - unless self.class.const_defined?(:UnknownStrategy) - raise ArgumentError, 'UndefinedStrategy not available!' - end - - strategy = self.class.strategies.find do |variant| - variant.condition.call(config) - end - - entry = self.class.entry_class(strategy) - - super(entry.new(config, metadata)) - end - - def self.strategy(name, **opts) - EntryStrategy.new(name, opts.fetch(:if)).tap do |strategy| - strategies.append(strategy) - end - end - - def self.strategies - @strategies ||= [] - end - - def self.entry_class(strategy) - if strategy.present? - self.const_get(strategy.name) - else - self::UnknownStrategy - end - end - end - end - end - end -end diff --git a/lib/gitlab/ci/config/entry/stage.rb b/lib/gitlab/ci/config/entry/stage.rb index 65ab5953131..d6d576a3139 100644 --- a/lib/gitlab/ci/config/entry/stage.rb +++ b/lib/gitlab/ci/config/entry/stage.rb @@ -7,8 +7,8 @@ module Gitlab ## # Entry that represents a stage for a job. # - class Stage < Node - include Validatable + class Stage < ::Gitlab::Config::Entry::Node + include ::Gitlab::Config::Entry::Validatable validations do validates :config, type: String diff --git a/lib/gitlab/ci/config/entry/stages.rb b/lib/gitlab/ci/config/entry/stages.rb index ab184246d29..2d715cbc6bb 100644 --- a/lib/gitlab/ci/config/entry/stages.rb +++ b/lib/gitlab/ci/config/entry/stages.rb @@ -7,8 +7,8 @@ module Gitlab ## # Entry that represents a configuration for pipeline stages. # - class Stages < Node - include Validatable + class Stages < ::Gitlab::Config::Entry::Node + include ::Gitlab::Config::Entry::Validatable validations do validates :config, array_of_strings: true diff --git a/lib/gitlab/ci/config/entry/undefined.rb b/lib/gitlab/ci/config/entry/undefined.rb deleted file mode 100644 index 77dcfa88170..00000000000 --- a/lib/gitlab/ci/config/entry/undefined.rb +++ /dev/null @@ -1,42 +0,0 @@ -# frozen_string_literal: true - -module Gitlab - module Ci - class Config - module Entry - ## - # This class represents an undefined entry. - # - class Undefined < Node - def initialize(*) - super(nil) - end - - def value - nil - end - - def valid? - true - end - - def errors - [] - end - - def specified? - false - end - - def relevant? - false - end - - def inspect - "#<#{self.class.name}>" - end - end - end - end - end -end diff --git a/lib/gitlab/ci/config/entry/unspecified.rb b/lib/gitlab/ci/config/entry/unspecified.rb deleted file mode 100644 index bab32489d2f..00000000000 --- a/lib/gitlab/ci/config/entry/unspecified.rb +++ /dev/null @@ -1,21 +0,0 @@ -# frozen_string_literal: true - -module Gitlab - module Ci - class Config - module Entry - ## - # This class represents an unspecified entry. - # - # It decorates original entry adding method that indicates it is - # unspecified. - # - class Unspecified < SimpleDelegator - def specified? - false - end - end - end - end - end -end diff --git a/lib/gitlab/ci/config/entry/validatable.rb b/lib/gitlab/ci/config/entry/validatable.rb deleted file mode 100644 index 08a6593c980..00000000000 --- a/lib/gitlab/ci/config/entry/validatable.rb +++ /dev/null @@ -1,40 +0,0 @@ -# frozen_string_literal: true - -module Gitlab - module Ci - class Config - module Entry - module Validatable - extend ActiveSupport::Concern - - def self.included(node) - node.aspects.append -> do - @validator = self.class.validator.new(self) - @validator.validate(:new) - end - end - - def errors - @validator.messages + descendants.flat_map(&:errors) # rubocop:disable Gitlab/ModuleWithInstanceVariables - end - - class_methods do - def validator - @validator ||= Class.new(Entry::Validator).tap do |validator| - if defined?(@validations) - @validations.each { |rules| validator.class_eval(&rules) } - end - end - end - - private - - def validations(&block) - (@validations ||= []).append(block) - end - end - end - end - end - end -end diff --git a/lib/gitlab/ci/config/entry/validator.rb b/lib/gitlab/ci/config/entry/validator.rb deleted file mode 100644 index 33ffdd3a95d..00000000000 --- a/lib/gitlab/ci/config/entry/validator.rb +++ /dev/null @@ -1,28 +0,0 @@ -# frozen_string_literal: true - -module Gitlab - module Ci - class Config - module Entry - class Validator < SimpleDelegator - include ActiveModel::Validations - include Entry::Validators - - def initialize(entry) - super(entry) - end - - def messages - errors.full_messages.map do |error| - "#{location} #{error}".downcase - end - end - - def self.name - 'Validator' - end - end - end - end - end -end diff --git a/lib/gitlab/ci/config/entry/validators.rb b/lib/gitlab/ci/config/entry/validators.rb deleted file mode 100644 index a1d552fb2e5..00000000000 --- a/lib/gitlab/ci/config/entry/validators.rb +++ /dev/null @@ -1,198 +0,0 @@ -# frozen_string_literal: true - -module Gitlab - module Ci - class Config - module Entry - module Validators - class AllowedKeysValidator < ActiveModel::EachValidator - def validate_each(record, attribute, value) - unknown_keys = value.try(:keys).to_a - options[:in] - - if unknown_keys.any? - record.errors.add(attribute, "contains unknown keys: " + - unknown_keys.join(', ')) - end - end - end - - class AllowedValuesValidator < ActiveModel::EachValidator - def validate_each(record, attribute, value) - unless options[:in].include?(value.to_s) - record.errors.add(attribute, "unknown value: #{value}") - end - end - end - - class AllowedArrayValuesValidator < ActiveModel::EachValidator - def validate_each(record, attribute, value) - unkown_values = value - options[:in] - unless unkown_values.empty? - record.errors.add(attribute, "contains unknown values: " + - unkown_values.join(', ')) - end - end - end - - class ArrayOfStringsValidator < ActiveModel::EachValidator - include LegacyValidationHelpers - - def validate_each(record, attribute, value) - unless validate_array_of_strings(value) - record.errors.add(attribute, 'should be an array of strings') - end - end - end - - class BooleanValidator < ActiveModel::EachValidator - include LegacyValidationHelpers - - def validate_each(record, attribute, value) - unless validate_boolean(value) - record.errors.add(attribute, 'should be a boolean value') - end - end - end - - class DurationValidator < ActiveModel::EachValidator - include LegacyValidationHelpers - - def validate_each(record, attribute, value) - unless validate_duration(value) - record.errors.add(attribute, 'should be a duration') - end - - if options[:limit] - unless validate_duration_limit(value, options[:limit]) - record.errors.add(attribute, 'should not exceed the limit') - end - end - end - end - - class HashOrStringValidator < ActiveModel::EachValidator - def validate_each(record, attribute, value) - unless value.is_a?(Hash) || value.is_a?(String) - record.errors.add(attribute, 'should be a hash or a string') - end - end - end - - class HashOrIntegerValidator < ActiveModel::EachValidator - def validate_each(record, attribute, value) - unless value.is_a?(Hash) || value.is_a?(Integer) - record.errors.add(attribute, 'should be a hash or an integer') - end - end - end - - class KeyValidator < ActiveModel::EachValidator - include LegacyValidationHelpers - - def validate_each(record, attribute, value) - if validate_string(value) - validate_path(record, attribute, value) - else - record.errors.add(attribute, 'should be a string or symbol') - end - end - - private - - def validate_path(record, attribute, value) - path = CGI.unescape(value.to_s) - - if path.include?('/') - record.errors.add(attribute, 'cannot contain the "/" character') - elsif path == '.' || path == '..' - record.errors.add(attribute, 'cannot be "." or ".."') - end - end - end - - class RegexpValidator < ActiveModel::EachValidator - include LegacyValidationHelpers - - def validate_each(record, attribute, value) - unless validate_regexp(value) - record.errors.add(attribute, 'must be a regular expression') - end - end - - private - - def look_like_regexp?(value) - value.is_a?(String) && value.start_with?('/') && - value.end_with?('/') - end - - def validate_regexp(value) - look_like_regexp?(value) && - Regexp.new(value.to_s[1...-1]) && - true - rescue RegexpError - false - end - end - - class ArrayOfStringsOrRegexpsValidator < RegexpValidator - def validate_each(record, attribute, value) - unless validate_array_of_strings_or_regexps(value) - record.errors.add(attribute, 'should be an array of strings or regexps') - end - end - - private - - def validate_array_of_strings_or_regexps(values) - values.is_a?(Array) && values.all?(&method(:validate_string_or_regexp)) - end - - def validate_string_or_regexp(value) - return false unless value.is_a?(String) - return validate_regexp(value) if look_like_regexp?(value) - - true - end - end - - class ArrayOfStringsOrStringValidator < RegexpValidator - def validate_each(record, attribute, value) - unless validate_array_of_strings_or_string(value) - record.errors.add(attribute, 'should be an array of strings or a string') - end - end - - private - - def validate_array_of_strings_or_string(values) - validate_array_of_strings(values) || validate_string(values) - end - end - - class TypeValidator < ActiveModel::EachValidator - def validate_each(record, attribute, value) - type = options[:with] - raise unless type.is_a?(Class) - - unless value.is_a?(type) - message = options[:message] || "should be a #{type.name}" - record.errors.add(attribute, message) - end - end - end - - class VariablesValidator < ActiveModel::EachValidator - include LegacyValidationHelpers - - def validate_each(record, attribute, value) - unless validate_variables(value) - record.errors.add(attribute, 'should be a hash of key value pairs') - end - end - end - end - end - end - end -end diff --git a/lib/gitlab/ci/config/entry/variables.rb b/lib/gitlab/ci/config/entry/variables.rb index 6fd3cec2f5f..89d790ebfa6 100644 --- a/lib/gitlab/ci/config/entry/variables.rb +++ b/lib/gitlab/ci/config/entry/variables.rb @@ -7,8 +7,8 @@ module Gitlab ## # Entry that represents environment variables. # - class Variables < Node - include Validatable + class Variables < ::Gitlab::Config::Entry::Node + include ::Gitlab::Config::Entry::Validatable validations do validates :config, variables: true diff --git a/lib/gitlab/ci/config/external/file/base.rb b/lib/gitlab/ci/config/external/file/base.rb index 15ca47ef60e..ee4ea9bbb1d 100644 --- a/lib/gitlab/ci/config/external/file/base.rb +++ b/lib/gitlab/ci/config/external/file/base.rb @@ -37,8 +37,8 @@ module Gitlab end def to_hash - @hash ||= Ci::Config::Loader.new(content).load! - rescue Ci::Config::Loader::FormatError + @hash ||= Gitlab::Config::Loader::Yaml.new(content).load! + rescue Gitlab::Config::Loader::FormatError nil end diff --git a/lib/gitlab/ci/templates/Auto-DevOps.gitlab-ci.yml b/lib/gitlab/ci/templates/Auto-DevOps.gitlab-ci.yml index c90976b2040..49f03d2ae19 100644 --- a/lib/gitlab/ci/templates/Auto-DevOps.gitlab-ci.yml +++ b/lib/gitlab/ci/templates/Auto-DevOps.gitlab-ci.yml @@ -658,6 +658,7 @@ rollout 100%: fi if [[ -n "$DB_INITIALIZE" && -z "$(helm ls -q "^$name$")" ]]; then + echo "Deploying first release with database initialization..." helm upgrade --install \ --wait \ --set service.enabled="$service_enabled" \ @@ -680,6 +681,7 @@ rollout 100%: "$name" \ chart/ + echo "Deploying second release..." helm upgrade --reuse-values \ --wait \ --set application.initializeCommand="" \ @@ -688,6 +690,7 @@ rollout 100%: "$name" \ chart/ else + echo "Deploying new release..." helm upgrade --install \ --wait \ --set service.enabled="$service_enabled" \ diff --git a/lib/gitlab/ci/yaml_processor.rb b/lib/gitlab/ci/yaml_processor.rb index e6ec400e476..172926b8ab0 100644 --- a/lib/gitlab/ci/yaml_processor.rb +++ b/lib/gitlab/ci/yaml_processor.rb @@ -5,7 +5,7 @@ module Gitlab class YamlProcessor ValidationError = Class.new(StandardError) - include Gitlab::Ci::Config::Entry::LegacyValidationHelpers + include Gitlab::Config::Entry::LegacyValidationHelpers attr_reader :cache, :stages, :jobs diff --git a/lib/gitlab/config/entry/attributable.rb b/lib/gitlab/config/entry/attributable.rb new file mode 100644 index 00000000000..560fe63df0e --- /dev/null +++ b/lib/gitlab/config/entry/attributable.rb @@ -0,0 +1,27 @@ +# frozen_string_literal: true + +module Gitlab + module Config + module Entry + module Attributable + extend ActiveSupport::Concern + + class_methods do + def attributes(*attributes) + attributes.flatten.each do |attribute| + if method_defined?(attribute) + raise ArgumentError, 'Method already defined!' + end + + define_method(attribute) do + return unless config.is_a?(Hash) + + config[attribute] + end + end + end + end + end + end + end +end diff --git a/lib/gitlab/config/entry/boolean.rb b/lib/gitlab/config/entry/boolean.rb new file mode 100644 index 00000000000..1e8a57356e3 --- /dev/null +++ b/lib/gitlab/config/entry/boolean.rb @@ -0,0 +1,18 @@ +# frozen_string_literal: true + +module Gitlab + module Config + module Entry + ## + # Entry that represents a boolean value. + # + class Boolean < Node + include Validatable + + validations do + validates :config, boolean: true + end + end + end + end +end diff --git a/lib/gitlab/config/entry/configurable.rb b/lib/gitlab/config/entry/configurable.rb new file mode 100644 index 00000000000..afdb60b2cd5 --- /dev/null +++ b/lib/gitlab/config/entry/configurable.rb @@ -0,0 +1,81 @@ +# frozen_string_literal: true + +module Gitlab + module Config + module Entry + ## + # This mixin is responsible for adding DSL, which purpose is to + # simplifly process of adding child nodes. + # + # This can be used only if parent node is a configuration entry that + # holds a hash as a configuration value, for example: + # + # job: + # script: ... + # artifacts: ... + # + module Configurable + extend ActiveSupport::Concern + + included do + include Validatable + + validations do + validates :config, type: Hash + end + end + + # rubocop: disable CodeReuse/ActiveRecord + def compose!(deps = nil) + return unless valid? + + self.class.nodes.each do |key, factory| + factory + .value(config[key]) + .with(key: key, parent: self) + + entries[key] = factory.create! + end + + yield if block_given? + + entries.each_value do |entry| + entry.compose!(deps) + end + end + # rubocop: enable CodeReuse/ActiveRecord + + class_methods do + def nodes + Hash[(@nodes || {}).map { |key, factory| [key, factory.dup] }] + end + + private + + # rubocop: disable CodeReuse/ActiveRecord + def entry(key, entry, metadata) + factory = ::Gitlab::Config::Entry::Factory.new(entry) + .with(description: metadata[:description]) + + (@nodes ||= {}).merge!(key.to_sym => factory) + end + # rubocop: enable CodeReuse/ActiveRecord + + def helpers(*nodes) + nodes.each do |symbol| + define_method("#{symbol}_defined?") do + entries[symbol]&.specified? + end + + define_method("#{symbol}_value") do + return unless entries[symbol] && entries[symbol].valid? + + entries[symbol].value + end + end + end + end + end + end + end +end diff --git a/lib/gitlab/config/entry/factory.rb b/lib/gitlab/config/entry/factory.rb new file mode 100644 index 00000000000..30d43c9f9a1 --- /dev/null +++ b/lib/gitlab/config/entry/factory.rb @@ -0,0 +1,73 @@ +# frozen_string_literal: true + +module Gitlab + module Config + module Entry + ## + # Factory class responsible for fabricating entry objects. + # + class Factory + InvalidFactory = Class.new(StandardError) + + def initialize(entry) + @entry = entry + @metadata = {} + @attributes = {} + end + + def value(value) + @value = value + self + end + + def metadata(metadata) + @metadata.merge!(metadata) + self + end + + def with(attributes) + @attributes.merge!(attributes) + self + end + + def create! + raise InvalidFactory unless defined?(@value) + + ## + # We assume that unspecified entry is undefined. + # See issue #18775. + # + if @value.nil? + Entry::Unspecified.new( + fabricate_unspecified + ) + else + fabricate(@entry, @value) + end + end + + private + + def fabricate_unspecified + ## + # If entry has a default value we fabricate concrete node + # with default value. + # + if @entry.default.nil? + fabricate(Entry::Undefined) + else + fabricate(@entry, @entry.default) + end + end + + def fabricate(entry, value = nil) + entry.new(value, @metadata).tap do |node| + node.key = @attributes[:key] + node.parent = @attributes[:parent] + node.description = @attributes[:description] + end + end + end + end + end +end diff --git a/lib/gitlab/config/entry/legacy_validation_helpers.rb b/lib/gitlab/config/entry/legacy_validation_helpers.rb new file mode 100644 index 00000000000..d3ab5625743 --- /dev/null +++ b/lib/gitlab/config/entry/legacy_validation_helpers.rb @@ -0,0 +1,70 @@ +# frozen_string_literal: true + +module Gitlab + module Config + module Entry + module LegacyValidationHelpers + private + + def validate_duration(value) + value.is_a?(String) && ChronicDuration.parse(value) + rescue ChronicDuration::DurationParseError + false + end + + def validate_duration_limit(value, limit) + return false unless value.is_a?(String) + + ChronicDuration.parse(value).second.from_now < + ChronicDuration.parse(limit).second.from_now + rescue ChronicDuration::DurationParseError + false + end + + def validate_array_of_strings(values) + values.is_a?(Array) && values.all? { |value| validate_string(value) } + end + + def validate_array_of_strings_or_regexps(values) + values.is_a?(Array) && values.all? { |value| validate_string_or_regexp(value) } + end + + def validate_variables(variables) + variables.is_a?(Hash) && + variables.flatten.all? do |value| + validate_string(value) || validate_integer(value) + end + end + + def validate_integer(value) + value.is_a?(Integer) + end + + def validate_string(value) + value.is_a?(String) || value.is_a?(Symbol) + end + + def validate_regexp(value) + !value.nil? && Regexp.new(value.to_s) && true + rescue RegexpError, TypeError + false + end + + def validate_string_or_regexp(value) + return true if value.is_a?(Symbol) + return false unless value.is_a?(String) + + if value.first == '/' && value.last == '/' + validate_regexp(value[1...-1]) + else + true + end + end + + def validate_boolean(value) + value.in?([true, false]) + end + end + end + end +end diff --git a/lib/gitlab/config/entry/node.rb b/lib/gitlab/config/entry/node.rb new file mode 100644 index 00000000000..30357b2c95b --- /dev/null +++ b/lib/gitlab/config/entry/node.rb @@ -0,0 +1,101 @@ +# frozen_string_literal: true + +module Gitlab + module Config + module Entry + ## + # Base abstract class for each configuration entry node. + # + class Node + InvalidError = Class.new(StandardError) + + attr_reader :config, :metadata + attr_accessor :key, :parent, :description + + def initialize(config, **metadata) + @config = config + @metadata = metadata + @entries = {} + + self.class.aspects.to_a.each do |aspect| + instance_exec(&aspect) + end + end + + def [](key) + @entries[key] || Entry::Undefined.new + end + + def compose!(deps = nil) + return unless valid? + + yield if block_given? + end + + def leaf? + @entries.none? + end + + def descendants + @entries.values + end + + def ancestors + @parent ? @parent.ancestors + [@parent] : [] + end + + def valid? + errors.none? + end + + def errors + [] + end + + def value + if leaf? + @config + else + meaningful = @entries.select do |_key, value| + value.specified? && value.relevant? + end + + Hash[meaningful.map { |key, entry| [key, entry.value] }] + end + end + + def specified? + true + end + + def relevant? + true + end + + def location + name = @key.presence || self.class.name.to_s.demodulize + .underscore.humanize.downcase + + ancestors.map(&:key).append(name).compact.join(':') + end + + def inspect + val = leaf? ? config : descendants + unspecified = specified? ? '' : '(unspecified) ' + "#<#{self.class.name} #{unspecified}{#{key}: #{val.inspect}}>" + end + + def self.default + end + + def self.aspects + @aspects ||= [] + end + + private + + attr_reader :entries + end + end + end +end diff --git a/lib/gitlab/config/entry/simplifiable.rb b/lib/gitlab/config/entry/simplifiable.rb new file mode 100644 index 00000000000..3e148fe2e91 --- /dev/null +++ b/lib/gitlab/config/entry/simplifiable.rb @@ -0,0 +1,43 @@ +# frozen_string_literal: true + +module Gitlab + module Config + module Entry + class Simplifiable < SimpleDelegator + EntryStrategy = Struct.new(:name, :condition) + + def initialize(config, **metadata) + unless self.class.const_defined?(:UnknownStrategy) + raise ArgumentError, 'UndefinedStrategy not available!' + end + + strategy = self.class.strategies.find do |variant| + variant.condition.call(config) + end + + entry = self.class.entry_class(strategy) + + super(entry.new(config, metadata)) + end + + def self.strategy(name, **opts) + EntryStrategy.new(name, opts.fetch(:if)).tap do |strategy| + strategies.append(strategy) + end + end + + def self.strategies + @strategies ||= [] + end + + def self.entry_class(strategy) + if strategy.present? + self.const_get(strategy.name) + else + self::UnknownStrategy + end + end + end + end + end +end diff --git a/lib/gitlab/config/entry/undefined.rb b/lib/gitlab/config/entry/undefined.rb new file mode 100644 index 00000000000..5f708abc80c --- /dev/null +++ b/lib/gitlab/config/entry/undefined.rb @@ -0,0 +1,40 @@ +# frozen_string_literal: true + +module Gitlab + module Config + module Entry + ## + # This class represents an undefined entry. + # + class Undefined < Node + def initialize(*) + super(nil) + end + + def value + nil + end + + def valid? + true + end + + def errors + [] + end + + def specified? + false + end + + def relevant? + false + end + + def inspect + "#<#{self.class.name}>" + end + end + end + end +end diff --git a/lib/gitlab/config/entry/unspecified.rb b/lib/gitlab/config/entry/unspecified.rb new file mode 100644 index 00000000000..c096180d0f8 --- /dev/null +++ b/lib/gitlab/config/entry/unspecified.rb @@ -0,0 +1,19 @@ +# frozen_string_literal: true + +module Gitlab + module Config + module Entry + ## + # This class represents an unspecified entry. + # + # It decorates original entry adding method that indicates it is + # unspecified. + # + class Unspecified < SimpleDelegator + def specified? + false + end + end + end + end +end diff --git a/lib/gitlab/config/entry/validatable.rb b/lib/gitlab/config/entry/validatable.rb new file mode 100644 index 00000000000..1c88c68c11c --- /dev/null +++ b/lib/gitlab/config/entry/validatable.rb @@ -0,0 +1,38 @@ +# frozen_string_literal: true + +module Gitlab + module Config + module Entry + module Validatable + extend ActiveSupport::Concern + + def self.included(node) + node.aspects.append -> do + @validator = self.class.validator.new(self) + @validator.validate(:new) + end + end + + def errors + @validator.messages + descendants.flat_map(&:errors) # rubocop:disable Gitlab/ModuleWithInstanceVariables + end + + class_methods do + def validator + @validator ||= Class.new(Entry::Validator).tap do |validator| + if defined?(@validations) + @validations.each { |rules| validator.class_eval(&rules) } + end + end + end + + private + + def validations(&block) + (@validations ||= []).append(block) + end + end + end + end + end +end diff --git a/lib/gitlab/config/entry/validator.rb b/lib/gitlab/config/entry/validator.rb new file mode 100644 index 00000000000..e5efd4a7b0a --- /dev/null +++ b/lib/gitlab/config/entry/validator.rb @@ -0,0 +1,26 @@ +# frozen_string_literal: true + +module Gitlab + module Config + module Entry + class Validator < SimpleDelegator + include ActiveModel::Validations + include Entry::Validators + + def initialize(entry) + super(entry) + end + + def messages + errors.full_messages.map do |error| + "#{location} #{error}".downcase + end + end + + def self.name + 'Validator' + end + end + end + end +end diff --git a/lib/gitlab/config/entry/validators.rb b/lib/gitlab/config/entry/validators.rb new file mode 100644 index 00000000000..25bfa50f829 --- /dev/null +++ b/lib/gitlab/config/entry/validators.rb @@ -0,0 +1,196 @@ +# frozen_string_literal: true + +module Gitlab + module Config + module Entry + module Validators + class AllowedKeysValidator < ActiveModel::EachValidator + def validate_each(record, attribute, value) + unknown_keys = value.try(:keys).to_a - options[:in] + + if unknown_keys.any? + record.errors.add(attribute, "contains unknown keys: " + + unknown_keys.join(', ')) + end + end + end + + class AllowedValuesValidator < ActiveModel::EachValidator + def validate_each(record, attribute, value) + unless options[:in].include?(value.to_s) + record.errors.add(attribute, "unknown value: #{value}") + end + end + end + + class AllowedArrayValuesValidator < ActiveModel::EachValidator + def validate_each(record, attribute, value) + unkown_values = value - options[:in] + unless unkown_values.empty? + record.errors.add(attribute, "contains unknown values: " + + unkown_values.join(', ')) + end + end + end + + class ArrayOfStringsValidator < ActiveModel::EachValidator + include LegacyValidationHelpers + + def validate_each(record, attribute, value) + unless validate_array_of_strings(value) + record.errors.add(attribute, 'should be an array of strings') + end + end + end + + class BooleanValidator < ActiveModel::EachValidator + include LegacyValidationHelpers + + def validate_each(record, attribute, value) + unless validate_boolean(value) + record.errors.add(attribute, 'should be a boolean value') + end + end + end + + class DurationValidator < ActiveModel::EachValidator + include LegacyValidationHelpers + + def validate_each(record, attribute, value) + unless validate_duration(value) + record.errors.add(attribute, 'should be a duration') + end + + if options[:limit] + unless validate_duration_limit(value, options[:limit]) + record.errors.add(attribute, 'should not exceed the limit') + end + end + end + end + + class HashOrStringValidator < ActiveModel::EachValidator + def validate_each(record, attribute, value) + unless value.is_a?(Hash) || value.is_a?(String) + record.errors.add(attribute, 'should be a hash or a string') + end + end + end + + class HashOrIntegerValidator < ActiveModel::EachValidator + def validate_each(record, attribute, value) + unless value.is_a?(Hash) || value.is_a?(Integer) + record.errors.add(attribute, 'should be a hash or an integer') + end + end + end + + class KeyValidator < ActiveModel::EachValidator + include LegacyValidationHelpers + + def validate_each(record, attribute, value) + if validate_string(value) + validate_path(record, attribute, value) + else + record.errors.add(attribute, 'should be a string or symbol') + end + end + + private + + def validate_path(record, attribute, value) + path = CGI.unescape(value.to_s) + + if path.include?('/') + record.errors.add(attribute, 'cannot contain the "/" character') + elsif path == '.' || path == '..' + record.errors.add(attribute, 'cannot be "." or ".."') + end + end + end + + class RegexpValidator < ActiveModel::EachValidator + include LegacyValidationHelpers + + def validate_each(record, attribute, value) + unless validate_regexp(value) + record.errors.add(attribute, 'must be a regular expression') + end + end + + private + + def look_like_regexp?(value) + value.is_a?(String) && value.start_with?('/') && + value.end_with?('/') + end + + def validate_regexp(value) + look_like_regexp?(value) && + Regexp.new(value.to_s[1...-1]) && + true + rescue RegexpError + false + end + end + + class ArrayOfStringsOrRegexpsValidator < RegexpValidator + def validate_each(record, attribute, value) + unless validate_array_of_strings_or_regexps(value) + record.errors.add(attribute, 'should be an array of strings or regexps') + end + end + + private + + def validate_array_of_strings_or_regexps(values) + values.is_a?(Array) && values.all?(&method(:validate_string_or_regexp)) + end + + def validate_string_or_regexp(value) + return false unless value.is_a?(String) + return validate_regexp(value) if look_like_regexp?(value) + + true + end + end + + class ArrayOfStringsOrStringValidator < RegexpValidator + def validate_each(record, attribute, value) + unless validate_array_of_strings_or_string(value) + record.errors.add(attribute, 'should be an array of strings or a string') + end + end + + private + + def validate_array_of_strings_or_string(values) + validate_array_of_strings(values) || validate_string(values) + end + end + + class TypeValidator < ActiveModel::EachValidator + def validate_each(record, attribute, value) + type = options[:with] + raise unless type.is_a?(Class) + + unless value.is_a?(type) + message = options[:message] || "should be a #{type.name}" + record.errors.add(attribute, message) + end + end + end + + class VariablesValidator < ActiveModel::EachValidator + include LegacyValidationHelpers + + def validate_each(record, attribute, value) + unless validate_variables(value) + record.errors.add(attribute, 'should be a hash of key value pairs') + end + end + end + end + end + end +end diff --git a/lib/gitlab/config/loader/format_error.rb b/lib/gitlab/config/loader/format_error.rb new file mode 100644 index 00000000000..848ff96d201 --- /dev/null +++ b/lib/gitlab/config/loader/format_error.rb @@ -0,0 +1,9 @@ +# frozen_string_literal: true + +module Gitlab + module Config + module Loader + FormatError = Class.new(StandardError) + end + end +end diff --git a/lib/gitlab/ci/config/loader.rb b/lib/gitlab/config/loader/yaml.rb index b4c491e84a6..8159f8b8026 100644 --- a/lib/gitlab/ci/config/loader.rb +++ b/lib/gitlab/config/loader/yaml.rb @@ -1,15 +1,13 @@ # frozen_string_literal: true module Gitlab - module Ci - class Config - class Loader - FormatError = Class.new(StandardError) - + module Config + module Loader + class Yaml def initialize(config) @config = YAML.safe_load(config, [Symbol], [], true) rescue Psych::Exception => e - raise FormatError, e.message + raise Loader::FormatError, e.message end def valid? @@ -18,7 +16,7 @@ module Gitlab def load! unless valid? - raise FormatError, 'Invalid configuration format' + raise Loader::FormatError, 'Invalid configuration format' end @config.deep_symbolize_keys diff --git a/lib/tasks/gettext.rake b/lib/tasks/gettext.rake index a497d26312e..2235a6ba194 100644 --- a/lib/tasks/gettext.rake +++ b/lib/tasks/gettext.rake @@ -82,7 +82,7 @@ namespace :gettext do # `gettext:find` writes touches to temp files to `stderr` which would cause # `static-analysis` to report failures. We can ignore these. - silence_sdterr do + silence_stderr do Rake::Task['gettext:find'].invoke end @@ -119,7 +119,7 @@ namespace :gettext do end end - def silence_sdterr(&block) + def silence_stderr(&block) old_stderr = $stderr.dup $stderr.reopen(File::NULL) $stderr.sync = true diff --git a/locale/gitlab.pot b/locale/gitlab.pot index 5672df5f965..3769dfab143 100644 --- a/locale/gitlab.pot +++ b/locale/gitlab.pot @@ -303,6 +303,9 @@ msgstr "" msgid "Abuse reports" msgstr "" +msgid "Accept invitation" +msgstr "" + msgid "Accept terms" msgstr "" @@ -2262,6 +2265,9 @@ msgstr "" msgid "December" msgstr "" +msgid "Decline" +msgstr "" + msgid "Decline and sign out" msgstr "" @@ -3373,6 +3379,9 @@ msgstr "" msgid "Housekeeping successfully started" msgstr "" +msgid "However, you are already a member of this %{member_source}. Sign in using a different account to accept the invitation." +msgstr "" + msgid "I accept the %{terms_link}" msgstr "" @@ -3574,6 +3583,9 @@ msgstr "" msgid "Introducing Cycle Analytics" msgstr "" +msgid "Invitation" +msgstr "" + msgid "Invite" msgstr "" @@ -4016,6 +4028,18 @@ msgstr "" msgid "MergeRequests|View replaced file @ %{commitId}" msgstr "" +msgid "MergeRequests|started a discussion" +msgstr "" + +msgid "MergeRequests|started a discussion on %{linkStart}an old version of the diff%{linkEnd}" +msgstr "" + +msgid "MergeRequests|started a discussion on %{linkStart}the diff%{linkEnd}" +msgstr "" + +msgid "MergeRequests|started a discussion on commit %{linkStart}%{commitId}%{linkEnd}" +msgstr "" + msgid "MergeRequest| %{paragraphStart}changed the description %{descriptionChangedTimes} times %{timeDifferenceMinutes}%{paragraphEnd}" msgstr "" @@ -4055,6 +4079,9 @@ msgstr "" msgid "Metrics|Learn about environments" msgstr "" +msgid "Metrics|No data to display" +msgstr "" + msgid "Metrics|No deployed environments" msgstr "" @@ -4366,6 +4393,9 @@ msgstr "" msgid "Note that the master branch is automatically protected. %{link_to_protected_branches}" msgstr "" +msgid "Note that this invitation was sent to %{mail_to_invite_email}, but you are signed in as %{link_to_current_user} with email %{mail_to_current_user}." +msgstr "" + msgid "Note: As an administrator you may like to configure %{github_integration_link}, which will allow login via GitHub and allow importing repositories without generating a Personal Access Token." msgstr "" @@ -7633,6 +7663,9 @@ msgstr "" msgid "from" msgstr "" +msgid "group" +msgstr "" + msgid "here" msgstr "" @@ -7879,6 +7912,9 @@ msgstr "" msgid "personal access token" msgstr "" +msgid "project" +msgstr "" + msgid "remaining" msgstr "" diff --git a/package.json b/package.json index e573fb1ecc0..520d7c620c2 100644 --- a/package.json +++ b/package.json @@ -37,7 +37,7 @@ "chart.js": "1.0.2", "classlist-polyfill": "^1.2.0", "clipboard": "^1.7.1", - "codesandbox-api": "^0.0.18", + "codesandbox-api": "^0.0.20", "compression-webpack-plugin": "^2.0.0", "core-js": "^2.4.1", "cropper": "^2.3.0", @@ -86,7 +86,7 @@ "sanitize-html": "^1.16.1", "select2": "3.5.2-browserify", "sha1": "^1.1.1", - "smooshpack": "^0.0.48", + "smooshpack": "^0.0.53", "sortablejs": "^1.7.0", "sql.js": "^0.4.0", "stickyfilljs": "^2.0.5", diff --git a/public/robots.txt b/public/robots.txt index ea931e1a223..7908297564e 100644 --- a/public/robots.txt +++ b/public/robots.txt @@ -70,3 +70,5 @@ Disallow: /*/*/hooks Disallow: /*/*/services Disallow: /*/*/protected_branches Disallow: /*/*/uploads/ +Disallow: /*/-/group_members +Disallow: /*/project_members diff --git a/scripts/trigger-build b/scripts/trigger-build index d2c71f5be3e..14af3281106 100755 --- a/scripts/trigger-build +++ b/scripts/trigger-build @@ -68,6 +68,7 @@ module Trigger def base_variables { + 'GITLAB_REF_SLUG' => ref_slug, 'TRIGGERED_USER' => ENV['TRIGGERED_USER'] || ENV['GITLAB_USER_NAME'], 'TRIGGER_SOURCE' => ENV['CI_JOB_URL'], 'TOP_UPSTREAM_SOURCE_PROJECT' => ENV['CI_PROJECT_PATH'], @@ -76,6 +77,12 @@ module Trigger } end + def ref_slug + return 'master' if ENV['CI_COMMIT_REF_SLUG'] =~ %r{(\Aqa[/-]|-qa\z)} + + ENV['CI_COMMIT_REF_SLUG'] + end + # Read version files from all components def version_file_variables Dir.glob("*_VERSION").each_with_object({}) do |version_file, params| @@ -106,18 +113,11 @@ module Trigger def extra_variables { 'GITLAB_VERSION' => ENV['CI_COMMIT_SHA'], - 'GITLAB_REF_SLUG' => ref_slug, 'ALTERNATIVE_SOURCES' => 'true', 'ee' => Trigger.ee? ? 'true' : 'false', 'QA_BRANCH' => ENV['QA_BRANCH'] || 'master' } end - - def ref_slug - return 'master' if ENV['CI_COMMIT_REF_SLUG'] =~ %r{(\Aqa[/-]|-qa\z)} - - ENV['CI_COMMIT_REF_SLUG'] - end end class CNG < Base diff --git a/spec/features/issues/filtered_search/filter_issues_spec.rb b/spec/features/issues/filtered_search/filter_issues_spec.rb index 35d57b3896d..4d9b8262f21 100644 --- a/spec/features/issues/filtered_search/filter_issues_spec.rb +++ b/spec/features/issues/filtered_search/filter_issues_spec.rb @@ -430,10 +430,10 @@ describe 'Filter issues', :js do expect_issues_list_count(2) - sort_toggle = find('.filtered-search-wrapper .dropdown-toggle') + sort_toggle = find('.filter-dropdown-container .dropdown-menu-toggle') sort_toggle.click - find('.filtered-search-wrapper .dropdown-menu li a', text: 'Created date').click + find('.filter-dropdown-container .dropdown-menu li a', text: 'Created date').click wait_for_requests expect(find('.issues-list .issue:first-of-type .issue-title-text a')).to have_content(new_issue.title) diff --git a/spec/features/issues/user_sorts_issues_spec.rb b/spec/features/issues/user_sorts_issues_spec.rb index 4771d2c6d28..3bc93933183 100644 --- a/spec/features/issues/user_sorts_issues_spec.rb +++ b/spec/features/issues/user_sorts_issues_spec.rb @@ -20,7 +20,7 @@ describe "User sorts issues" do end it 'keeps the sort option' do - find('button.dropdown-toggle').click + find('.filter-dropdown-container button.dropdown-menu-toggle').click page.within('.content ul.dropdown-menu.dropdown-menu-right li') do click_link('Milestone') @@ -40,7 +40,7 @@ describe "User sorts issues" do end it "sorts by popularity" do - find("button.dropdown-toggle").click + find(".filter-dropdown-container button.dropdown-menu-toggle").click page.within(".content ul.dropdown-menu.dropdown-menu-right li") do click_link("Popularity") diff --git a/spec/features/merge_request/user_sees_deployment_widget_spec.rb b/spec/features/merge_request/user_sees_deployment_widget_spec.rb index 3e40179ad9a..fe8e0b07d39 100644 --- a/spec/features/merge_request/user_sees_deployment_widget_spec.rb +++ b/spec/features/merge_request/user_sees_deployment_widget_spec.rb @@ -29,6 +29,22 @@ describe 'Merge request > User sees deployment widget', :js do expect(page).to have_content("Deployed to #{environment.name}") expect(find('.js-deploy-time')['data-original-title']).to eq(deployment.created_at.to_time.in_time_zone.to_s(:medium)) end + + context 'when a user created a new merge request with the same SHA' do + let(:pipeline2) { create(:ci_pipeline_without_jobs, sha: sha, project: project, ref: 'new-patch-1') } + let(:build2) { create(:ci_build, :success, pipeline: pipeline2) } + let(:environment2) { create(:environment, project: project) } + let!(:deployment2) { create(:deployment, environment: environment2, sha: sha, ref: 'new-patch-1', deployable: build2) } + + it 'displays one environment which is related to the pipeline' do + visit project_merge_request_path(project, merge_request) + wait_for_requests + + expect(page).to have_selector('.js-deployment-info', count: 1) + expect(page).to have_content("#{environment.name}") + expect(page).not_to have_content("#{environment2.name}") + end + end end context 'when deployment failed' do diff --git a/spec/features/merge_request/user_sees_discussions_spec.rb b/spec/features/merge_request/user_sees_discussions_spec.rb index 7b8c3bacfe2..4ab9a87ad4b 100644 --- a/spec/features/merge_request/user_sees_discussions_spec.rb +++ b/spec/features/merge_request/user_sees_discussions_spec.rb @@ -53,13 +53,11 @@ describe 'Merge request > User sees discussions', :js do shared_examples 'a functional discussion' do let(:discussion_id) { note.discussion_id(merge_request) } - # TODO: https://gitlab.com/gitlab-org/gitlab-ce/issues/48034 - xit 'is displayed' do + it 'is displayed' do expect(page).to have_css(".discussion[data-discussion-id='#{discussion_id}']") end - # TODO: https://gitlab.com/gitlab-org/gitlab-ce/issues/48034 - xit 'can be replied to' do + it 'can be replied to' do within(".discussion[data-discussion-id='#{discussion_id}']") do click_button 'Reply...' fill_in 'note[note]', with: 'Test!' @@ -74,16 +72,21 @@ describe 'Merge request > User sees discussions', :js do visit project_merge_request_path(project, merge_request) end - context 'a regular commit comment' do - let(:note) { create(:note_on_commit, project: project) } - - it_behaves_like 'a functional discussion' - end + # TODO: https://gitlab.com/gitlab-org/gitlab-ce/issues/48034 + # context 'a regular commit comment' do + # let(:note) { create(:note_on_commit, project: project) } + # + # it_behaves_like 'a functional discussion' + # end context 'a commit diff comment' do let(:note) { create(:diff_note_on_commit, project: project) } it_behaves_like 'a functional discussion' + + it 'displays correct header' do + expect(page).to have_content "started a discussion on commit #{note.commit_id[0...7]}" + end end end end diff --git a/spec/features/merge_request/user_sees_merge_widget_spec.rb b/spec/features/merge_request/user_sees_merge_widget_spec.rb index 582be101399..d8ebd3c92af 100644 --- a/spec/features/merge_request/user_sees_merge_widget_spec.rb +++ b/spec/features/merge_request/user_sees_merge_widget_spec.rb @@ -60,7 +60,7 @@ describe 'Merge request > User sees merge widget', :js do it 'shows environments link' do wait_for_requests - page.within('.js-pre-merge-deploy') do + page.within('.js-pre-deployment') do expect(page).to have_content("Deployed to #{environment.name}") expect(find('.js-deploy-url')[:href]).to include(environment.formatted_external_url) end diff --git a/spec/features/merge_requests/user_sorts_merge_requests_spec.rb b/spec/features/merge_requests/user_sorts_merge_requests_spec.rb index e163868e8e7..61e8f1c4662 100644 --- a/spec/features/merge_requests/user_sorts_merge_requests_spec.rb +++ b/spec/features/merge_requests/user_sorts_merge_requests_spec.rb @@ -19,7 +19,7 @@ describe 'User sorts merge requests' do end it 'keeps the sort option' do - find('button.dropdown-toggle').click + find('.filter-dropdown-container button.dropdown-menu-toggle').click page.within('.content ul.dropdown-menu.dropdown-menu-right li') do click_link('Milestone') @@ -49,7 +49,7 @@ describe 'User sorts merge requests' do it 'separates remember sorting with issues' do create(:issue, project: project) - find('button.dropdown-toggle').click + find('.filter-dropdown-container button.dropdown-menu-toggle').click page.within('.content ul.dropdown-menu.dropdown-menu-right li') do click_link('Milestone') @@ -70,7 +70,7 @@ describe 'User sorts merge requests' do end it 'sorts by popularity' do - find('button.dropdown-toggle').click + find('.filter-dropdown-container button.dropdown-menu-toggle').click page.within('.content ul.dropdown-menu.dropdown-menu-right li') do click_link('Popularity') diff --git a/spec/features/projects/labels/issues_sorted_by_priority_spec.rb b/spec/features/projects/labels/issues_sorted_by_priority_spec.rb index 6178f11ded7..b778c72bc76 100644 --- a/spec/features/projects/labels/issues_sorted_by_priority_spec.rb +++ b/spec/features/projects/labels/issues_sorted_by_priority_spec.rb @@ -32,7 +32,7 @@ describe 'Issue prioritization' do visit project_issues_path(project, sort: 'label_priority') # Ensure we are indicating that issues are sorted by priority - expect(page).to have_selector('.dropdown-toggle', text: 'Label priority') + expect(page).to have_selector('.dropdown-menu-toggle', text: 'Label priority') page.within('.issues-holder') do issue_titles = all('.issues-list .issue-title-text').map(&:text) @@ -70,7 +70,7 @@ describe 'Issue prioritization' do sign_in user visit project_issues_path(project, sort: 'label_priority') - expect(page).to have_selector('.dropdown-toggle', text: 'Label priority') + expect(page).to have_selector('.dropdown-menu-toggle', text: 'Label priority') page.within('.issues-holder') do issue_titles = all('.issues-list .issue-title-text').map(&:text) diff --git a/spec/fixtures/security-reports/feature-branch/gl-dependency-scanning-report.json b/spec/fixtures/security-reports/feature-branch/gl-dependency-scanning-report.json index 4b47e259c0f..314f04107eb 100644 --- a/spec/fixtures/security-reports/feature-branch/gl-dependency-scanning-report.json +++ b/spec/fixtures/security-reports/feature-branch/gl-dependency-scanning-report.json @@ -1,46 +1,154 @@ [ { - "priority": "Unknown", - "file": "pom.xml", - "cve": "CVE-2012-4387", - "url": "http://struts.apache.org/docs/s2-011.html", - "message": "Long parameter name DoS for org.apache.struts/struts2-core", - "tools": [ - "gemnasium" + "category": "dependency_scanning", + "name": "io.netty/netty - CVE-2014-3488", + "message": "DoS by CPU exhaustion when using malicious SSL packets", + "cve": "app/pom.xml:io.netty/netty@3.9.1.Final:CVE-2014-3488", + "severity": "Unknown", + "solution": "Upgrade to the latest version", + "scanner": { + "id": "gemnasium", + "name": "Gemnasium" + }, + "location": { + "file": "app/pom.xml" + }, + "identifiers": [ + { + "type": "gemnasium", + "name": "Gemnasium-d1bf36d9-9f07-46cd-9cfc-8675338ada8f", + "value": "d1bf36d9-9f07-46cd-9cfc-8675338ada8f", + "url": "https://deps.sec.gitlab.com/packages/maven/io.netty/netty/versions/3.9.1.Final/advisories" + }, + { + "type": "cve", + "name": "CVE-2014-3488", + "value": "CVE-2014-3488", + "url": "https://cve.mitre.org/cgi-bin/cvename.cgi?name=CVE-2014-3488" + } + ], + "links": [ + { + "url": "https://bugzilla.redhat.com/CVE-2014-3488" + }, + { + "url": "http://netty.io/news/2014/06/11/3.html" + }, + { + "url": "https://github.com/netty/netty/issues/2562" + } ], + "priority": "Unknown", + "file": "app/pom.xml", + "url": "https://bugzilla.redhat.com/CVE-2014-3488", "tool": "gemnasium" }, { - "priority": "Unknown", - "file": "pom.xml", - "cve": "CVE-2013-1966", - "url": "http://struts.apache.org/docs/s2-014.html", - "message": "Remote command execution due to flaw in the includeParams attribute of URL and Anchor tags for org.apache.struts/struts2-core", - "tools": [ - "gemnasium" + "category": "dependency_scanning", + "name": "Django - CVE-2017-12794", + "message": "Possible XSS in traceback section of technical 500 debug page", + "cve": "app/requirements.txt:Django@1.11.3:CVE-2017-12794", + "severity": "Unknown", + "solution": "Upgrade to latest version or apply patch.", + "scanner": { + "id": "gemnasium", + "name": "Gemnasium" + }, + "location": { + "file": "app/requirements.txt" + }, + "identifiers": [ + { + "type": "gemnasium", + "name": "Gemnasium-6162a015-8635-4a15-8d7c-dc9321db366f", + "value": "6162a015-8635-4a15-8d7c-dc9321db366f", + "url": "https://deps.sec.gitlab.com/packages/pypi/Django/versions/1.11.3/advisories" + }, + { + "type": "cve", + "name": "CVE-2017-12794", + "value": "CVE-2017-12794", + "url": "https://cve.mitre.org/cgi-bin/cvename.cgi?name=CVE-2017-12794" + } + ], + "links": [ + { + "url": "https://www.djangoproject.com/weblog/2017/sep/05/security-releases/" + } ], + "priority": "Unknown", + "file": "app/requirements.txt", + "url": "https://www.djangoproject.com/weblog/2017/sep/05/security-releases/", "tool": "gemnasium" }, { - "priority": "Unknown", - "file": "pom.xml", - "cve": "CVE-2013-2115", - "url": "http://struts.apache.org/docs/s2-014.html", - "message": "Remote command execution due to flaw in the includeParams attribute of URL and Anchor tags for org.apache.struts/struts2-core", - "tools": [ - "gemnasium" + "category": "dependency_scanning", + "name": "nokogiri - USN-3424-1", + "message": "Vulnerabilities in libxml2", + "cve": "rails/Gemfile.lock:nokogiri@1.8.0:USN-3424-1", + "severity": "Unknown", + "solution": "Upgrade to latest version.", + "scanner": { + "id": "gemnasium", + "name": "Gemnasium" + }, + "location": { + "file": "rails/Gemfile.lock" + }, + "identifiers": [ + { + "type": "gemnasium", + "name": "Gemnasium-06565b64-486d-4326-b906-890d9915804d", + "value": "06565b64-486d-4326-b906-890d9915804d", + "url": "https://deps.sec.gitlab.com/packages/gem/nokogiri/versions/1.8.0/advisories" + }, + { + "type": "usn", + "name": "USN-3424-1", + "value": "USN-3424-1", + "url": "https://usn.ubuntu.com/3424-1/" + } + ], + "links": [ + { + "url": "https://github.com/sparklemotion/nokogiri/issues/1673" + } ], + "priority": "Unknown", + "file": "rails/Gemfile.lock", + "url": "https://github.com/sparklemotion/nokogiri/issues/1673", "tool": "gemnasium" }, { - "priority": "Unknown", - "file": "pom.xml", - "cve": "CVE-2013-2134", - "url": "http://struts.apache.org/docs/s2-015.html", - "message": "Arbitrary OGNL code execution via unsanitized wildcard matching for org.apache.struts/struts2-core", - "tools": [ - "gemnasium" + "category": "dependency_scanning", + "name": "ffi - CVE-2018-1000201", + "message": "ruby-ffi DDL loading issue on Windows OS", + "cve": "ffi:1.9.18:CVE-2018-1000201", + "severity": "High", + "solution": "upgrade to \u003e= 1.9.24", + "scanner": { + "id": "bundler_audit", + "name": "bundler-audit" + }, + "location": { + "file": "sast-sample-rails/Gemfile.lock" + }, + "identifiers": [ + { + "type": "cve", + "name": "CVE-2018-1000201", + "value": "CVE-2018-1000201", + "url": "https://cve.mitre.org/cgi-bin/cvename.cgi?name=CVE-2018-1000201" + } ], - "tool": "gemnasium" + "links": [ + { + "url": "https://github.com/ffi/ffi/releases/tag/1.9.24" + } + ], + "priority": "High", + "file": "sast-sample-rails/Gemfile.lock", + "url": "https://github.com/ffi/ffi/releases/tag/1.9.24", + "tool": "bundler_audit" } ] diff --git a/spec/fixtures/security-reports/master/gl-dependency-scanning-report.json b/spec/fixtures/security-reports/master/gl-dependency-scanning-report.json index b4e4e8e7dd5..314f04107eb 100644 --- a/spec/fixtures/security-reports/master/gl-dependency-scanning-report.json +++ b/spec/fixtures/security-reports/master/gl-dependency-scanning-report.json @@ -1,35 +1,154 @@ [ { - "priority": "Unknown", - "file": "pom.xml", - "cve": "CVE-2012-4386", - "url": "http://struts.apache.org/docs/s2-010.html", - "message": "CSRF protection bypass for org.apache.struts/struts2-core", - "tools": [ - "gemnasium" + "category": "dependency_scanning", + "name": "io.netty/netty - CVE-2014-3488", + "message": "DoS by CPU exhaustion when using malicious SSL packets", + "cve": "app/pom.xml:io.netty/netty@3.9.1.Final:CVE-2014-3488", + "severity": "Unknown", + "solution": "Upgrade to the latest version", + "scanner": { + "id": "gemnasium", + "name": "Gemnasium" + }, + "location": { + "file": "app/pom.xml" + }, + "identifiers": [ + { + "type": "gemnasium", + "name": "Gemnasium-d1bf36d9-9f07-46cd-9cfc-8675338ada8f", + "value": "d1bf36d9-9f07-46cd-9cfc-8675338ada8f", + "url": "https://deps.sec.gitlab.com/packages/maven/io.netty/netty/versions/3.9.1.Final/advisories" + }, + { + "type": "cve", + "name": "CVE-2014-3488", + "value": "CVE-2014-3488", + "url": "https://cve.mitre.org/cgi-bin/cvename.cgi?name=CVE-2014-3488" + } + ], + "links": [ + { + "url": "https://bugzilla.redhat.com/CVE-2014-3488" + }, + { + "url": "http://netty.io/news/2014/06/11/3.html" + }, + { + "url": "https://github.com/netty/netty/issues/2562" + } ], + "priority": "Unknown", + "file": "app/pom.xml", + "url": "https://bugzilla.redhat.com/CVE-2014-3488", "tool": "gemnasium" }, { - "priority": "Unknown", - "file": "pom.xml", - "cve": "CVE-2012-4387", - "url": "http://struts.apache.org/docs/s2-011.html", - "message": "Long parameter name DoS for org.apache.struts/struts2-core", - "tools": [ - "gemnasium" + "category": "dependency_scanning", + "name": "Django - CVE-2017-12794", + "message": "Possible XSS in traceback section of technical 500 debug page", + "cve": "app/requirements.txt:Django@1.11.3:CVE-2017-12794", + "severity": "Unknown", + "solution": "Upgrade to latest version or apply patch.", + "scanner": { + "id": "gemnasium", + "name": "Gemnasium" + }, + "location": { + "file": "app/requirements.txt" + }, + "identifiers": [ + { + "type": "gemnasium", + "name": "Gemnasium-6162a015-8635-4a15-8d7c-dc9321db366f", + "value": "6162a015-8635-4a15-8d7c-dc9321db366f", + "url": "https://deps.sec.gitlab.com/packages/pypi/Django/versions/1.11.3/advisories" + }, + { + "type": "cve", + "name": "CVE-2017-12794", + "value": "CVE-2017-12794", + "url": "https://cve.mitre.org/cgi-bin/cvename.cgi?name=CVE-2017-12794" + } ], + "links": [ + { + "url": "https://www.djangoproject.com/weblog/2017/sep/05/security-releases/" + } + ], + "priority": "Unknown", + "file": "app/requirements.txt", + "url": "https://www.djangoproject.com/weblog/2017/sep/05/security-releases/", "tool": "gemnasium" }, { - "priority": "Unknown", - "file": "pom.xml", - "cve": "CVE-2013-1966", - "url": "http://struts.apache.org/docs/s2-014.html", - "message": "Remote command execution due to flaw in the includeParams attribute of URL and Anchor tags for org.apache.struts/struts2-core", - "tools": [ - "gemnasium" + "category": "dependency_scanning", + "name": "nokogiri - USN-3424-1", + "message": "Vulnerabilities in libxml2", + "cve": "rails/Gemfile.lock:nokogiri@1.8.0:USN-3424-1", + "severity": "Unknown", + "solution": "Upgrade to latest version.", + "scanner": { + "id": "gemnasium", + "name": "Gemnasium" + }, + "location": { + "file": "rails/Gemfile.lock" + }, + "identifiers": [ + { + "type": "gemnasium", + "name": "Gemnasium-06565b64-486d-4326-b906-890d9915804d", + "value": "06565b64-486d-4326-b906-890d9915804d", + "url": "https://deps.sec.gitlab.com/packages/gem/nokogiri/versions/1.8.0/advisories" + }, + { + "type": "usn", + "name": "USN-3424-1", + "value": "USN-3424-1", + "url": "https://usn.ubuntu.com/3424-1/" + } ], + "links": [ + { + "url": "https://github.com/sparklemotion/nokogiri/issues/1673" + } + ], + "priority": "Unknown", + "file": "rails/Gemfile.lock", + "url": "https://github.com/sparklemotion/nokogiri/issues/1673", "tool": "gemnasium" + }, + { + "category": "dependency_scanning", + "name": "ffi - CVE-2018-1000201", + "message": "ruby-ffi DDL loading issue on Windows OS", + "cve": "ffi:1.9.18:CVE-2018-1000201", + "severity": "High", + "solution": "upgrade to \u003e= 1.9.24", + "scanner": { + "id": "bundler_audit", + "name": "bundler-audit" + }, + "location": { + "file": "sast-sample-rails/Gemfile.lock" + }, + "identifiers": [ + { + "type": "cve", + "name": "CVE-2018-1000201", + "value": "CVE-2018-1000201", + "url": "https://cve.mitre.org/cgi-bin/cvename.cgi?name=CVE-2018-1000201" + } + ], + "links": [ + { + "url": "https://github.com/ffi/ffi/releases/tag/1.9.24" + } + ], + "priority": "High", + "file": "sast-sample-rails/Gemfile.lock", + "url": "https://github.com/ffi/ffi/releases/tag/1.9.24", + "tool": "bundler_audit" } ] diff --git a/spec/javascripts/diffs/components/app_spec.js b/spec/javascripts/diffs/components/app_spec.js index 1a0b7612ee9..1e2f7ff4fd8 100644 --- a/spec/javascripts/diffs/components/app_spec.js +++ b/spec/javascripts/diffs/components/app_spec.js @@ -13,6 +13,8 @@ describe('diffs/components/app', () => { beforeEach(() => { // setup globals (needed for component to mount :/) window.mrTabs = jasmine.createSpyObj('mrTabs', ['resetViewContainer']); + window.mrTabs.expandViewContainer = jasmine.createSpy(); + window.location.hash = 'ABC_123'; // setup component const store = createDiffsStore(); @@ -39,4 +41,15 @@ describe('diffs/components/app', () => { it('does not show commit info', () => { expect(vm.$el).not.toContainElement('.blob-commit-info'); }); + + it('sets highlighted row if hash exists in location object', done => { + vm.$props.shouldShow = true; + + vm.$nextTick() + .then(() => { + expect(vm.$store.state.diffs.highlightedRow).toBe('ABC_123'); + }) + .then(done) + .catch(done.fail); + }); }); diff --git a/spec/javascripts/diffs/components/diff_table_cell_spec.js b/spec/javascripts/diffs/components/diff_table_cell_spec.js new file mode 100644 index 00000000000..170e661beea --- /dev/null +++ b/spec/javascripts/diffs/components/diff_table_cell_spec.js @@ -0,0 +1,37 @@ +import Vue from 'vue'; +import store from '~/mr_notes/stores'; +import DiffTableCell from '~/diffs/components/diff_table_cell.vue'; +import { createComponentWithStore } from 'spec/helpers/vue_mount_component_helper'; +import diffFileMockData from '../mock_data/diff_file'; + +describe('DiffTableCell', () => { + const createComponent = options => + createComponentWithStore(Vue.extend(DiffTableCell), store, { + line: diffFileMockData.highlighted_diff_lines[0], + fileHash: diffFileMockData.file_hash, + contextLinesPath: 'contextLinesPath', + ...options, + }).$mount(); + + it('does not highlight row when isHighlighted prop is false', done => { + const vm = createComponent({ isHighlighted: false }); + + vm.$nextTick() + .then(() => { + expect(vm.$el.classList).not.toContain('hll'); + }) + .then(done) + .catch(done.fail); + }); + + it('highlights row when isHighlighted prop is true', done => { + const vm = createComponent({ isHighlighted: true }); + + vm.$nextTick() + .then(() => { + expect(vm.$el.classList).toContain('hll'); + }) + .then(done) + .catch(done.fail); + }); +}); diff --git a/spec/javascripts/diffs/components/inline_diff_table_row_spec.js b/spec/javascripts/diffs/components/inline_diff_table_row_spec.js new file mode 100644 index 00000000000..97926f6625e --- /dev/null +++ b/spec/javascripts/diffs/components/inline_diff_table_row_spec.js @@ -0,0 +1,42 @@ +import Vue from 'vue'; +import store from '~/mr_notes/stores'; +import InlineDiffTableRow from '~/diffs/components/inline_diff_table_row.vue'; +import { createComponentWithStore } from 'spec/helpers/vue_mount_component_helper'; +import diffFileMockData from '../mock_data/diff_file'; + +describe('InlineDiffTableRow', () => { + let vm; + const thisLine = diffFileMockData.highlighted_diff_lines[0]; + + beforeEach(() => { + vm = createComponentWithStore(Vue.extend(InlineDiffTableRow), store, { + line: thisLine, + fileHash: diffFileMockData.file_hash, + contextLinesPath: 'contextLinesPath', + isHighlighted: false, + }).$mount(); + }); + + it('does not add hll class to line content when line does not match highlighted row', done => { + vm.$nextTick() + .then(() => { + expect(vm.$el.querySelector('.line_content').classList).not.toContain('hll'); + }) + .then(done) + .catch(done.fail); + }); + + it('adds hll class to lineContent when line is the highlighted row', done => { + vm.$nextTick() + .then(() => { + vm.$store.state.diffs.highlightedRow = thisLine.line_code; + + return vm.$nextTick(); + }) + .then(() => { + expect(vm.$el.querySelector('.line_content').classList).toContain('hll'); + }) + .then(done) + .catch(done.fail); + }); +}); diff --git a/spec/javascripts/diffs/components/parallel_diff_table_row_spec.js b/spec/javascripts/diffs/components/parallel_diff_table_row_spec.js new file mode 100644 index 00000000000..311eaaaa7c8 --- /dev/null +++ b/spec/javascripts/diffs/components/parallel_diff_table_row_spec.js @@ -0,0 +1,85 @@ +import Vue from 'vue'; +import { createStore } from '~/mr_notes/stores'; +import ParallelDiffTableRow from '~/diffs/components/parallel_diff_table_row.vue'; +import { createComponentWithStore } from 'spec/helpers/vue_mount_component_helper'; +import diffFileMockData from '../mock_data/diff_file'; + +describe('ParallelDiffTableRow', () => { + describe('when one side is empty', () => { + let vm; + const thisLine = diffFileMockData.parallel_diff_lines[0]; + const rightLine = diffFileMockData.parallel_diff_lines[0].right; + + beforeEach(() => { + vm = createComponentWithStore(Vue.extend(ParallelDiffTableRow), createStore(), { + line: thisLine, + fileHash: diffFileMockData.file_hash, + contextLinesPath: 'contextLinesPath', + isHighlighted: false, + }).$mount(); + }); + + it('does not highlight non empty line content when line does not match highlighted row', done => { + vm.$nextTick() + .then(() => { + expect(vm.$el.querySelector('.line_content.right-side').classList).not.toContain('hll'); + }) + .then(done) + .catch(done.fail); + }); + + it('highlights nonempty line content when line is the highlighted row', done => { + vm.$nextTick() + .then(() => { + vm.$store.state.diffs.highlightedRow = rightLine.line_code; + + return vm.$nextTick(); + }) + .then(() => { + expect(vm.$el.querySelector('.line_content.right-side').classList).toContain('hll'); + }) + .then(done) + .catch(done.fail); + }); + }); + + describe('when both sides have content', () => { + let vm; + const thisLine = diffFileMockData.parallel_diff_lines[2]; + const rightLine = diffFileMockData.parallel_diff_lines[2].right; + + beforeEach(() => { + vm = createComponentWithStore(Vue.extend(ParallelDiffTableRow), createStore(), { + line: thisLine, + fileHash: diffFileMockData.file_hash, + contextLinesPath: 'contextLinesPath', + isHighlighted: false, + }).$mount(); + }); + + it('does not highlight either line when line does not match highlighted row', done => { + vm.$nextTick() + .then(() => { + expect(vm.$el.querySelector('.line_content.right-side').classList).not.toContain('hll'); + expect(vm.$el.querySelector('.line_content.left-side').classList).not.toContain('hll'); + }) + .then(done) + .catch(done.fail); + }); + + it('adds hll class to lineContent when line is the highlighted row', done => { + vm.$nextTick() + .then(() => { + vm.$store.state.diffs.highlightedRow = rightLine.line_code; + + return vm.$nextTick(); + }) + .then(() => { + expect(vm.$el.querySelector('.line_content.right-side').classList).toContain('hll'); + expect(vm.$el.querySelector('.line_content.left-side').classList).toContain('hll'); + }) + .then(done) + .catch(done.fail); + }); + }); +}); diff --git a/spec/javascripts/diffs/store/actions_spec.js b/spec/javascripts/diffs/store/actions_spec.js index 43d8d950bed..205138bd845 100644 --- a/spec/javascripts/diffs/store/actions_spec.js +++ b/spec/javascripts/diffs/store/actions_spec.js @@ -22,6 +22,7 @@ import actions, { expandAllFiles, toggleFileDiscussions, saveDiffDiscussion, + setHighlightedRow, toggleTreeOpen, scrollToFile, toggleShowTreeList, @@ -92,6 +93,14 @@ describe('DiffsStoreActions', () => { }); }); + describe('setHighlightedRow', () => { + it('should set lineHash and fileHash of highlightedRow', () => { + testAction(setHighlightedRow, 'ABC_123', {}, [ + { type: types.SET_HIGHLIGHTED_ROW, payload: 'ABC_123' }, + ]); + }); + }); + describe('assignDiscussionsToDiff', () => { it('should merge discussions into diffs', done => { const state = { @@ -469,7 +478,7 @@ describe('DiffsStoreActions', () => { describe('scrollToLineIfNeededInline', () => { const lineMock = { - lineCode: 'ABC_123', + line_code: 'ABC_123', }; it('should not call handleLocationHash when there is not hash', () => { @@ -520,7 +529,7 @@ describe('DiffsStoreActions', () => { const lineMock = { left: null, right: { - lineCode: 'ABC_123', + line_code: 'ABC_123', }, }; diff --git a/spec/javascripts/diffs/store/mutations_spec.js b/spec/javascripts/diffs/store/mutations_spec.js index d1040ace5ca..7a06c178f0b 100644 --- a/spec/javascripts/diffs/store/mutations_spec.js +++ b/spec/javascripts/diffs/store/mutations_spec.js @@ -360,6 +360,16 @@ describe('DiffsStoreMutations', () => { }); }); + describe('Set highlighted row', () => { + it('sets highlighted row', () => { + const state = createState(); + + mutations[types.SET_HIGHLIGHTED_ROW](state, 'ABC_123'); + + expect(state.highlightedRow).toBe('ABC_123'); + }); + }); + describe('TOGGLE_LINE_HAS_FORM', () => { it('sets hasForm on lines', () => { const file = { diff --git a/spec/javascripts/lib/utils/url_utility_spec.js b/spec/javascripts/lib/utils/url_utility_spec.js index c7f4092911c..e4df8441793 100644 --- a/spec/javascripts/lib/utils/url_utility_spec.js +++ b/spec/javascripts/lib/utils/url_utility_spec.js @@ -1,4 +1,4 @@ -import { webIDEUrl } from '~/lib/utils/url_utility'; +import { webIDEUrl, mergeUrlParams } from '~/lib/utils/url_utility'; describe('URL utility', () => { describe('webIDEUrl', () => { @@ -26,4 +26,26 @@ describe('URL utility', () => { }); }); }); + + describe('mergeUrlParams', () => { + it('adds w', () => { + expect(mergeUrlParams({ w: 1 }, '#frag')).toBe('?w=1#frag'); + expect(mergeUrlParams({ w: 1 }, '/path#frag')).toBe('/path?w=1#frag'); + expect(mergeUrlParams({ w: 1 }, 'https://host/path')).toBe('https://host/path?w=1'); + expect(mergeUrlParams({ w: 1 }, 'https://host/path#frag')).toBe('https://host/path?w=1#frag'); + expect(mergeUrlParams({ w: 1 }, 'https://h/p?k1=v1#frag')).toBe('https://h/p?k1=v1&w=1#frag'); + }); + + it('updates w', () => { + expect(mergeUrlParams({ w: 1 }, '?k1=v1&w=0#frag')).toBe('?k1=v1&w=1#frag'); + }); + + it('adds multiple params', () => { + expect(mergeUrlParams({ a: 1, b: 2, c: 3 }, '#frag')).toBe('?a=1&b=2&c=3#frag'); + }); + + it('adds and updates encoded params', () => { + expect(mergeUrlParams({ a: '&', q: '?' }, '?a=%23#frag')).toBe('?a=%26&q=%3F#frag'); + }); + }); }); diff --git a/spec/javascripts/monitoring/graph_spec.js b/spec/javascripts/monitoring/graph_spec.js index 4cc18afdf24..59d6d4f3a7f 100644 --- a/spec/javascripts/monitoring/graph_spec.js +++ b/spec/javascripts/monitoring/graph_spec.js @@ -5,6 +5,7 @@ import { deploymentData, convertDatesMultipleSeries, singleRowMetricsMultipleSeries, + queryWithoutData, } from './mock_data'; const tagsPath = 'http://test.host/frontend-fixtures/environments-project/tags'; @@ -104,4 +105,23 @@ describe('Graph', () => { expect(component.currentData).toBe(component.timeSeries[0].values[10]); }); + + describe('Without data to display', () => { + it('shows a "no data to display" empty state on a graph', done => { + const component = createComponent({ + graphData: queryWithoutData, + deploymentData, + tagsPath, + projectPath, + }); + + Vue.nextTick(() => { + expect( + component.$el.querySelector('.js-no-data-to-display text').textContent.trim(), + ).toEqual('No data to display'); + + done(); + }); + }); + }); }); diff --git a/spec/javascripts/monitoring/mock_data.js b/spec/javascripts/monitoring/mock_data.js index 6c833b17f98..18ad9843d22 100644 --- a/spec/javascripts/monitoring/mock_data.js +++ b/spec/javascripts/monitoring/mock_data.js @@ -14,7 +14,7 @@ export const metricsGroupsAPIResponse = { queries: [ { query_range: 'avg(container_memory_usage_bytes{%{environment_filter}}) / 2^20', - y_label: 'Memory', + label: 'Memory', unit: 'MiB', result: [ { @@ -324,12 +324,15 @@ export const metricsGroupsAPIResponse = { ], }, { + id: 6, title: 'CPU usage', weight: 1, queries: [ { query_range: 'avg(rate(container_cpu_usage_seconds_total{%{environment_filter}}[2m])) * 100', + label: 'Core Usage', + unit: 'Cores', result: [ { metric: {}, @@ -639,6 +642,39 @@ export const metricsGroupsAPIResponse = { }, ], }, + { + group: 'NGINX', + priority: 2, + metrics: [ + { + id: 100, + title: 'Http Error Rate', + weight: 100, + queries: [ + { + query_range: + 'sum(rate(nginx_upstream_responses_total{status_code="5xx", upstream=~"nginx-test-8691397-production-.*"}[2m])) / sum(rate(nginx_upstream_responses_total{upstream=~"nginx-test-8691397-production-.*"}[2m])) * 100', + label: '5xx errors', + unit: '%', + result: [ + { + metric: {}, + values: [ + [1495700554.925, NaN], + [1495700614.925, NaN], + [1495700674.925, NaN], + [1495700734.925, NaN], + [1495700794.925, NaN], + [1495700854.925, NaN], + [1495700914.925, NaN], + ], + }, + ], + }, + ], + }, + ], + }, ], last_update: '2017-05-25T13:18:34.949Z', }; @@ -6526,6 +6562,21 @@ export const singleRowMetricsMultipleSeries = [ }, ]; +export const queryWithoutData = { + title: 'HTTP Error rate', + weight: 10, + y_label: 'Http Error Rate', + queries: [ + { + query_range: + 'sum(rate(nginx_upstream_responses_total{status_code="5xx", upstream=~"nginx-test-8691397-production-.*"}[2m])) / sum(rate(nginx_upstream_responses_total{upstream=~"nginx-test-8691397-production-.*"}[2m])) * 100', + label: '5xx errors', + unit: '%', + result: [], + }, + ], +}; + export function convertDatesMultipleSeries(multipleSeries) { const convertedMultiple = multipleSeries; multipleSeries.forEach((column, index) => { diff --git a/spec/javascripts/monitoring/monitoring_store_spec.js b/spec/javascripts/monitoring/monitoring_store_spec.js index bf68c911549..d8a980c874d 100644 --- a/spec/javascripts/monitoring/monitoring_store_spec.js +++ b/spec/javascripts/monitoring/monitoring_store_spec.js @@ -1,31 +1,35 @@ import MonitoringStore from '~/monitoring/stores/monitoring_store'; import MonitoringMock, { deploymentData, environmentData } from './mock_data'; -describe('MonitoringStore', function() { - this.store = new MonitoringStore(); - this.store.storeMetrics(MonitoringMock.data); - - it('contains one group that contains two queries sorted by priority', () => { - expect(this.store.groups).toBeDefined(); - expect(this.store.groups.length).toEqual(1); - expect(this.store.groups[0].metrics.length).toEqual(2); +describe('MonitoringStore', () => { + const store = new MonitoringStore(); + store.storeMetrics(MonitoringMock.data); + + it('contains two groups that contains, one of which has two queries sorted by priority', () => { + expect(store.groups).toBeDefined(); + expect(store.groups.length).toEqual(2); + expect(store.groups[0].metrics.length).toEqual(2); }); it('gets the metrics count for every group', () => { - expect(this.store.getMetricsCount()).toEqual(2); + expect(store.getMetricsCount()).toEqual(3); }); it('contains deployment data', () => { - this.store.storeDeploymentData(deploymentData); + store.storeDeploymentData(deploymentData); - expect(this.store.deploymentData).toBeDefined(); - expect(this.store.deploymentData.length).toEqual(3); - expect(typeof this.store.deploymentData[0]).toEqual('object'); + expect(store.deploymentData).toBeDefined(); + expect(store.deploymentData.length).toEqual(3); + expect(typeof store.deploymentData[0]).toEqual('object'); }); it('only stores environment data that contains deployments', () => { - this.store.storeEnvironmentsData(environmentData); + store.storeEnvironmentsData(environmentData); + + expect(store.environmentsData.length).toEqual(2); + }); - expect(this.store.environmentsData.length).toEqual(2); + it('removes the data if all the values from a query are not defined', () => { + expect(store.groups[1].metrics[0].queries[0].result.length).toEqual(0); }); }); diff --git a/spec/javascripts/pipelines/graph/job_item_spec.js b/spec/javascripts/pipelines/graph/job_item_spec.js index 88e1789184d..1cdb0aff524 100644 --- a/spec/javascripts/pipelines/graph/job_item_spec.js +++ b/spec/javascripts/pipelines/graph/job_item_spec.js @@ -139,57 +139,17 @@ describe('pipeline graph job item', () => { }); }); - describe('tooltip placement', () => { - it('does not set tooltip boundary by default', () => { - component = mountComponent(JobComponent, { - job: mockJob, - }); - - expect(component.tooltipBoundary).toBeNull(); - }); - - it('sets tooltip boundary to viewport for small dropdowns', () => { - component = mountComponent(JobComponent, { - job: mockJob, - dropdownLength: 1, - }); - - expect(component.tooltipBoundary).toEqual('viewport'); - }); - - it('does not set tooltip boundary for large lists', () => { - component = mountComponent(JobComponent, { - job: mockJob, - dropdownLength: 7, - }); - - expect(component.tooltipBoundary).toBeNull(); - }); - }); - describe('for delayed job', () => { - beforeEach(() => { - const fifteenMinutesInMilliseconds = 900000; - spyOn(Date, 'now').and.callFake( - () => new Date(delayedJobFixture.scheduled_at).getTime() - fifteenMinutesInMilliseconds, - ); - }); - - it('displays remaining time in tooltip', done => { + it('displays remaining time in tooltip', () => { component = mountComponent(JobComponent, { job: delayedJobFixture, }); - Vue.nextTick() - .then(() => { - expect( - component.$el - .querySelector('.js-pipeline-graph-job-link') - .getAttribute('data-original-title'), - ).toEqual('delayed job - delayed manual action (00:15:00)'); - }) - .then(done) - .catch(done.fail); + expect( + component.$el + .querySelector('.js-pipeline-graph-job-link') + .getAttribute('data-original-title'), + ).toEqual(`delayed job - delayed manual action (${component.remainingTime})`); }); }); }); diff --git a/spec/javascripts/vue_mr_widget/components/mr_widget_container_spec.js b/spec/javascripts/vue_mr_widget/components/mr_widget_container_spec.js new file mode 100644 index 00000000000..16c8c939a6f --- /dev/null +++ b/spec/javascripts/vue_mr_widget/components/mr_widget_container_spec.js @@ -0,0 +1,51 @@ +import { shallowMount, createLocalVue } from '@vue/test-utils'; +import MrWidgetContainer from '~/vue_merge_request_widget/components/mr_widget_container.vue'; + +const BODY_HTML = '<div class="test-body">Hello World</div>'; +const FOOTER_HTML = '<div class="test-footer">Goodbye!</div>'; + +describe('MrWidgetContainer', () => { + let wrapper; + + const factory = (options = {}) => { + const localVue = createLocalVue(); + + wrapper = shallowMount(localVue.extend(MrWidgetContainer), { + localVue, + ...options, + }); + }; + + afterEach(() => { + wrapper.destroy(); + }); + + it('has layout', () => { + factory(); + + expect(wrapper.is('.mr-widget-heading')).toBe(true); + expect(wrapper.contains('.mr-widget-content')).toBe(true); + }); + + it('accepts default slot', () => { + factory({ + slots: { + default: BODY_HTML, + }, + }); + + expect(wrapper.contains('.mr-widget-content .test-body')).toBe(true); + }); + + it('accepts footer slot', () => { + factory({ + slots: { + default: BODY_HTML, + footer: FOOTER_HTML, + }, + }); + + expect(wrapper.contains('.mr-widget-content .test-body')).toBe(true); + expect(wrapper.contains('.test-footer')).toBe(true); + }); +}); diff --git a/spec/javascripts/vue_mr_widget/components/mr_widget_icon_spec.js b/spec/javascripts/vue_mr_widget/components/mr_widget_icon_spec.js new file mode 100644 index 00000000000..f7c2376eebf --- /dev/null +++ b/spec/javascripts/vue_mr_widget/components/mr_widget_icon_spec.js @@ -0,0 +1,30 @@ +import { shallowMount, createLocalVue } from '@vue/test-utils'; +import MrWidgetIcon from '~/vue_merge_request_widget/components/mr_widget_icon.vue'; +import Icon from '~/vue_shared/components/icon.vue'; + +const TEST_ICON = 'commit'; + +describe('MrWidgetIcon', () => { + let wrapper; + + beforeEach(() => { + const localVue = createLocalVue(); + + wrapper = shallowMount(localVue.extend(MrWidgetIcon), { + propsData: { + name: TEST_ICON, + }, + sync: false, + localVue, + }); + }); + + afterEach(() => { + wrapper.destroy(); + }); + + it('renders icon and container', () => { + expect(wrapper.is('.circle-icon-container')).toBe(true); + expect(wrapper.find(Icon).props('name')).toEqual(TEST_ICON); + }); +}); diff --git a/spec/javascripts/vue_mr_widget/components/mr_widget_pipeline_container_spec.js b/spec/javascripts/vue_mr_widget/components/mr_widget_pipeline_container_spec.js new file mode 100644 index 00000000000..e5155573f6f --- /dev/null +++ b/spec/javascripts/vue_mr_widget/components/mr_widget_pipeline_container_spec.js @@ -0,0 +1,90 @@ +import { shallowMount, createLocalVue } from '@vue/test-utils'; +import MrWidgetPipelineContainer from '~/vue_merge_request_widget/components/mr_widget_pipeline_container.vue'; +import MrWidgetPipeline from '~/vue_merge_request_widget/components/mr_widget_pipeline.vue'; +import { mockStore } from '../mock_data'; + +describe('MrWidgetPipelineContainer', () => { + let wrapper; + + const factory = (props = {}) => { + const localVue = createLocalVue(); + + wrapper = shallowMount(localVue.extend(MrWidgetPipelineContainer), { + propsData: { + mr: Object.assign({}, mockStore), + ...props, + }, + localVue, + }); + }; + + afterEach(() => { + wrapper.destroy(); + }); + + describe('when pre merge', () => { + beforeEach(() => { + factory(); + }); + + it('renders pipeline', () => { + expect(wrapper.find(MrWidgetPipeline).exists()).toBe(true); + expect(wrapper.find(MrWidgetPipeline).props()).toEqual( + jasmine.objectContaining({ + pipeline: mockStore.pipeline, + ciStatus: mockStore.ciStatus, + hasCi: mockStore.hasCI, + sourceBranch: mockStore.sourceBranch, + sourceBranchLink: mockStore.sourceBranchLink, + }), + ); + }); + + it('renders deployments', () => { + const expectedProps = mockStore.deployments.map(dep => + jasmine.objectContaining({ + deployment: dep, + showMetrics: false, + }), + ); + + const deployments = wrapper.findAll('.mr-widget-extension .js-pre-deployment'); + + expect(deployments.wrappers.map(x => x.props())).toEqual(expectedProps); + }); + }); + + describe('when post merge', () => { + beforeEach(() => { + factory({ + isPostMerge: true, + }); + }); + + it('renders pipeline', () => { + expect(wrapper.find(MrWidgetPipeline).exists()).toBe(true); + expect(wrapper.find(MrWidgetPipeline).props()).toEqual( + jasmine.objectContaining({ + pipeline: mockStore.mergePipeline, + ciStatus: mockStore.ciStatus, + hasCi: mockStore.hasCI, + sourceBranch: mockStore.targetBranch, + sourceBranchLink: mockStore.targetBranch, + }), + ); + }); + + it('renders deployments', () => { + const expectedProps = mockStore.postMergeDeployments.map(dep => + jasmine.objectContaining({ + deployment: dep, + showMetrics: true, + }), + ); + + const deployments = wrapper.findAll('.mr-widget-extension .js-post-deployment'); + + expect(deployments.wrappers.map(x => x.props())).toEqual(expectedProps); + }); + }); +}); diff --git a/spec/javascripts/vue_mr_widget/mock_data.js b/spec/javascripts/vue_mr_widget/mock_data.js index 17554c4fe42..072e98fc0e8 100644 --- a/spec/javascripts/vue_mr_widget/mock_data.js +++ b/spec/javascripts/vue_mr_widget/mock_data.js @@ -222,3 +222,16 @@ export default { 'http://localhost:3000/root/acets-app/commit/53027d060246c8f47e4a9310fb332aa52f221775', troubleshooting_docs_path: 'help', }; + +export const mockStore = { + pipeline: { id: 0 }, + mergePipeline: { id: 1 }, + targetBranch: 'target-branch', + sourceBranch: 'source-branch', + sourceBranchLink: 'source-branch-link', + deployments: [{ id: 0, name: 'bogus' }, { id: 1, name: 'bogus-docs' }], + postMergeDeployments: [{ id: 0, name: 'prod' }, { id: 1, name: 'prod-docs' }], + troubleshootingDocsPath: 'troubleshooting-docs-path', + ciStatus: 'ci-status', + hasCI: true, +}; diff --git a/spec/javascripts/vue_shared/components/notes/timeline_entry_item_spec.js b/spec/javascripts/vue_shared/components/notes/timeline_entry_item_spec.js new file mode 100644 index 00000000000..c15635f2105 --- /dev/null +++ b/spec/javascripts/vue_shared/components/notes/timeline_entry_item_spec.js @@ -0,0 +1,40 @@ +import { shallowMount, createLocalVue } from '@vue/test-utils'; +import TimelineEntryItem from '~/vue_shared/components/notes/timeline_entry_item.vue'; + +describe(TimelineEntryItem.name, () => { + let wrapper; + + const factory = (options = {}) => { + const localVue = createLocalVue(); + + wrapper = shallowMount(TimelineEntryItem, { + localVue, + ...options, + }); + }; + + afterEach(() => { + wrapper.destroy(); + }); + + it('renders correctly', () => { + factory(); + + expect(wrapper.is('.timeline-entry')).toBe(true); + + expect(wrapper.contains('.timeline-entry-inner')).toBe(true); + }); + + it('accepts default slot', () => { + const dummyContent = '<p>some content</p>'; + factory({ + slots: { + default: dummyContent, + }, + }); + + const content = wrapper.find('.timeline-entry-inner :first-child'); + + expect(content.html()).toBe(dummyContent); + }); +}); diff --git a/spec/lib/gitlab/ci/config/entry/attributable_spec.rb b/spec/lib/gitlab/config/entry/attributable_spec.rb index b028b771375..abb4fff3ad7 100644 --- a/spec/lib/gitlab/ci/config/entry/attributable_spec.rb +++ b/spec/lib/gitlab/config/entry/attributable_spec.rb @@ -1,9 +1,9 @@ require 'spec_helper' -describe Gitlab::Ci::Config::Entry::Attributable do +describe Gitlab::Config::Entry::Attributable do let(:node) do Class.new do - include Gitlab::Ci::Config::Entry::Attributable + include Gitlab::Config::Entry::Attributable end end @@ -48,7 +48,7 @@ describe Gitlab::Ci::Config::Entry::Attributable do it 'raises an error' do expectation = expect do Class.new(String) do - include Gitlab::Ci::Config::Entry::Attributable + include Gitlab::Config::Entry::Attributable attributes :length end diff --git a/spec/lib/gitlab/ci/config/entry/boolean_spec.rb b/spec/lib/gitlab/config/entry/boolean_spec.rb index 5f067cad93c..1b7a3f850ec 100644 --- a/spec/lib/gitlab/ci/config/entry/boolean_spec.rb +++ b/spec/lib/gitlab/config/entry/boolean_spec.rb @@ -1,6 +1,6 @@ require 'spec_helper' -describe Gitlab::Ci::Config::Entry::Boolean do +describe Gitlab::Config::Entry::Boolean do let(:entry) { described_class.new(config) } describe 'validations' do diff --git a/spec/lib/gitlab/ci/config/entry/configurable_spec.rb b/spec/lib/gitlab/config/entry/configurable_spec.rb index 088d4b472da..85a7cf1d241 100644 --- a/spec/lib/gitlab/ci/config/entry/configurable_spec.rb +++ b/spec/lib/gitlab/config/entry/configurable_spec.rb @@ -1,9 +1,9 @@ require 'spec_helper' -describe Gitlab::Ci::Config::Entry::Configurable do +describe Gitlab::Config::Entry::Configurable do let(:entry) do - Class.new(Gitlab::Ci::Config::Entry::Node) do - include Gitlab::Ci::Config::Entry::Configurable + Class.new(Gitlab::Config::Entry::Node) do + include Gitlab::Config::Entry::Configurable end end @@ -39,7 +39,7 @@ describe Gitlab::Ci::Config::Entry::Configurable do it 'creates a node factory' do expect(entry.nodes[:object]) - .to be_an_instance_of Gitlab::Ci::Config::Entry::Factory + .to be_an_instance_of Gitlab::Config::Entry::Factory end it 'returns a duplicated factory object' do diff --git a/spec/lib/gitlab/ci/config/entry/factory_spec.rb b/spec/lib/gitlab/config/entry/factory_spec.rb index 8dd48e4efae..c29d17eaee3 100644 --- a/spec/lib/gitlab/ci/config/entry/factory_spec.rb +++ b/spec/lib/gitlab/config/entry/factory_spec.rb @@ -1,9 +1,17 @@ require 'spec_helper' -describe Gitlab::Ci::Config::Entry::Factory do +describe Gitlab::Config::Entry::Factory do describe '#create!' do + class Script < Gitlab::Config::Entry::Node + include Gitlab::Config::Entry::Validatable + + validations do + validates :config, array_of_strings: true + end + end + + let(:entry) { Script } let(:factory) { described_class.new(entry) } - let(:entry) { Gitlab::Ci::Config::Entry::Script } context 'when setting a concrete value' do it 'creates entry with valid value' do @@ -54,7 +62,7 @@ describe Gitlab::Ci::Config::Entry::Factory do context 'when not setting a value' do it 'raises error' do expect { factory.create! }.to raise_error( - Gitlab::Ci::Config::Entry::Factory::InvalidFactory + Gitlab::Config::Entry::Factory::InvalidFactory ) end end diff --git a/spec/lib/gitlab/ci/config/entry/simplifiable_spec.rb b/spec/lib/gitlab/config/entry/simplifiable_spec.rb index 395062207a3..bc8387ada67 100644 --- a/spec/lib/gitlab/ci/config/entry/simplifiable_spec.rb +++ b/spec/lib/gitlab/config/entry/simplifiable_spec.rb @@ -1,6 +1,6 @@ require 'spec_helper' -describe Gitlab::Ci::Config::Entry::Simplifiable do +describe Gitlab::Config::Entry::Simplifiable do describe '.strategy' do let(:entry) do Class.new(described_class) do diff --git a/spec/lib/gitlab/ci/config/entry/undefined_spec.rb b/spec/lib/gitlab/config/entry/undefined_spec.rb index fdf48d84192..48f9d276c95 100644 --- a/spec/lib/gitlab/ci/config/entry/undefined_spec.rb +++ b/spec/lib/gitlab/config/entry/undefined_spec.rb @@ -1,6 +1,6 @@ require 'spec_helper' -describe Gitlab::Ci::Config::Entry::Undefined do +describe Gitlab::Config::Entry::Undefined do let(:entry) { described_class.new } describe '#leaf?' do diff --git a/spec/lib/gitlab/ci/config/entry/unspecified_spec.rb b/spec/lib/gitlab/config/entry/unspecified_spec.rb index 66f88fa35b6..64421824a12 100644 --- a/spec/lib/gitlab/ci/config/entry/unspecified_spec.rb +++ b/spec/lib/gitlab/config/entry/unspecified_spec.rb @@ -1,6 +1,6 @@ require 'spec_helper' -describe Gitlab::Ci::Config::Entry::Unspecified do +describe Gitlab::Config::Entry::Unspecified do let(:unspecified) { described_class.new(entry) } let(:entry) { spy('Entry') } diff --git a/spec/lib/gitlab/ci/config/entry/validatable_spec.rb b/spec/lib/gitlab/config/entry/validatable_spec.rb index ae2a7a51ba6..5a8f9766d23 100644 --- a/spec/lib/gitlab/ci/config/entry/validatable_spec.rb +++ b/spec/lib/gitlab/config/entry/validatable_spec.rb @@ -1,9 +1,9 @@ require 'spec_helper' -describe Gitlab::Ci::Config::Entry::Validatable do +describe Gitlab::Config::Entry::Validatable do let(:entry) do - Class.new(Gitlab::Ci::Config::Entry::Node) do - include Gitlab::Ci::Config::Entry::Validatable + Class.new(Gitlab::Config::Entry::Node) do + include Gitlab::Config::Entry::Validatable end end @@ -20,7 +20,7 @@ describe Gitlab::Ci::Config::Entry::Validatable do it 'returns validator' do expect(entry.validator.superclass) - .to be Gitlab::Ci::Config::Entry::Validator + .to be Gitlab::Config::Entry::Validator end it 'returns only one validator to mitigate leaks' do diff --git a/spec/lib/gitlab/ci/config/entry/validator_spec.rb b/spec/lib/gitlab/config/entry/validator_spec.rb index 172b6b47a4f..efa16c4265c 100644 --- a/spec/lib/gitlab/ci/config/entry/validator_spec.rb +++ b/spec/lib/gitlab/config/entry/validator_spec.rb @@ -1,6 +1,6 @@ require 'spec_helper' -describe Gitlab::Ci::Config::Entry::Validator do +describe Gitlab::Config::Entry::Validator do let(:validator) { Class.new(described_class) } let(:validator_instance) { validator.new(node) } let(:node) { spy('node') } diff --git a/spec/lib/gitlab/ci/config/loader_spec.rb b/spec/lib/gitlab/config/loader/yaml_spec.rb index 590fc8904c1..44c9a3896a8 100644 --- a/spec/lib/gitlab/ci/config/loader_spec.rb +++ b/spec/lib/gitlab/config/loader/yaml_spec.rb @@ -1,6 +1,6 @@ require 'spec_helper' -describe Gitlab::Ci::Config::Loader do +describe Gitlab::Config::Loader::Yaml do let(:loader) { described_class.new(yml) } context 'when yaml syntax is correct' do @@ -31,7 +31,7 @@ describe Gitlab::Ci::Config::Loader do describe '#load!' do it 'raises error' do expect { loader.load! }.to raise_error( - Gitlab::Ci::Config::Loader::FormatError, + Gitlab::Config::Loader::FormatError, 'Invalid configuration format' ) end @@ -43,7 +43,7 @@ describe Gitlab::Ci::Config::Loader do describe '#initialize' do it 'raises FormatError' do - expect { loader }.to raise_error(Gitlab::Ci::Config::Loader::FormatError, 'Unknown alias: bad_alias') + expect { loader }.to raise_error(Gitlab::Config::Loader::FormatError, 'Unknown alias: bad_alias') end end end diff --git a/spec/lib/gitlab/import_export/all_models.yml b/spec/lib/gitlab/import_export/all_models.yml index 1d184375a52..31ab11bbf8d 100644 --- a/spec/lib/gitlab/import_export/all_models.yml +++ b/spec/lib/gitlab/import_export/all_models.yml @@ -121,6 +121,8 @@ pipelines: - artifacts - pipeline_schedule - merge_requests +- deployments +- environments pipeline_variables: - pipeline stages: @@ -245,6 +247,7 @@ project: - protected_branches - protected_tags - project_members +- project_repository - users - requesters - deploy_keys_projects diff --git a/spec/migrations/clean_up_for_members_spec.rb b/spec/migrations/clean_up_for_members_spec.rb index 0258860d169..7876536cb3e 100644 --- a/spec/migrations/clean_up_for_members_spec.rb +++ b/spec/migrations/clean_up_for_members_spec.rb @@ -3,6 +3,7 @@ require Rails.root.join('db', 'migrate', '20171216111734_clean_up_for_members.rb describe CleanUpForMembers, :migration do let(:migration) { described_class.new } + let(:groups) { table(:namespaces) } let!(:group_member) { create_group_member } let!(:unbinded_group_member) { create_group_member } let!(:invited_group_member) { create_group_member(true) } @@ -25,7 +26,7 @@ describe CleanUpForMembers, :migration do end def create_group_member(invited = false) - fill_member(GroupMember.new(group: create_group), invited) + fill_member(GroupMember.new(source_id: create_group.id, source_type: 'Namespace'), invited) end def create_project_member(invited = false) @@ -54,7 +55,7 @@ describe CleanUpForMembers, :migration do def create_group name = FFaker::Lorem.characters(10) - Group.create(name: name, path: name.downcase.gsub(/\s/, '_')) + groups.create!(type: 'Group', name: name, path: name.downcase.gsub(/\s/, '_')) end def create_project diff --git a/spec/migrations/delete_inconsistent_internal_id_records_spec.rb b/spec/migrations/delete_inconsistent_internal_id_records_spec.rb index 4af51217031..8c55daf0d37 100644 --- a/spec/migrations/delete_inconsistent_internal_id_records_spec.rb +++ b/spec/migrations/delete_inconsistent_internal_id_records_spec.rb @@ -94,17 +94,18 @@ describe DeleteInconsistentInternalIdRecords, :migration do end context 'for milestones (by group)' do - # milestones (by group) is a little different than all of the other models - let!(:group1) { create(:group) } - let!(:group2) { create(:group) } - let!(:group3) { create(:group) } + # milestones (by group) is a little different than most of the other models + let(:groups) { table(:namespaces) } + let(:group1) { groups.create(name: 'Group 1', type: 'Group', path: 'group_1') } + let(:group2) { groups.create(name: 'Group 2', type: 'Group', path: 'group_2') } + let(:group3) { groups.create(name: 'Group 2', type: 'Group', path: 'group_3') } let(:internal_id_query) { ->(group) { InternalId.where(usage: InternalId.usages['milestones'], namespace: group) } } before do - 3.times { create(:milestone, group: group1) } - 3.times { create(:milestone, group: group2) } - 3.times { create(:milestone, group: group3) } + 3.times { create(:milestone, group_id: group1.id) } + 3.times { create(:milestone, group_id: group2.id) } + 3.times { create(:milestone, group_id: group3.id) } internal_id_query.call(group1).first.tap do |iid| iid.last_value = iid.last_value - 2 diff --git a/spec/models/environment_status_spec.rb b/spec/models/environment_status_spec.rb index 90f7e4a4590..9da16dea929 100644 --- a/spec/models/environment_status_spec.rb +++ b/spec/models/environment_status_spec.rb @@ -92,16 +92,12 @@ describe EnvironmentStatus do end describe '.build_environments_status' do - subject { described_class.send(:build_environments_status, merge_request, user, sha) } + subject { described_class.send(:build_environments_status, merge_request, user, pipeline) } let!(:build) { create(:ci_build, :deploy_to_production, pipeline: pipeline) } let(:environment) { build.deployment.environment } let(:user) { project.owner } - before do - build.deployment&.update!(sha: sha) - end - context 'when environment is created on a forked project' do let(:project) { create(:project, :repository) } let(:forked) { fork_project(project, user, repository: true) } @@ -160,6 +156,39 @@ describe EnvironmentStatus do expect(subject.count).to eq(0) end end + + context 'when multiple deployments with the same SHA in different environments' do + let(:pipeline2) { create(:ci_pipeline, sha: sha, project: project) } + let!(:build2) { create(:ci_build, :start_review_app, pipeline: pipeline2) } + + it 'returns deployments related to the head pipeline' do + expect(subject.count).to eq(1) + expect(subject[0].environment).to eq(environment) + expect(subject[0].merge_request).to eq(merge_request) + expect(subject[0].sha).to eq(sha) + end + end + + context 'when multiple deployments in the same pipeline for the same environments' do + let!(:build2) { create(:ci_build, :deploy_to_production, pipeline: pipeline) } + + it 'returns unique entries' do + expect(subject.count).to eq(1) + expect(subject[0].environment).to eq(environment) + expect(subject[0].merge_request).to eq(merge_request) + expect(subject[0].sha).to eq(sha) + end + end + + context 'when environment is stopped' do + before do + environment.stop! + end + + it 'does not return environment status' do + expect(subject.count).to eq(0) + end + end end end end diff --git a/spec/models/pool_repository_spec.rb b/spec/models/pool_repository_spec.rb index 6c904710fb5..541e78507e5 100644 --- a/spec/models/pool_repository_spec.rb +++ b/spec/models/pool_repository_spec.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + require 'spec_helper' describe PoolRepository do diff --git a/spec/models/project_repository_spec.rb b/spec/models/project_repository_spec.rb new file mode 100644 index 00000000000..c966447fedc --- /dev/null +++ b/spec/models/project_repository_spec.rb @@ -0,0 +1,23 @@ +# frozen_string_literal: true + +require 'spec_helper' + +describe ProjectRepository do + describe 'associations' do + it { is_expected.to belong_to(:shard) } + it { is_expected.to belong_to(:project) } + end + + describe '.find_project' do + it 'finds project by disk path' do + project = create(:project) + project.track_project_repository + + expect(described_class.find_project(project.disk_path)).to eq(project) + end + + it 'returns nil when it does not find the project' do + expect(described_class.find_project('@@unexisting/path/to/project')).to be_nil + end + end +end diff --git a/spec/models/project_spec.rb b/spec/models/project_spec.rb index e98c69e636a..af5b0939ca2 100644 --- a/spec/models/project_spec.rb +++ b/spec/models/project_spec.rb @@ -54,6 +54,7 @@ describe Project do it { is_expected.to have_one(:gitlab_issue_tracker_service) } it { is_expected.to have_one(:external_wiki_service) } it { is_expected.to have_one(:project_feature) } + it { is_expected.to have_one(:project_repository) } it { is_expected.to have_one(:statistics).class_name('ProjectStatistics') } it { is_expected.to have_one(:import_data).class_name('ProjectImportData') } it { is_expected.to have_one(:last_event).class_name('Event') } @@ -1618,6 +1619,30 @@ describe Project do end end + describe '#track_project_repository' do + let(:project) { create(:project, :repository) } + + it 'creates a project_repository' do + project.track_project_repository + + expect(project.reload.project_repository).to be_present + expect(project.project_repository.disk_path).to eq(project.disk_path) + expect(project.project_repository.shard_name).to eq(project.repository_storage) + end + + it 'updates the project_repository' do + project.track_project_repository + + allow(project).to receive(:disk_path).and_return('@fancy/new/path') + + expect do + project.track_project_repository + end.not_to change(ProjectRepository, :count) + + expect(project.reload.project_repository.disk_path).to eq(project.disk_path) + end + end + describe '#create_repository' do let(:project) { create(:project, :repository) } let(:shell) { Gitlab::Shell.new } diff --git a/spec/models/todo_spec.rb b/spec/models/todo_spec.rb index 82ff2a002e0..3682e21ca40 100644 --- a/spec/models/todo_spec.rb +++ b/spec/models/todo_spec.rb @@ -236,7 +236,8 @@ describe Todo do create(:todo, target: create(:merge_request)) - expect(described_class.for_target(todo.target)).to eq([todo]) + expect(described_class.for_type(Issue.name).for_target(todo.target)) + .to contain_exactly(todo) end end diff --git a/spec/requests/api/namespaces_spec.rb b/spec/requests/api/namespaces_spec.rb index e2000ab42e8..145356c4df5 100644 --- a/spec/requests/api/namespaces_spec.rb +++ b/spec/requests/api/namespaces_spec.rb @@ -3,7 +3,7 @@ require 'spec_helper' describe API::Namespaces do let(:admin) { create(:admin) } let(:user) { create(:user) } - let!(:group1) { create(:group) } + let!(:group1) { create(:group, name: 'group.one') } let!(:group2) { create(:group, :nested) } describe "GET /namespaces" do diff --git a/spec/services/ci/create_pipeline_service_spec.rb b/spec/services/ci/create_pipeline_service_spec.rb index 4d9c5aabbda..a4582d1bc64 100644 --- a/spec/services/ci/create_pipeline_service_spec.rb +++ b/spec/services/ci/create_pipeline_service_spec.rb @@ -657,4 +657,37 @@ describe Ci::CreatePipelineService do end end end + + describe '#execute!' do + subject { service.execute!(*args) } + + let(:service) { described_class.new(project, user, ref: ref_name) } + let(:args) { [:push] } + + context 'when user has a permission to create a pipeline' do + let(:user) { create(:user) } + + before do + project.add_developer(user) + end + + it 'does not raise an error' do + expect { subject }.not_to raise_error + end + + it 'creates a pipeline' do + expect { subject }.to change { Ci::Pipeline.count }.by(1) + end + end + + context 'when user does not have a permission to create a pipeline' do + let(:user) { create(:user) } + + it 'raises an error' do + expect { subject } + .to raise_error(described_class::CreateError) + .with_message('Insufficient permissions to create a new pipeline') + end + end + end end diff --git a/spec/support/helpers/features/sorting_helpers.rb b/spec/support/helpers/features/sorting_helpers.rb index ad0053ec5cf..a1ae428586e 100644 --- a/spec/support/helpers/features/sorting_helpers.rb +++ b/spec/support/helpers/features/sorting_helpers.rb @@ -13,7 +13,7 @@ module Spec module Features module SortingHelpers def sort_by(value) - find('button.dropdown-toggle').click + find('.filter-dropdown-container button.dropdown-menu-toggle').click page.within('.content ul.dropdown-menu.dropdown-menu-right li') do click_link(value) diff --git a/spec/support/helpers/sorting_helper.rb b/spec/support/helpers/sorting_helper.rb index 9496a94d8f4..e505a6b7258 100644 --- a/spec/support/helpers/sorting_helper.rb +++ b/spec/support/helpers/sorting_helper.rb @@ -10,7 +10,7 @@ # module SortingHelper def sorting_by(value) - find('button.dropdown-toggle').click + find('.filter-dropdown-container button.dropdown-menu-toggle').click page.within('.content ul.dropdown-menu.dropdown-menu-right li') do click_link value end diff --git a/spec/workers/pipeline_schedule_worker_spec.rb b/spec/workers/pipeline_schedule_worker_spec.rb index a2fe4734d47..c5a60e9855b 100644 --- a/spec/workers/pipeline_schedule_worker_spec.rb +++ b/spec/workers/pipeline_schedule_worker_spec.rb @@ -56,17 +56,89 @@ describe PipelineScheduleWorker do expect { subject }.not_to change { project.pipelines.count } end end + + context 'when gitlab-ci.yml is corrupted' do + before do + stub_ci_pipeline_yaml_file(YAML.dump(rspec: { variables: 'rspec' } )) + end + + it 'creates a failed pipeline with the reason' do + expect { subject }.to change { project.pipelines.count }.by(1) + expect(Ci::Pipeline.last).to be_config_error + expect(Ci::Pipeline.last.yaml_errors).not_to be_nil + end + end end context 'when the schedule is not runnable by the user' do - it 'deactivates the schedule' do + before do + expect(Gitlab::Sentry) + .to receive(:track_exception) + .with(Ci::CreatePipelineService::CreateError, + issue_url: 'https://gitlab.com/gitlab-org/gitlab-ce/issues/41231', + extra: { schedule_id: pipeline_schedule.id } ).once + end + + it 'does not deactivate the schedule' do + subject + + expect(pipeline_schedule.reload.active).to be_truthy + end + + it 'increments Prometheus counter' do + expect(Gitlab::Metrics) + .to receive(:counter) + .with(:pipeline_schedule_creation_failed_total, "Counter of failed attempts of pipeline schedule creation") + .and_call_original + + subject + end + + it 'logging a pipeline error' do + expect(Rails.logger) + .to receive(:error) + .with(a_string_matching("Insufficient permissions to create a new pipeline")) + .and_call_original + subject + end + + it 'does not create a pipeline' do + expect { subject }.not_to change { project.pipelines.count } + end - expect(pipeline_schedule.reload.active).to be_falsy + it 'does not raise an exception' do + expect { subject }.not_to raise_error end + end + + context 'when .gitlab-ci.yml is missing in the project' do + before do + stub_ci_pipeline_yaml_file(nil) + project.add_maintainer(user) - it 'does not schedule a pipeline' do + expect(Gitlab::Sentry) + .to receive(:track_exception) + .with(Ci::CreatePipelineService::CreateError, + issue_url: 'https://gitlab.com/gitlab-org/gitlab-ce/issues/41231', + extra: { schedule_id: pipeline_schedule.id } ).once + end + + it 'logging a pipeline error' do + expect(Rails.logger) + .to receive(:error) + .with(a_string_matching("Missing .gitlab-ci.yml file")) + .and_call_original + + subject + end + + it 'does not create a pipeline' do expect { subject }.not_to change { project.pipelines.count } end + + it 'does not raise an exception' do + expect { subject }.not_to raise_error + end end end diff --git a/vendor/jupyter/values.yaml b/vendor/jupyter/values.yaml index 24136a7aca5..781d6e3042f 100644 --- a/vendor/jupyter/values.yaml +++ b/vendor/jupyter/values.yaml @@ -22,3 +22,4 @@ ingress: enabled: true annotations: kubernetes.io/ingress.class: "nginx" + kubernetes.io/tls-acme: "true" diff --git a/yarn.lock b/yarn.lock index 6d5ff62715b..623e38815aa 100644 --- a/yarn.lock +++ b/yarn.lock @@ -1933,10 +1933,10 @@ code-point-at@^1.0.0: resolved "https://registry.yarnpkg.com/code-point-at/-/code-point-at-1.1.0.tgz#0d070b4d043a5bea33a2f1a40e2edb3d9a4ccf77" integrity sha1-DQcLTQQ6W+ozovGkDi7bPZpMz3c= -codesandbox-api@^0.0.18: - version "0.0.18" - resolved "https://registry.yarnpkg.com/codesandbox-api/-/codesandbox-api-0.0.18.tgz#56b96b37533f80d20c21861e5e477d3557e613ca" - integrity sha512-DHLR8QQpMplNDF9GDbV8EevwmF9mlMBQwiWB8bZBnP2NQQbklthqjpBwNjah8qlDgfD7vQNNcwT8uIZ24WZb7Q== +codesandbox-api@^0.0.20: + version "0.0.20" + resolved "https://registry.yarnpkg.com/codesandbox-api/-/codesandbox-api-0.0.20.tgz#174bcd76c9f31521175c6bceabc37da6b1fbc30b" + integrity sha512-jhxZzAmjCKBZad8QWMeueiQVFE87igK6F2DBOEVFFJO6jgTXT8qjuzGYepr+B8bjgo/icN7bc/2xmEMBA63s2w== codesandbox-import-util-types@^1.2.11: version "1.2.11" @@ -7261,12 +7261,12 @@ slugify@^1.3.1: resolved "https://registry.yarnpkg.com/slugify/-/slugify-1.3.1.tgz#f572127e8535329fbc6c1edb74ab856b61ad7de2" integrity sha512-6BwyhjF5tG5P8s+0DPNyJmBSBePG6iMyhjvIW5zGdA3tFik9PtK+yNkZgTeiroCRGZYgkHftFA62tGVK1EI9Kw== -smooshpack@^0.0.48: - version "0.0.48" - resolved "https://registry.yarnpkg.com/smooshpack/-/smooshpack-0.0.48.tgz#6fbeaaf59226a1fe500f56aa17185eed377d2823" - integrity sha512-BmaIr6fK6w7WBCI4V7tcZIg78WeE6X56zrhGSNk5vXavT1bQPXH+brZFq6Hwi833upY/yusG2FMVkf7TZsVv/w== +smooshpack@^0.0.53: + version "0.0.53" + resolved "https://registry.yarnpkg.com/smooshpack/-/smooshpack-0.0.53.tgz#aa397ca43619912e8ac0aa32012846ff4feaa5e8" + integrity sha512-FVXvKvZOz5+Tk/zUJ/wxM+ftu1yZtFEmeQl4chCqbzK/reU0L/OdDiYpx+/27Jt2VX09j08oIzwoyQ5fHH4+WQ== dependencies: - codesandbox-api "^0.0.18" + codesandbox-api "^0.0.20" codesandbox-import-utils "^1.2.3" lodash.isequal "^4.5.0" |