diff options
170 files changed, 2246 insertions, 522 deletions
diff --git a/.gitlab/ci/rails.gitlab-ci.yml b/.gitlab/ci/rails.gitlab-ci.yml index 01e71a7faf1..35c5f67427e 100644 --- a/.gitlab/ci/rails.gitlab-ci.yml +++ b/.gitlab/ci/rails.gitlab-ci.yml @@ -6,7 +6,7 @@ .use-pg-10: &use-pg-10 services: - - postgres:10.0 + - postgres:10.7 - redis:alpine .use-mysql: &use-mysql diff --git a/.gitlab/ci/reports.gitlab-ci.yml b/.gitlab/ci/reports.gitlab-ci.yml index 8bcf8d4cb48..d0e09dbf2f8 100644 --- a/.gitlab/ci/reports.gitlab-ci.yml +++ b/.gitlab/ci/reports.gitlab-ci.yml @@ -26,11 +26,31 @@ sast: services: - docker:stable-dind script: + - | # this is required to avoid undesirable reset of Docker image ENV variables being set on build stage + function propagate_env_vars() { + CURRENT_ENV=$(printenv) + + for VAR_NAME; do + echo $CURRENT_ENV | grep "${VAR_NAME}=" > /dev/null && echo "--env $VAR_NAME " + done + } - export SP_VERSION=$(echo "$CI_SERVER_VERSION" | sed 's/^\([0-9]*\)\.\([0-9]*\).*/\1-\2-stable/') - - docker run - --env SAST_CONFIDENCE_LEVEL="${SAST_CONFIDENCE_LEVEL:-3}" - --volume "$PWD:/code" - --volume /var/run/docker.sock:/var/run/docker.sock + - | + docker run \ + $(propagate_env_vars \ + SAST_ANALYZER_IMAGES \ + SAST_ANALYZER_IMAGE_PREFIX \ + SAST_ANALYZER_IMAGE_TAG \ + SAST_DEFAULT_ANALYZERS \ + SAST_BRAKEMAN_LEVEL \ + SAST_GOSEC_LEVEL \ + SAST_FLAWFINDER_LEVEL \ + SAST_DOCKER_CLIENT_NEGOTIATION_TIMEOUT \ + SAST_PULL_ANALYZER_IMAGE_TIMEOUT \ + SAST_RUN_ANALYZER_TIMEOUT \ + ) \ + --volume "$PWD:/code" \ + --volume /var/run/docker.sock:/var/run/docker.sock \ "registry.gitlab.com/gitlab-org/security-products/sast:$SP_VERSION" /app/bin/run /code artifacts: reports: @@ -50,10 +70,28 @@ dependency_scanning: - docker:stable-dind script: - export SP_VERSION=$(echo "$CI_SERVER_VERSION" | sed 's/^\([0-9]*\)\.\([0-9]*\).*/\1-\2-stable/') - - docker run - --env DEP_SCAN_DISABLE_REMOTE_CHECKS="${DEP_SCAN_DISABLE_REMOTE_CHECKS:-false}" - --volume "$PWD:/code" - --volume /var/run/docker.sock:/var/run/docker.sock + - | # this is required to avoid undesirable reset of Docker image ENV variables being set on build stage + function propagate_env_vars() { + CURRENT_ENV=$(printenv) + + for VAR_NAME; do + echo $CURRENT_ENV | grep "${VAR_NAME}=" > /dev/null && echo "--env $VAR_NAME " + done + } + - | + docker run \ + $(propagate_env_vars \ + DS_ANALYZER_IMAGES \ + DS_ANALYZER_IMAGE_PREFIX \ + DS_ANALYZER_IMAGE_TAG \ + DS_DEFAULT_ANALYZERS \ + DEP_SCAN_DISABLE_REMOTE_CHECKS \ + DS_DOCKER_CLIENT_NEGOTIATION_TIMEOUT \ + DS_PULL_ANALYZER_IMAGE_TIMEOUT \ + DS_RUN_ANALYZER_TIMEOUT \ + ) \ + --volume "$PWD:/code" \ + --volume /var/run/docker.sock:/var/run/docker.sock \ "registry.gitlab.com/gitlab-org/security-products/dependency-scanning:$SP_VERSION" /code artifacts: reports: diff --git a/GITALY_SERVER_VERSION b/GITALY_SERVER_VERSION index 39fc130ef85..32b7211cb61 100644 --- a/GITALY_SERVER_VERSION +++ b/GITALY_SERVER_VERSION @@ -1 +1 @@ -1.36.0 +1.40.0 @@ -258,8 +258,7 @@ gem 'chronic_duration', '~> 0.10.6' gem 'webpack-rails', '~> 0.9.10' gem 'rack-proxy', '~> 0.6.0' -gem 'sass-rails', '~> 5.0.6' -gem 'sass', '~> 3.5' +gem 'sassc-rails', '~> 2.1.0' gem 'uglifier', '~> 2.7.2' gem 'addressable', '~> 2.5.2' @@ -417,7 +416,7 @@ group :ed25519 do end # Gitaly GRPC client -gem 'gitaly-proto', '~> 1.26.0', require: 'gitaly' +gem 'gitaly-proto', '~> 1.27.0', require: 'gitaly' gem 'grpc', '~> 1.19.0' diff --git a/Gemfile.lock b/Gemfile.lock index 4db00ba4e18..e02de8292bd 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -283,7 +283,7 @@ GEM gettext_i18n_rails (>= 0.7.1) po_to_json (>= 1.0.0) rails (>= 3.2.0) - gitaly-proto (1.26.0) + gitaly-proto (1.27.0) grpc (~> 1.0) github-markup (1.7.0) gitlab-default_value_for (3.1.1) @@ -822,12 +822,15 @@ GEM sass-listen (4.0.0) rb-fsevent (~> 0.9, >= 0.9.4) rb-inotify (~> 0.9, >= 0.9.7) - sass-rails (5.0.6) - railties (>= 4.0.0, < 6) - sass (~> 3.1) - sprockets (>= 2.8, < 4.0) - sprockets-rails (>= 2.0, < 4.0) - tilt (>= 1.1, < 3) + sassc (2.0.1) + ffi (~> 1.9) + rake + sassc-rails (2.1.0) + railties (>= 4.0.0) + sassc (>= 2.0) + sprockets (> 3.0) + sprockets-rails + tilt sawyer (0.8.1) addressable (>= 2.3.5, < 2.6) faraday (~> 0.8, < 1.0) @@ -1056,7 +1059,7 @@ DEPENDENCIES gettext (~> 3.2.2) gettext_i18n_rails (~> 1.8.0) gettext_i18n_rails_js (~> 1.3) - gitaly-proto (~> 1.26.0) + gitaly-proto (~> 1.27.0) github-markup (~> 1.7.0) gitlab-default_value_for (~> 3.1.1) gitlab-labkit (~> 0.2.0) @@ -1177,8 +1180,7 @@ DEPENDENCIES rubyzip (~> 1.2.2) rugged (~> 0.28) sanitize (~> 4.6) - sass (~> 3.5) - sass-rails (~> 5.0.6) + sassc-rails (~> 2.1.0) scss_lint (~> 0.56.0) seed-fu (~> 2.3.7) selenium-webdriver (~> 3.141) diff --git a/app/assets/javascripts/diffs/components/diff_content.vue b/app/assets/javascripts/diffs/components/diff_content.vue index 8d09c2a7399..2b3d6d1a3fa 100644 --- a/app/assets/javascripts/diffs/components/diff_content.vue +++ b/app/assets/javascripts/diffs/components/diff_content.vue @@ -1,5 +1,6 @@ <script> import { mapActions, mapGetters, mapState } from 'vuex'; +import { GlLoadingIcon } from '@gitlab/ui'; import diffLineNoteFormMixin from 'ee_else_ce/notes/mixins/diff_line_note_form'; import draftCommentsMixin from 'ee_else_ce/diffs/mixins/draft_comments'; import DiffViewer from '~/vue_shared/components/diff_viewer/diff_viewer.vue'; @@ -16,6 +17,7 @@ import { diffViewerModes } from '~/ide/constants'; export default { components: { + GlLoadingIcon, InlineDiffView, ParallelDiffView, DiffViewer, @@ -108,6 +110,7 @@ export default { :diff-lines="diffFile.parallel_diff_lines || []" :help-page-path="helpPagePath" /> + <gl-loading-icon v-if="diffFile.renderingLines" size="md" class="mt-3" /> </template> <not-diffable-viewer v-else-if="notDiffable" /> <no-preview-viewer v-else-if="noPreview" /> 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 6709df48637..1281f9b17ef 100644 --- a/app/assets/javascripts/diffs/components/diff_line_gutter_content.vue +++ b/app/assets/javascripts/diffs/components/diff_line_gutter_content.vue @@ -84,8 +84,6 @@ export default { }, shouldShowCommentButton() { return ( - this.isLoggedIn && - this.showCommentButton && this.isHover && !this.isMatchLine && !this.isContextLine && @@ -102,6 +100,9 @@ export default { } return this.showCommentButton && this.hasDiscussions; }, + shouldRenderCommentButton() { + return this.isLoggedIn && this.showCommentButton; + }, }, methods: { ...mapActions('diffs', ['loadMoreLines', 'showCommentForm', 'setHighlightedRow']), @@ -167,6 +168,7 @@ export default { > <template v-else> <button + v-if="shouldRenderCommentButton" v-show="shouldShowCommentButton" type="button" class="add-diff-note js-add-diff-note-button qa-diff-comment" diff --git a/app/assets/javascripts/diffs/components/diff_table_cell.vue b/app/assets/javascripts/diffs/components/diff_table_cell.vue index d174b13e133..0f3e9208d21 100644 --- a/app/assets/javascripts/diffs/components/diff_table_cell.vue +++ b/app/assets/javascripts/diffs/components/diff_table_cell.vue @@ -89,17 +89,19 @@ export default { classNameMap() { const { type } = this.line; - return { - hll: this.isHighlighted, - [type]: type, - [LINE_UNFOLD_CLASS_NAME]: this.isMatchLine, - [LINE_HOVER_CLASS_NAME]: - this.isLoggedIn && - this.isHover && - !this.isMatchLine && - !this.isContextLine && - !this.isMetaLine, - }; + return [ + type, + { + hll: this.isHighlighted, + [LINE_UNFOLD_CLASS_NAME]: this.isMatchLine, + [LINE_HOVER_CLASS_NAME]: + this.isLoggedIn && + this.isHover && + !this.isMatchLine && + !this.isContextLine && + !this.isMetaLine, + }, + ]; }, lineNumber() { return this.lineType === OLD_LINE_TYPE ? this.line.old_line : this.line.new_line; 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 c764cbeb8e0..2d5262baeec 100644 --- a/app/assets/javascripts/diffs/components/inline_diff_table_row.vue +++ b/app/assets/javascripts/diffs/components/inline_diff_table_row.vue @@ -1,12 +1,11 @@ <script> -import { mapGetters, mapActions, mapState } from 'vuex'; +import { mapActions, mapState } from 'vuex'; import DiffTableCell from './diff_table_cell.vue'; import { NEW_LINE_TYPE, OLD_LINE_TYPE, CONTEXT_LINE_TYPE, CONTEXT_LINE_CLASS_NAME, - PARALLEL_DIFF_VIEW_TYPE, LINE_POSITION_LEFT, LINE_POSITION_RIGHT, } from '../constants'; @@ -45,16 +44,16 @@ export default { return this.line.line_code !== null && this.line.line_code === state.diffs.highlightedRow; }, }), - ...mapGetters('diffs', ['isInlineView']), isContextLine() { return this.line.type === CONTEXT_LINE_TYPE; }, classNameMap() { - return { - [this.line.type]: this.line.type, - [CONTEXT_LINE_CLASS_NAME]: this.isContextLine, - [PARALLEL_DIFF_VIEW_TYPE]: this.isParallelView, - }; + return [ + this.line.type, + { + [CONTEXT_LINE_CLASS_NAME]: this.isContextLine, + }, + ]; }, inlineRowId() { return this.line.line_code || `${this.fileHash}_${this.line.old_line}_${this.line.new_line}`; diff --git a/app/assets/javascripts/diffs/constants.js b/app/assets/javascripts/diffs/constants.js index 4feb73cfef2..d84e1af11f3 100644 --- a/app/assets/javascripts/diffs/constants.js +++ b/app/assets/javascripts/diffs/constants.js @@ -50,3 +50,10 @@ export const LEFT_LINE_KEY = 'left'; export const CENTERED_LIMITED_CONTAINER_CLASSES = 'container-limited limit-container-width mx-lg-auto px-3'; + +export const MAX_RENDERING_DIFF_LINES = 500; +export const MAX_RENDERING_BULK_ROWS = 30; +export const MIN_RENDERING_MS = 2; +export const START_RENDERING_INDEX = 200; +export const INLINE_DIFF_LINES_KEY = 'highlighted_diff_lines'; +export const PARALLEL_DIFF_LINES_KEY = 'parallel_diff_lines'; diff --git a/app/assets/javascripts/diffs/store/actions.js b/app/assets/javascripts/diffs/store/actions.js index b58ae0d248c..386d08aed2b 100644 --- a/app/assets/javascripts/diffs/store/actions.js +++ b/app/assets/javascripts/diffs/store/actions.js @@ -7,7 +7,12 @@ import { handleLocationHash, historyPushState, scrollToElement } from '~/lib/uti import { mergeUrlParams, getLocationHash } from '~/lib/utils/url_utility'; import TreeWorker from '../workers/tree_worker'; import eventHub from '../../notes/event_hub'; -import { getDiffPositionByLineCode, getNoteFormData } from './utils'; +import { + getDiffPositionByLineCode, + getNoteFormData, + convertExpandLines, + idleCallback, +} from './utils'; import * as types from './mutation_types'; import { PARALLEL_DIFF_VIEW_TYPE, @@ -17,6 +22,16 @@ import { TREE_LIST_STORAGE_KEY, WHITESPACE_STORAGE_KEY, TREE_LIST_WIDTH_STORAGE_KEY, + OLD_LINE_KEY, + NEW_LINE_KEY, + TYPE_KEY, + LEFT_LINE_KEY, + MAX_RENDERING_DIFF_LINES, + MAX_RENDERING_BULK_ROWS, + MIN_RENDERING_MS, + START_RENDERING_INDEX, + INLINE_DIFF_LINES_KEY, + PARALLEL_DIFF_LINES_KEY, } from '../constants'; import { diffViewerModes } from '~/ide/constants'; @@ -313,13 +328,98 @@ export const cacheTreeListWidth = (_, size) => { }; export const requestFullDiff = ({ commit }, filePath) => commit(types.REQUEST_FULL_DIFF, filePath); -export const receiveFullDiffSucess = ({ commit }, { filePath, data }) => - commit(types.RECEIVE_FULL_DIFF_SUCCESS, { filePath, data }); +export const receiveFullDiffSucess = ({ commit }, { filePath }) => + commit(types.RECEIVE_FULL_DIFF_SUCCESS, { filePath }); export const receiveFullDiffError = ({ commit }, filePath) => { commit(types.RECEIVE_FULL_DIFF_ERROR, filePath); createFlash(s__('MergeRequest|Error loading full diff. Please try again.')); }; +export const setExpandedDiffLines = ({ commit, state }, { file, data }) => { + const expandedDiffLines = { + highlighted_diff_lines: convertExpandLines({ + diffLines: file.highlighted_diff_lines, + typeKey: TYPE_KEY, + oldLineKey: OLD_LINE_KEY, + newLineKey: NEW_LINE_KEY, + data, + mapLine: ({ line, oldLine, newLine }) => + Object.assign(line, { + old_line: oldLine, + new_line: newLine, + line_code: `${file.file_hash}_${oldLine}_${newLine}`, + }), + }), + parallel_diff_lines: convertExpandLines({ + diffLines: file.parallel_diff_lines, + typeKey: [LEFT_LINE_KEY, TYPE_KEY], + oldLineKey: [LEFT_LINE_KEY, OLD_LINE_KEY], + newLineKey: [LEFT_LINE_KEY, NEW_LINE_KEY], + data, + mapLine: ({ line, oldLine, newLine }) => ({ + left: { + ...line, + old_line: oldLine, + line_code: `${file.file_hash}_${oldLine}_${newLine}`, + }, + right: { + ...line, + new_line: newLine, + line_code: `${file.file_hash}_${newLine}_${oldLine}`, + }, + }), + }), + }; + const currentDiffLinesKey = + state.diffViewType === INLINE_DIFF_VIEW_TYPE ? INLINE_DIFF_LINES_KEY : PARALLEL_DIFF_LINES_KEY; + const hiddenDiffLinesKey = + state.diffViewType === INLINE_DIFF_VIEW_TYPE ? PARALLEL_DIFF_LINES_KEY : INLINE_DIFF_LINES_KEY; + + commit(types.SET_HIDDEN_VIEW_DIFF_FILE_LINES, { + filePath: file.file_path, + lines: expandedDiffLines[hiddenDiffLinesKey], + }); + + if (expandedDiffLines[currentDiffLinesKey].length > MAX_RENDERING_DIFF_LINES) { + let index = START_RENDERING_INDEX; + commit(types.SET_CURRENT_VIEW_DIFF_FILE_LINES, { + filePath: file.file_path, + lines: expandedDiffLines[currentDiffLinesKey].slice(0, index), + }); + commit(types.TOGGLE_DIFF_FILE_RENDERING_MORE, file.file_path); + + const idleCb = t => { + const startIndex = index; + + while ( + t.timeRemaining() >= MIN_RENDERING_MS && + index !== expandedDiffLines[currentDiffLinesKey].length && + index - startIndex !== MAX_RENDERING_BULK_ROWS + ) { + const line = expandedDiffLines[currentDiffLinesKey][index]; + + if (line) { + commit(types.ADD_CURRENT_VIEW_DIFF_FILE_LINES, { filePath: file.file_path, line }); + index += 1; + } + } + + if (index !== expandedDiffLines[currentDiffLinesKey].length) { + idleCallback(idleCb); + } else { + commit(types.TOGGLE_DIFF_FILE_RENDERING_MORE, file.file_path); + } + }; + + idleCallback(idleCb); + } else { + commit(types.SET_CURRENT_VIEW_DIFF_FILE_LINES, { + filePath: file.file_path, + lines: expandedDiffLines[currentDiffLinesKey], + }); + } +}; + export const fetchFullDiff = ({ dispatch }, file) => axios .get(file.context_lines_path, { @@ -328,8 +428,10 @@ export const fetchFullDiff = ({ dispatch }, file) => from_merge_request: true, }, }) - .then(({ data }) => dispatch('receiveFullDiffSucess', { filePath: file.file_path, data })) - .then(() => scrollToElement(`#${file.file_hash}`)) + .then(({ data }) => { + dispatch('receiveFullDiffSucess', { filePath: file.file_path }); + dispatch('setExpandedDiffLines', { file, data }); + }) .catch(() => dispatch('receiveFullDiffError', file.file_path)); export const toggleFullDiff = ({ dispatch, getters, state }, filePath) => { @@ -340,7 +442,6 @@ export const toggleFullDiff = ({ dispatch, getters, state }, filePath) => { if (file.isShowingFullFile) { dispatch('loadCollapsedDiff', file) .then(() => dispatch('assignDiscussionsToDiff', getters.getDiffFileDiscussions(file))) - .then(() => scrollToElement(`#${file.file_hash}`)) .catch(() => dispatch('receiveFullDiffError', filePath)); } else { dispatch('fetchFullDiff', file); diff --git a/app/assets/javascripts/diffs/store/mutation_types.js b/app/assets/javascripts/diffs/store/mutation_types.js index adf56eba3f8..6bb24c97139 100644 --- a/app/assets/javascripts/diffs/store/mutation_types.js +++ b/app/assets/javascripts/diffs/store/mutation_types.js @@ -28,3 +28,8 @@ export const REQUEST_FULL_DIFF = 'REQUEST_FULL_DIFF'; export const RECEIVE_FULL_DIFF_SUCCESS = 'RECEIVE_FULL_DIFF_SUCCESS'; export const RECEIVE_FULL_DIFF_ERROR = 'RECEIVE_FULL_DIFF_ERROR'; export const SET_FILE_COLLAPSED = 'SET_FILE_COLLAPSED'; + +export const SET_HIDDEN_VIEW_DIFF_FILE_LINES = 'SET_HIDDEN_VIEW_DIFF_FILE_LINES'; +export const SET_CURRENT_VIEW_DIFF_FILE_LINES = 'SET_CURRENT_VIEW_DIFF_FILE_LINES'; +export const ADD_CURRENT_VIEW_DIFF_FILE_LINES = 'ADD_CURRENT_VIEW_DIFF_FILE_LINES'; +export const TOGGLE_DIFF_FILE_RENDERING_MORE = 'TOGGLE_DIFF_FILE_RENDERING_MORE'; diff --git a/app/assets/javascripts/diffs/store/mutations.js b/app/assets/javascripts/diffs/store/mutations.js index 572fbfb5be4..67bc1724738 100644 --- a/app/assets/javascripts/diffs/store/mutations.js +++ b/app/assets/javascripts/diffs/store/mutations.js @@ -6,10 +6,8 @@ import { addContextLines, prepareDiffData, isDiscussionApplicableToLine, - convertExpandLines, } from './utils'; import * as types from './mutation_types'; -import { OLD_LINE_KEY, NEW_LINE_KEY, TYPE_KEY, LEFT_LINE_KEY } from '../constants'; export default { [types.SET_BASE_CONFIG](state, options) { @@ -265,45 +263,11 @@ export default { file.isLoadingFullFile = false; }, - [types.RECEIVE_FULL_DIFF_SUCCESS](state, { filePath, data }) { + [types.RECEIVE_FULL_DIFF_SUCCESS](state, { filePath }) { const file = findDiffFile(state.diffFiles, filePath, 'file_path'); file.isShowingFullFile = true; file.isLoadingFullFile = false; - - file.highlighted_diff_lines = convertExpandLines({ - diffLines: file.highlighted_diff_lines, - typeKey: [TYPE_KEY], - oldLineKey: [OLD_LINE_KEY], - newLineKey: [NEW_LINE_KEY], - data, - mapLine: ({ line, oldLine, newLine }) => ({ - ...line, - old_line: oldLine, - new_line: newLine, - line_code: `${file.file_hash}_${oldLine}_${newLine}`, - }), - }); - - file.parallel_diff_lines = convertExpandLines({ - diffLines: file.parallel_diff_lines, - typeKey: [LEFT_LINE_KEY, TYPE_KEY], - oldLineKey: [LEFT_LINE_KEY, OLD_LINE_KEY], - newLineKey: [LEFT_LINE_KEY, NEW_LINE_KEY], - data, - mapLine: ({ line, oldLine, newLine }) => ({ - left: { - ...line, - old_line: oldLine, - line_code: `${file.file_hash}_${oldLine}_${newLine}`, - }, - right: { - ...line, - new_line: newLine, - line_code: `${file.file_hash}_${newLine}_${oldLine}`, - }, - }), - }); }, [types.SET_FILE_COLLAPSED](state, { filePath, collapsed }) { const file = state.diffFiles.find(f => f.file_path === filePath); @@ -312,4 +276,30 @@ export default { file.viewer.collapsed = collapsed; } }, + [types.SET_HIDDEN_VIEW_DIFF_FILE_LINES](state, { filePath, lines }) { + const file = state.diffFiles.find(f => f.file_path === filePath); + const hiddenDiffLinesKey = + state.diffViewType === 'inline' ? 'parallel_diff_lines' : 'highlighted_diff_lines'; + + file[hiddenDiffLinesKey] = lines; + }, + [types.SET_CURRENT_VIEW_DIFF_FILE_LINES](state, { filePath, lines }) { + const file = state.diffFiles.find(f => f.file_path === filePath); + const currentDiffLinesKey = + state.diffViewType === 'inline' ? 'highlighted_diff_lines' : 'parallel_diff_lines'; + + file[currentDiffLinesKey] = lines; + }, + [types.ADD_CURRENT_VIEW_DIFF_FILE_LINES](state, { filePath, line }) { + const file = state.diffFiles.find(f => f.file_path === filePath); + const currentDiffLinesKey = + state.diffViewType === 'inline' ? 'highlighted_diff_lines' : 'parallel_diff_lines'; + + file[currentDiffLinesKey].push(line); + }, + [types.TOGGLE_DIFF_FILE_RENDERING_MORE](state, filePath) { + const file = state.diffFiles.find(f => f.file_path === filePath); + + file.renderingLines = !file.renderingLines; + }, }; diff --git a/app/assets/javascripts/diffs/store/utils.js b/app/assets/javascripts/diffs/store/utils.js index 27a79369a24..71956255eef 100644 --- a/app/assets/javascripts/diffs/store/utils.js +++ b/app/assets/javascripts/diffs/store/utils.js @@ -253,6 +253,7 @@ export function prepareDiffData(diffData) { isShowingFullFile: false, isLoadingFullFile: false, discussions: [], + renderingLines: false, }); } } @@ -423,27 +424,33 @@ export const convertExpandLines = ({ mapLine, }) => { const dataLength = data.length; + const lines = []; + + for (let i = 0, diffLinesLength = diffLines.length; i < diffLinesLength; i += 1) { + const line = diffLines[i]; - return diffLines.reduce((acc, line, i) => { if (_.property(typeKey)(line) === 'match') { const beforeLine = diffLines[i - 1]; const afterLine = diffLines[i + 1]; - const beforeLineIndex = _.property(newLineKey)(beforeLine) || 0; - const afterLineIndex = _.property(newLineKey)(afterLine) - 1 || dataLength; - - acc.push( - ...data.slice(beforeLineIndex, afterLineIndex).map((l, index) => ({ - ...mapLine({ - line: { ...l, hasForm: false, discussions: [] }, + const newLineProperty = _.property(newLineKey); + const beforeLineIndex = newLineProperty(beforeLine) || 0; + const afterLineIndex = newLineProperty(afterLine) - 1 || dataLength; + + lines.push( + ...data.slice(beforeLineIndex, afterLineIndex).map((l, index) => + mapLine({ + line: Object.assign(l, { hasForm: false, discussions: [] }), oldLine: (_.property(oldLineKey)(beforeLine) || 0) + index + 1, - newLine: (_.property(newLineKey)(beforeLine) || 0) + index + 1, + newLine: (newLineProperty(beforeLine) || 0) + index + 1, }), - })), + ), ); } else { - acc.push(line); + lines.push(line); } + } - return acc; - }, []); + return lines; }; + +export const idleCallback = cb => requestIdleCallback(cb); diff --git a/app/assets/javascripts/notes/components/note_form.vue b/app/assets/javascripts/notes/components/note_form.vue index fb098095cf3..acbb91ce7be 100644 --- a/app/assets/javascripts/notes/components/note_form.vue +++ b/app/assets/javascripts/notes/components/note_form.vue @@ -8,6 +8,7 @@ import issuableStateMixin from '../mixins/issuable_state'; import resolvable from '../mixins/resolvable'; import { __ } from '~/locale'; import { getDraft, updateDraft } from '~/lib/utils/autosave'; +import noteFormMixin from 'ee_else_ce/notes/mixins/note_form'; export default { name: 'NoteForm', @@ -15,7 +16,7 @@ export default { issueWarning, markdownField, }, - mixins: [issuableStateMixin, resolvable], + mixins: [issuableStateMixin, resolvable, noteFormMixin], props: { noteBody: { type: String, @@ -195,21 +196,6 @@ export default { return shouldResolve || shouldToggleState; }, - handleKeySubmit() { - this.handleUpdate(); - }, - handleUpdate(shouldResolve) { - const beforeSubmitDiscussionState = this.discussionResolved; - this.isSubmitting = true; - - this.$emit('handleFormUpdate', this.updatedNoteBody, this.$refs.editNoteForm, () => { - this.isSubmitting = false; - - if (this.shouldToggleResolved(shouldResolve, beforeSubmitDiscussionState)) { - this.resolveHandler(beforeSubmitDiscussionState); - } - }); - }, editMyLastNote() { if (this.updatedNoteBody === '') { const lastNoteInDiscussion = this.getDiscussionLastNote(this.discussion); @@ -279,28 +265,74 @@ export default { ></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 qa-reply-comment-button" - @click="handleUpdate()" - > - {{ saveButtonTitle }} - </button> - <button - v-if="discussion.resolvable" - class="btn btn-nr btn-default append-right-10 js-comment-resolve-button" - @click.prevent="handleUpdate(true)" - > - {{ resolveButtonTitle }} - </button> - <button - class="btn btn-cancel note-edit-cancel js-close-discussion-note-form" - type="button" - @click="cancelHandler()" - > - Cancel - </button> + <template v-if="showBatchCommentsActions"> + <p v-if="showResolveDiscussionToggle"> + <label> + <template v-if="discussionResolved"> + <input + v-model="isUnresolving" + type="checkbox" + class="qa-unresolve-review-discussion" + /> + {{ __('Unresolve discussion') }} + </template> + <template v-else> + <input v-model="isResolving" type="checkbox" class="qa-resolve-review-discussion" /> + {{ __('Resolve discussion') }} + </template> + </label> + </p> + <div> + <button + :disabled="isDisabled" + type="button" + class="btn btn-success qa-start-review" + @click="handleAddToReview" + > + <template v-if="hasDrafts">{{ __('Add to review') }}</template> + <template v-else>{{ __('Start a review') }}</template> + </button> + <button + :disabled="isDisabled" + type="button" + class="btn qa-comment-now" + @click="handleUpdate()" + > + {{ __('Add comment now') }} + </button> + <button + class="btn btn-cancel note-edit-cancel js-close-discussion-note-form" + type="button" + @click="cancelHandler()" + > + {{ __('Cancel') }} + </button> + </div> + </template> + <template v-else> + <button + :disabled="isDisabled" + type="button" + class="js-vue-issue-save btn btn-success js-comment-button qa-reply-comment-button" + @click="handleUpdate()" + > + {{ saveButtonTitle }} + </button> + <button + v-if="discussion.resolvable" + class="btn btn-nr btn-default append-right-10 js-comment-resolve-button" + @click.prevent="handleUpdate(true)" + > + {{ resolveButtonTitle }} + </button> + <button + class="btn btn-cancel note-edit-cancel js-close-discussion-note-form" + type="button" + @click="cancelHandler()" + > + Cancel + </button> + </template> </div> </form> </div> diff --git a/app/assets/javascripts/notes/mixins/note_form.js b/app/assets/javascripts/notes/mixins/note_form.js new file mode 100644 index 00000000000..b74879f2256 --- /dev/null +++ b/app/assets/javascripts/notes/mixins/note_form.js @@ -0,0 +1,24 @@ +export default { + data() { + return { + showBatchCommentsActions: false, + }; + }, + methods: { + handleKeySubmit() { + this.handleUpdate(); + }, + handleUpdate(shouldResolve) { + const beforeSubmitDiscussionState = this.discussionResolved; + this.isSubmitting = true; + + this.$emit('handleFormUpdate', this.updatedNoteBody, this.$refs.editNoteForm, () => { + this.isSubmitting = false; + + if (this.shouldToggleResolved(shouldResolve, beforeSubmitDiscussionState)) { + this.resolveHandler(beforeSubmitDiscussionState); + } + }); + }, + }, +}; diff --git a/app/assets/javascripts/pipelines/components/graph/action_component.vue b/app/assets/javascripts/pipelines/components/graph/action_component.vue index 8ca539351a7..3c85bb61ce8 100644 --- a/app/assets/javascripts/pipelines/components/graph/action_component.vue +++ b/app/assets/javascripts/pipelines/components/graph/action_component.vue @@ -1,5 +1,5 @@ <script> -import { GlTooltipDirective, GlButton } from '@gitlab/ui'; +import { GlTooltipDirective, GlButton, GlLoadingIcon } from '@gitlab/ui'; import axios from '~/lib/utils/axios_utils'; import { dasherize } from '~/lib/utils/text_utility'; import { __ } from '~/locale'; @@ -20,6 +20,7 @@ export default { components: { Icon, GlButton, + GlLoadingIcon, }, directives: { GlTooltip: GlTooltipDirective, @@ -41,6 +42,7 @@ export default { data() { return { isDisabled: false, + isLoading: false, }; }, computed: { @@ -59,15 +61,19 @@ export default { onClickAction() { this.$root.$emit('bv::hide::tooltip', `js-ci-action-${this.link}`); this.isDisabled = true; + this.isLoading = true; axios .post(`${this.link}.json`) .then(() => { this.isDisabled = false; + this.isLoading = false; + this.$emit('pipelineActionRequestComplete'); }) .catch(() => { this.isDisabled = false; + this.isLoading = false; createFlash(__('An error occurred while making the request.')); }); @@ -82,10 +88,10 @@ export default { :title="tooltipText" :class="cssClass" :disabled="isDisabled" - class="js-ci-action btn btn-blank -btn-transparent ci-action-icon-container ci-action-icon-wrapper" + class="js-ci-action btn btn-blank btn-transparent ci-action-icon-container ci-action-icon-wrapper" @click="onClickAction" > - <icon :name="actionIcon" /> + <gl-loading-icon v-if="isLoading" class="js-action-icon-loading" /> + <icon v-else :name="actionIcon" /> </gl-button> </template> diff --git a/app/assets/javascripts/pipelines/components/graph/graph_component.vue b/app/assets/javascripts/pipelines/components/graph/graph_component.vue index a49dc311bd0..ba0dea626dc 100644 --- a/app/assets/javascripts/pipelines/components/graph/graph_component.vue +++ b/app/assets/javascripts/pipelines/components/graph/graph_component.vue @@ -24,6 +24,7 @@ export default { :groups="stage.groups" :stage-connector-class="stageConnectorClass(index, stage)" :is-first-column="isFirstColumn(index)" + :action="stage.status.action" @refreshPipelineGraph="refreshPipelineGraph" /> </ul> diff --git a/app/assets/javascripts/pipelines/components/graph/stage_column_component.vue b/app/assets/javascripts/pipelines/components/graph/stage_column_component.vue index 348c407f1b5..7e611b93087 100644 --- a/app/assets/javascripts/pipelines/components/graph/stage_column_component.vue +++ b/app/assets/javascripts/pipelines/components/graph/stage_column_component.vue @@ -3,11 +3,13 @@ import _ from 'underscore'; import stageColumnMixin from 'ee_else_ce/pipelines/mixins/stage_column_mixin'; import JobItem from './job_item.vue'; import JobGroupDropdown from './job_group_dropdown.vue'; +import ActionComponent from './action_component.vue'; export default { components: { JobItem, JobGroupDropdown, + ActionComponent, }, mixins: [stageColumnMixin], props: { @@ -29,6 +31,16 @@ export default { required: false, default: '', }, + action: { + type: Object, + required: false, + default: () => ({}), + }, + }, + computed: { + hasAction() { + return !_.isEmpty(this.action); + }, }, methods: { groupId(group) { @@ -42,7 +54,18 @@ export default { </script> <template> <li :class="stageConnectorClass" class="stage-column"> - <div class="stage-name">{{ title }}</div> + <div class="stage-name position-relative"> + {{ title }} + <action-component + v-if="hasAction" + :action-icon="action.icon" + :tooltip-text="action.title" + :link="action.path" + class="js-stage-action position-absolute position-top-0 rounded" + @pipelineActionRequestComplete="pipelineActionRequestComplete" + /> + </div> + <div class="builds-container"> <ul> <li diff --git a/app/assets/javascripts/releases/components/release_block.vue b/app/assets/javascripts/releases/components/release_block.vue index 7ed1b407ddd..0958b9fa926 100644 --- a/app/assets/javascripts/releases/components/release_block.vue +++ b/app/assets/javascripts/releases/components/release_block.vue @@ -86,7 +86,7 @@ export default { </div> <div - v-if="assets.links.length || assets.sources.length" + v-if="assets.links.length || (assets.sources && assets.sources.length)" class="card-text prepend-top-default" > <b> @@ -103,7 +103,7 @@ export default { </li> </ul> - <div v-if="assets.sources.length" class="dropdown"> + <div v-if="assets.sources && assets.sources.length" class="dropdown"> <button type="button" class="btn btn-link" diff --git a/app/assets/javascripts/reports/store/utils.js b/app/assets/javascripts/reports/store/utils.js index 35632218269..10560d0ae8e 100644 --- a/app/assets/javascripts/reports/store/utils.js +++ b/app/assets/javascripts/reports/store/utils.js @@ -1,4 +1,4 @@ -import { sprintf, n__, s__ } from '~/locale'; +import { sprintf, n__, s__, __ } from '~/locale'; import { STATUS_FAILED, STATUS_SUCCESS, @@ -38,12 +38,12 @@ const textBuilder = results => { export const summaryTextBuilder = (name = '', results = {}) => { const resultsString = textBuilder(results); - return `${name} contained ${resultsString}`; + return sprintf(__('%{name} contained %{resultsString}'), { name, resultsString }); }; export const reportTextBuilder = (name = '', results = {}) => { const resultsString = textBuilder(results); - return `${name} found ${resultsString}`; + return sprintf(__('%{name} found %{resultsString}'), { name, resultsString }); }; export const statusIcon = status => { diff --git a/app/assets/javascripts/sidebar/lib/sidebar_move_issue.js b/app/assets/javascripts/sidebar/lib/sidebar_move_issue.js index 225ebb61195..110175a6779 100644 --- a/app/assets/javascripts/sidebar/lib/sidebar_move_issue.js +++ b/app/assets/javascripts/sidebar/lib/sidebar_move_issue.js @@ -1,5 +1,6 @@ import $ from 'jquery'; import _ from 'underscore'; +import { __ } from '~/locale'; function isValidProjectId(id) { return id > 0; @@ -40,7 +41,9 @@ class SidebarMoveIssue { this.mediator .fetchAutocompleteProjects(searchTerm) .then(callback) - .catch(() => new window.Flash('An error occurred while fetching projects autocomplete.')); + .catch( + () => new window.Flash(__('An error occurred while fetching projects autocomplete.')), + ); }, renderRow: project => ` <li> @@ -72,7 +75,7 @@ class SidebarMoveIssue { this.$confirmButton.disable().addClass('is-loading'); this.mediator.moveIssue().catch(() => { - window.Flash('An error occurred while moving the issue.'); + window.Flash(__('An error occurred while moving the issue.')); this.$confirmButton.enable().removeClass('is-loading'); }); } diff --git a/app/assets/javascripts/sidebar/sidebar_mediator.js b/app/assets/javascripts/sidebar/sidebar_mediator.js index 3e040ec8428..22ac8df9699 100644 --- a/app/assets/javascripts/sidebar/sidebar_mediator.js +++ b/app/assets/javascripts/sidebar/sidebar_mediator.js @@ -2,6 +2,7 @@ import { visitUrl } from '../lib/utils/url_utility'; import Flash from '../flash'; import Service from './services/sidebar_service'; import Store from './stores/sidebar_store'; +import { __ } from '~/locale'; export default class SidebarMediator { constructor(options) { @@ -45,7 +46,7 @@ export default class SidebarMediator { .then(data => { this.processFetchedData(data); }) - .catch(() => new Flash('Error occurred when fetching sidebar data')); + .catch(() => new Flash(__('Error occurred when fetching sidebar data'))); } processFetchedData(data) { diff --git a/app/assets/javascripts/snippet/snippet_embed.js b/app/assets/javascripts/snippet/snippet_embed.js index 873a506a92f..fe08d2c7ebb 100644 --- a/app/assets/javascripts/snippet/snippet_embed.js +++ b/app/assets/javascripts/snippet/snippet_embed.js @@ -1,3 +1,5 @@ +import { __ } from '~/locale'; + export default () => { const { protocol, host, pathname } = window.location; const shareBtn = document.querySelector('.js-share-btn'); @@ -10,7 +12,7 @@ export default () => { shareBtn.classList.add('is-active'); embedBtn.classList.remove('is-active'); snippetUrlArea.value = url; - embedAction.innerText = 'Share'; + embedAction.innerText = __('Share'); }); embedBtn.addEventListener('click', () => { @@ -18,6 +20,6 @@ export default () => { shareBtn.classList.remove('is-active'); const scriptTag = `<script src="${url}.js"></script>`; snippetUrlArea.value = scriptTag; - embedAction.innerText = 'Embed'; + embedAction.innerText = __('Embed'); }); }; diff --git a/app/assets/stylesheets/pages/pipelines.scss b/app/assets/stylesheets/pages/pipelines.scss index 093fc89a56f..96fdf74267e 100644 --- a/app/assets/stylesheets/pages/pipelines.scss +++ b/app/assets/stylesheets/pages/pipelines.scss @@ -565,6 +565,7 @@ white-space: nowrap; overflow: hidden; text-overflow: ellipsis; + line-height: 2.2em; } .build { diff --git a/app/controllers/clusters/clusters_controller.rb b/app/controllers/clusters/clusters_controller.rb index edaf07063ec..73ebd4e0e42 100644 --- a/app/controllers/clusters/clusters_controller.rb +++ b/app/controllers/clusters/clusters_controller.rb @@ -156,6 +156,7 @@ class Clusters::ClustersController < Clusters::BaseController :enabled, :name, :environment_scope, + :managed, provider_gcp_attributes: [ :gcp_project_id, :zone, @@ -174,6 +175,7 @@ class Clusters::ClustersController < Clusters::BaseController :enabled, :name, :environment_scope, + :managed, platform_kubernetes_attributes: [ :namespace, :api_url, diff --git a/app/controllers/projects/stages_controller.rb b/app/controllers/projects/stages_controller.rb new file mode 100644 index 00000000000..c8db5b1277f --- /dev/null +++ b/app/controllers/projects/stages_controller.rb @@ -0,0 +1,25 @@ +# frozen_string_literal: true + +class Projects::StagesController < Projects::PipelinesController + before_action :authorize_update_pipeline! + + def play_manual + ::Ci::PlayManualStageService + .new(@project, current_user, pipeline: pipeline) + .execute(stage) + + respond_to do |format| + format.json do + render json: StageSerializer + .new(project: @project, current_user: @current_user) + .represent(stage) + end + end + end + + private + + def stage + @pipeline_stage ||= pipeline.find_stage_by_name!(params[:stage_name]) + end +end diff --git a/app/controllers/uploads_controller.rb b/app/controllers/uploads_controller.rb index 568c6e2a852..060b09f015c 100644 --- a/app/controllers/uploads_controller.rb +++ b/app/controllers/uploads_controller.rb @@ -56,8 +56,9 @@ class UploadsController < ApplicationController def authorize_create_access! return unless model - # for now we support only personal snippets comments - authorized = can?(current_user, :comment_personal_snippet, model) + # for now we support only personal snippets comments. Only personal_snippet + # is allowed as a model to #create through routing. + authorized = can?(current_user, :create_note, model) render_unauthorized unless authorized end diff --git a/app/helpers/notes_helper.rb b/app/helpers/notes_helper.rb index a50137bea3d..2e31a5e2ed4 100644 --- a/app/helpers/notes_helper.rb +++ b/app/helpers/notes_helper.rb @@ -128,15 +128,9 @@ module NotesHelper end def can_create_note? - issuable = @issue || @merge_request + noteable = @issue || @merge_request || @snippet || @project - if @snippet.is_a?(PersonalSnippet) - can?(current_user, :comment_personal_snippet, @snippet) - elsif issuable - can?(current_user, :create_note, issuable) - else - can?(current_user, :create_note, @project) - end + can?(current_user, :create_note, noteable) end def initial_notes_data(autocomplete) diff --git a/app/helpers/projects_helper.rb b/app/helpers/projects_helper.rb index 2ac90eb8d9f..8977ccaa9d8 100644 --- a/app/helpers/projects_helper.rb +++ b/app/helpers/projects_helper.rb @@ -318,8 +318,9 @@ module ProjectsHelper def get_project_nav_tabs(project, current_user) nav_tabs = [:home] - if !project.empty_repo? && can?(current_user, :download_code, project) - nav_tabs << [:files, :commits, :network, :graphs, :forks, :releases] + unless project.empty_repo? + nav_tabs << [:files, :commits, :network, :graphs, :forks] if can?(current_user, :download_code, project) + nav_tabs << :releases if can?(current_user, :read_release, project) end if project.repo_exists? && can?(current_user, :read_merge_request, project) diff --git a/app/helpers/storage_helper.rb b/app/helpers/storage_helper.rb index be8761db562..15041bd5805 100644 --- a/app/helpers/storage_helper.rb +++ b/app/helpers/storage_helper.rb @@ -6,4 +6,14 @@ module StorageHelper number_to_human_size(size_in_bytes, delimiter: ',', precision: precision, significant: false) end + + def storage_counters_details(statistics) + counters = { + counter_repositories: storage_counter(statistics.repository_size), + counter_build_artifacts: storage_counter(statistics.build_artifacts_size), + counter_lfs_objects: storage_counter(statistics.lfs_objects_size) + } + + _("%{counter_repositories} repositories, %{counter_build_artifacts} build artifacts, %{counter_lfs_objects} LFS") % counters + end end diff --git a/app/models/ci/legacy_stage.rb b/app/models/ci/legacy_stage.rb index 96dbc7b6895..930c8a71453 100644 --- a/app/models/ci/legacy_stage.rb +++ b/app/models/ci/legacy_stage.rb @@ -58,5 +58,9 @@ module Ci statuses.latest.failed_but_allowed.any? end end + + def manual_playable? + %[manual scheduled skipped].include?(status.to_s) + end end end diff --git a/app/models/ci/pipeline.rb b/app/models/ci/pipeline.rb index 2b7835d7fab..80401ca0a1e 100644 --- a/app/models/ci/pipeline.rb +++ b/app/models/ci/pipeline.rb @@ -771,6 +771,10 @@ module Ci Gitlab::Utils.slugify(source_ref.to_s) end + def find_stage_by_name!(name) + stages.find_by!(name: name) + end + private def ci_yaml_from_repo diff --git a/app/models/ci/stage.rb b/app/models/ci/stage.rb index b25b0369666..d90339d90dc 100644 --- a/app/models/ci/stage.rb +++ b/app/models/ci/stage.rb @@ -120,5 +120,9 @@ module Ci .new(self, current_user) .fabricate! end + + def manual_playable? + blocked? || skipped? + end end end diff --git a/app/models/clusters/cluster.rb b/app/models/clusters/cluster.rb index 4262c03498d..f6d2082d257 100644 --- a/app/models/clusters/cluster.rb +++ b/app/models/clusters/cluster.rb @@ -94,6 +94,7 @@ module Clusters scope :user_provided, -> { where(provider_type: ::Clusters::Cluster.provider_types[:user]) } scope :gcp_provided, -> { where(provider_type: ::Clusters::Cluster.provider_types[:gcp]) } scope :gcp_installed, -> { gcp_provided.includes(:provider_gcp).where(cluster_providers_gcp: { status: ::Clusters::Providers::Gcp.state_machines[:status].states[:created].value }) } + scope :managed, -> { where(managed: true) } scope :default_environment, -> { where(environment_scope: DEFAULT_ENVIRONMENT) } diff --git a/app/models/clusters/platforms/kubernetes.rb b/app/models/clusters/platforms/kubernetes.rb index ca7d109d4f0..3b7b93e7631 100644 --- a/app/models/clusters/platforms/kubernetes.rb +++ b/app/models/clusters/platforms/kubernetes.rb @@ -92,11 +92,12 @@ module Clusters if kubernetes_namespace = cluster.kubernetes_namespaces.has_service_account_token.find_by(project: project) variables.concat(kubernetes_namespace.predefined_variables) - elsif cluster.project_type? - # From 11.5, every Clusters::Project should have at least one - # Clusters::KubernetesNamespace, so once migration has been completed, - # this 'else' branch will be removed. For more information, please see - # https://gitlab.com/gitlab-org/gitlab-ce/merge_requests/22433 + elsif cluster.project_type? || !cluster.managed? + # As of 11.11 a user can create a cluster that they manage themselves, + # which replicates the existing project-level cluster behaviour. + # Once we have marked all project-level clusters that make use of this + # behaviour as "unmanaged", we can remove the `cluster.project_type?` + # check here. variables .append(key: 'KUBE_URL', value: api_url) .append(key: 'KUBE_TOKEN', value: token, public: false, masked: true) diff --git a/app/models/commit_status.rb b/app/models/commit_status.rb index f97dc38dab7..be6f3e9c5b0 100644 --- a/app/models/commit_status.rb +++ b/app/models/commit_status.rb @@ -140,7 +140,7 @@ class CommitStatus < ApplicationRecord end def locking_enabled? - status_changed? + will_save_change_to_status? end def before_sha diff --git a/app/models/concerns/issuable.rb b/app/models/concerns/issuable.rb index 775aaa44e5b..127430cc68f 100644 --- a/app/models/concerns/issuable.rb +++ b/app/models/concerns/issuable.rb @@ -117,7 +117,7 @@ module Issuable # We want to use optimistic lock for cases when only title or description are involved # http://api.rubyonrails.org/classes/ActiveRecord/Locking/Optimistic.html def locking_enabled? - title_changed? || description_changed? + will_save_change_to_title? || will_save_change_to_description? end def allows_multiple_assignees? diff --git a/app/models/concerns/storage/legacy_namespace.rb b/app/models/concerns/storage/legacy_namespace.rb index 1cbe27ad03a..a15dc19e07a 100644 --- a/app/models/concerns/storage/legacy_namespace.rb +++ b/app/models/concerns/storage/legacy_namespace.rb @@ -13,20 +13,20 @@ module Storage raise Gitlab::UpdatePathError.new("Namespace #{name} (#{id}) cannot be moved because at least one project (e.g. #{proj_with_tags.name} (#{proj_with_tags.id})) has tags in container registry") end - parent_was = if parent_changed? && parent_id_before_last_save.present? + parent_was = if saved_change_to_parent? && parent_id_before_last_save.present? Namespace.find(parent_id_before_last_save) # raise NotFound early if needed end move_repositories - if parent_changed? + if saved_change_to_parent? former_parent_full_path = parent_was&.full_path parent_full_path = parent&.full_path Gitlab::UploadsTransfer.new.move_namespace(path, former_parent_full_path, parent_full_path) Gitlab::PagesTransfer.new.move_namespace(path, former_parent_full_path, parent_full_path) else - Gitlab::UploadsTransfer.new.rename_namespace(full_path_was, full_path) - Gitlab::PagesTransfer.new.rename_namespace(full_path_was, full_path) + Gitlab::UploadsTransfer.new.rename_namespace(full_path_before_last_save, full_path) + Gitlab::PagesTransfer.new.rename_namespace(full_path_before_last_save, full_path) end # If repositories moved successfully we need to @@ -38,7 +38,7 @@ module Storage write_projects_repository_config rescue => e # Raise if development/test environment, else just notify Sentry - Gitlab::Sentry.track_exception(e, extra: { full_path_was: full_path_was, full_path: full_path, action: 'move_dir' }) + Gitlab::Sentry.track_exception(e, extra: { full_path_before_last_save: full_path_before_last_save, full_path: full_path, action: 'move_dir' }) end true # false would cancel later callbacks but not rollback @@ -57,14 +57,14 @@ module Storage # Move the namespace directory in all storages used by member projects repository_storages.each do |repository_storage| # Ensure old directory exists before moving it - gitlab_shell.add_namespace(repository_storage, full_path_was) + gitlab_shell.add_namespace(repository_storage, full_path_before_last_save) # Ensure new directory exists before moving it (if there's a parent) gitlab_shell.add_namespace(repository_storage, parent.full_path) if parent - unless gitlab_shell.mv_namespace(repository_storage, full_path_was, full_path) + unless gitlab_shell.mv_namespace(repository_storage, full_path_before_last_save, full_path) - Rails.logger.error "Exception moving path #{repository_storage} from #{full_path_was} to #{full_path}" + Rails.logger.error "Exception moving path #{repository_storage} from #{full_path_before_last_save} to #{full_path}" # if we cannot move namespace directory we should rollback # db changes in order to prevent out of sync between db and fs diff --git a/app/models/merge_request_diff.rb b/app/models/merge_request_diff.rb index 0b787217410..5f5d92bc2f0 100644 --- a/app/models/merge_request_diff.rb +++ b/app/models/merge_request_diff.rb @@ -134,7 +134,7 @@ class MergeRequestDiff < ApplicationRecord # It allows you to override variables like head_commit_sha before getting diff. after_create :save_git_content, unless: :importing? - after_save :update_external_diff_store, if: -> { !importing? && external_diff_changed? } + after_save :update_external_diff_store, if: -> { !importing? && saved_change_to_external_diff? } def self.find_by_diff_refs(diff_refs) find_by(start_commit_sha: diff_refs.start_sha, head_commit_sha: diff_refs.head_sha, base_commit_sha: diff_refs.base_sha) @@ -154,7 +154,14 @@ class MergeRequestDiff < ApplicationRecord ensure_commit_shas save_commits save_diffs + + # Another set of `after_save` hooks will be called here when we update the record save + # We need to reset so that dirty tracking is reset when running the original set + # of `after_save` hooks that come after this `after_create` hook. Otherwise, the + # hooks that run when an attribute was changed are run twice. + reset + keep_around_commits end @@ -348,7 +355,7 @@ class MergeRequestDiff < ApplicationRecord has_attribute?(:external_diff_store) end - def external_diff_changed? + def saved_change_to_external_diff? super if has_attribute?(:external_diff) end diff --git a/app/models/namespace.rb b/app/models/namespace.rb index 7228aab2c2e..7393ef4b05c 100644 --- a/app/models/namespace.rb +++ b/app/models/namespace.rb @@ -63,7 +63,7 @@ class Namespace < ApplicationRecord # Legacy Storage specific hooks - after_update :move_dir, if: :path_or_parent_changed? + after_update :move_dir, if: :saved_change_to_path_or_parent? before_destroy(prepend: true) { prepare_for_destroy } after_destroy :rm_dir @@ -77,7 +77,8 @@ class Namespace < ApplicationRecord 'COALESCE(SUM(ps.storage_size), 0) AS storage_size', 'COALESCE(SUM(ps.repository_size), 0) AS repository_size', 'COALESCE(SUM(ps.lfs_objects_size), 0) AS lfs_objects_size', - 'COALESCE(SUM(ps.build_artifacts_size), 0) AS build_artifacts_size' + 'COALESCE(SUM(ps.build_artifacts_size), 0) AS build_artifacts_size', + 'COALESCE(SUM(ps.packages_size), 0) AS packages_size' ) end @@ -144,7 +145,7 @@ class Namespace < ApplicationRecord def send_update_instructions projects.each do |project| - project.send_move_instructions("#{full_path_was}/#{project.path}") + project.send_move_instructions("#{full_path_before_last_save}/#{project.path}") end end @@ -229,10 +230,6 @@ class Namespace < ApplicationRecord [owner_id] end - def parent_changed? - parent_id_changed? - end - # Includes projects from this namespace and projects from all subgroups # that belongs to this namespace def all_projects @@ -262,12 +259,12 @@ class Namespace < ApplicationRecord false end - def full_path_was - if parent_id_was.nil? - path_was + def full_path_before_last_save + if parent_id_before_last_save.nil? + path_before_last_save else - previous_parent = Group.find_by(id: parent_id_was) - previous_parent.full_path + '/' + path_was + previous_parent = Group.find_by(id: parent_id_before_last_save) + previous_parent.full_path + '/' + path_before_last_save end end @@ -293,7 +290,15 @@ class Namespace < ApplicationRecord private - def path_or_parent_changed? + def parent_changed? + parent_id_changed? + end + + def saved_change_to_parent? + saved_change_to_parent_id? + end + + def saved_change_to_path_or_parent? saved_change_to_path? || saved_change_to_parent_id? end diff --git a/app/models/pages_domain.rb b/app/models/pages_domain.rb index 9e806b2e232..407d85b1520 100644 --- a/app/models/pages_domain.rb +++ b/app/models/pages_domain.rb @@ -26,7 +26,7 @@ class PagesDomain < ApplicationRecord after_initialize :set_verification_code after_create :update_daemon - after_update :update_daemon, if: :pages_config_changed? + after_update :update_daemon, if: :saved_change_to_pages_config? after_destroy :update_daemon scope :enabled, -> { where('enabled_until >= ?', Time.now ) } @@ -148,7 +148,7 @@ class PagesDomain < ApplicationRecord end # rubocop: enable CodeReuse/ServiceClass - def pages_config_changed? + def saved_change_to_pages_config? saved_change_to_project_id? || saved_change_to_domain? || saved_change_to_certificate? || diff --git a/app/models/project.rb b/app/models/project.rb index a29100405f9..228ab9e9618 100644 --- a/app/models/project.rb +++ b/app/models/project.rb @@ -56,6 +56,7 @@ class Project < ApplicationRecord VALID_MIRROR_PROTOCOLS = %w(http https ssh git).freeze ignore_column :import_status, :import_jid, :import_error + ignore_column :ci_id cache_markdown_field :description, pipeline: :description @@ -1912,8 +1913,8 @@ class Project < ApplicationRecord false end - def full_path_was - File.join(namespace.full_path, previous_changes['path'].first) + def full_path_before_last_save + File.join(namespace.full_path, path_before_last_save) end alias_method :name_with_namespace, :full_name diff --git a/app/models/project_services/chat_message/deployment_message.rb b/app/models/project_services/chat_message/deployment_message.rb index 656a3e6ab4b..dae3a56116e 100644 --- a/app/models/project_services/chat_message/deployment_message.rb +++ b/app/models/project_services/chat_message/deployment_message.rb @@ -2,27 +2,31 @@ module ChatMessage class DeploymentMessage < BaseMessage + attr_reader :commit_title attr_reader :commit_url attr_reader :deployable_id attr_reader :deployable_url attr_reader :environment attr_reader :short_sha attr_reader :status + attr_reader :user_url def initialize(data) super + @commit_title = data[:commit_title] @commit_url = data[:commit_url] @deployable_id = data[:deployable_id] @deployable_url = data[:deployable_url] @environment = data[:environment] @short_sha = data[:short_sha] @status = data[:status] + @user_url = data[:user_url] end def attachments [{ - text: "#{project_link}\n#{deployment_link}, SHA #{commit_link}, by #{user_combined_name}", + text: "#{project_link} with job #{deployment_link} by #{user_link}\n#{commit_link}: #{commit_title}", color: color }] end @@ -55,7 +59,11 @@ module ChatMessage end def deployment_link - link("Job ##{deployable_id}", deployable_url) + link("##{deployable_id}", deployable_url) + end + + def user_link + link(user_combined_name, user_url) end def commit_link diff --git a/app/models/project_statistics.rb b/app/models/project_statistics.rb index c020e72908c..6fe8cb40d25 100644 --- a/app/models/project_statistics.rb +++ b/app/models/project_statistics.rb @@ -7,7 +7,7 @@ class ProjectStatistics < ApplicationRecord before_save :update_storage_size COLUMNS_TO_REFRESH = [:repository_size, :lfs_objects_size, :commit_count].freeze - INCREMENTABLE_COLUMNS = { build_artifacts_size: %i[storage_size] }.freeze + INCREMENTABLE_COLUMNS = { build_artifacts_size: %i[storage_size], packages_size: %i[storage_size] }.freeze def total_repository_size repository_size + lfs_objects_size @@ -36,8 +36,13 @@ class ProjectStatistics < ApplicationRecord self.lfs_objects_size = project.lfs_objects.sum(:size) end + # older migrations fail due to non-existent attribute without this + def packages_size + has_attribute?(:packages_size) ? super.to_i : 0 + end + def update_storage_size - self.storage_size = repository_size + lfs_objects_size + build_artifacts_size + self.storage_size = repository_size + lfs_objects_size + build_artifacts_size + packages_size end # Since this incremental update method does not call update_storage_size above, diff --git a/app/models/release.rb b/app/models/release.rb index 0f9e94373c7..7bbeb3c9976 100644 --- a/app/models/release.rb +++ b/app/models/release.rb @@ -31,8 +31,11 @@ class Release < ApplicationRecord actual_tag.nil? end - def assets_count - links.count + sources.count + def assets_count(except: []) + links_count = links.count + sources_count = except.include?(:sources) ? 0 : sources.count + + links_count + sources_count end def sources diff --git a/app/models/remote_mirror.rb b/app/models/remote_mirror.rb index 535f772b6a1..cbfc1a7c1b2 100644 --- a/app/models/remote_mirror.rb +++ b/app/models/remote_mirror.rb @@ -22,8 +22,8 @@ class RemoteMirror < ApplicationRecord before_save :set_new_remote_name, if: :mirror_url_changed? after_save :set_override_remote_mirror_available, unless: -> { Gitlab::CurrentSettings.current_application_settings.mirror_available } - after_save :refresh_remote, if: :mirror_url_changed? - after_update :reset_fields, if: :mirror_url_changed? + after_save :refresh_remote, if: :saved_change_to_mirror_url? + after_update :reset_fields, if: :saved_change_to_mirror_url? after_commit :remove_remote, on: :destroy @@ -265,4 +265,8 @@ class RemoteMirror < ApplicationRecord def mirror_url_changed? url_changed? || credentials_changed? end + + def saved_change_to_mirror_url? + saved_change_to_url? || saved_change_to_credentials? + end end diff --git a/app/models/repository.rb b/app/models/repository.rb index f495a03ad8e..be17b54ff12 100644 --- a/app/models/repository.rb +++ b/app/models/repository.rb @@ -1037,11 +1037,41 @@ class Repository raw_repository.fetch_ref(source_repository.raw_repository, source_ref: source_ref, target_ref: target_ref) end + # DEPRECATED: https://gitlab.com/gitlab-org/gitaly/issues/1628 + def rebase_deprecated(user, merge_request) + rebase_sha = raw.rebase_deprecated( + user, + merge_request.id, + branch: merge_request.source_branch, + branch_sha: merge_request.source_branch_sha, + remote_repository: merge_request.target_project.repository.raw, + remote_branch: merge_request.target_branch + ) + + # To support the full deprecated behaviour, set the + # `rebase_commit_sha` for the merge_request here and return the value + merge_request.update(rebase_commit_sha: rebase_sha) + + rebase_sha + end + def rebase(user, merge_request) - raw.rebase(user, merge_request.id, branch: merge_request.source_branch, - branch_sha: merge_request.source_branch_sha, - remote_repository: merge_request.target_project.repository.raw, - remote_branch: merge_request.target_branch) + if Feature.disabled?(:two_step_rebase, default_enabled: true) + return rebase_deprecated(user, merge_request) + end + + MergeRequest.transaction do + raw.rebase( + user, + merge_request.id, + branch: merge_request.source_branch, + branch_sha: merge_request.source_branch_sha, + remote_repository: merge_request.target_project.repository.raw, + remote_branch: merge_request.target_branch + ) do |commit_id| + merge_request.update!(rebase_commit_sha: commit_id) + end + end end def squash(user, merge_request, message) diff --git a/app/models/storage/legacy_project.rb b/app/models/storage/legacy_project.rb index 76ac5c13c18..b483c677be9 100644 --- a/app/models/storage/legacy_project.rb +++ b/app/models/storage/legacy_project.rb @@ -30,7 +30,7 @@ module Storage end def rename_repo(old_full_path: nil, new_full_path: nil) - old_full_path ||= project.full_path_was + old_full_path ||= project.full_path_before_last_save new_full_path ||= project.build_full_path if gitlab_shell.mv_repository(repository_storage, old_full_path, new_full_path) diff --git a/app/policies/personal_snippet_policy.rb b/app/policies/personal_snippet_policy.rb index 2b5cca76c20..40dd49b4afd 100644 --- a/app/policies/personal_snippet_policy.rb +++ b/app/policies/personal_snippet_policy.rb @@ -7,7 +7,7 @@ class PersonalSnippetPolicy < BasePolicy rule { public_snippet }.policy do enable :read_personal_snippet - enable :comment_personal_snippet + enable :create_note end rule { is_author }.policy do @@ -15,7 +15,7 @@ class PersonalSnippetPolicy < BasePolicy enable :update_personal_snippet enable :destroy_personal_snippet enable :admin_personal_snippet - enable :comment_personal_snippet + enable :create_note end rule { ~anonymous }.enable :create_personal_snippet @@ -23,15 +23,12 @@ class PersonalSnippetPolicy < BasePolicy rule { internal_snippet & ~external_user }.policy do enable :read_personal_snippet - enable :comment_personal_snippet + enable :create_note end - rule { anonymous }.prevent :comment_personal_snippet + rule { anonymous }.prevent :create_note - rule { can?(:comment_personal_snippet) }.policy do - enable :create_note - enable :award_emoji - end + rule { can?(:create_note) }.enable :award_emoji rule { full_private_access }.enable :read_personal_snippet end diff --git a/app/policies/project_policy.rb b/app/policies/project_policy.rb index ba38af9c529..76544249688 100644 --- a/app/policies/project_policy.rb +++ b/app/policies/project_policy.rb @@ -186,6 +186,7 @@ class ProjectPolicy < BasePolicy enable :read_cycle_analytics enable :award_emoji enable :read_pages_content + enable :read_release end # These abilities are not allowed to admins that are not members of the project, @@ -212,7 +213,6 @@ class ProjectPolicy < BasePolicy enable :read_deployment enable :read_merge_request enable :read_sentry_issue - enable :read_release enable :read_prometheus end diff --git a/app/services/ci/play_manual_stage_service.rb b/app/services/ci/play_manual_stage_service.rb new file mode 100644 index 00000000000..2497fc52e6b --- /dev/null +++ b/app/services/ci/play_manual_stage_service.rb @@ -0,0 +1,29 @@ +# frozen_string_literal: true + +module Ci + class PlayManualStageService < BaseService + def initialize(project, current_user, params) + super + + @pipeline = params[:pipeline] + end + + def execute(stage) + stage.builds.manual.each do |build| + next unless build.playable? + + build.play(current_user) + rescue Gitlab::Access::AccessDeniedError + logger.error(message: 'Unable to play manual action', build_id: build.id) + end + end + + private + + attr_reader :pipeline, :current_user + + def logger + Gitlab::AppLogger + end + end +end diff --git a/app/services/clusters/refresh_service.rb b/app/services/clusters/refresh_service.rb index 76ad8dd0fb0..b02bb9c0247 100644 --- a/app/services/clusters/refresh_service.rb +++ b/app/services/clusters/refresh_service.rb @@ -22,9 +22,9 @@ module Clusters def self.clusters_with_missing_kubernetes_namespaces_for_project(project) if Feature.enabled?(:ci_preparing_state, default_enabled: true) - project.clusters.missing_kubernetes_namespace(project.kubernetes_namespaces) + project.clusters.managed.missing_kubernetes_namespace(project.kubernetes_namespaces) else - project.all_clusters.missing_kubernetes_namespace(project.kubernetes_namespaces) + project.all_clusters.managed.missing_kubernetes_namespace(project.kubernetes_namespaces) end end diff --git a/app/services/merge_requests/rebase_service.rb b/app/services/merge_requests/rebase_service.rb index 31b3ebf311e..4b9921c28ba 100644 --- a/app/services/merge_requests/rebase_service.rb +++ b/app/services/merge_requests/rebase_service.rb @@ -20,17 +20,7 @@ module MergeRequests return false end - log_prefix = "#{self.class.name} info (#{merge_request.to_reference(full: true)}):" - - Gitlab::GitLogger.info("#{log_prefix} rebase started") - - rebase_sha = repository.rebase(current_user, merge_request) - - Gitlab::GitLogger.info("#{log_prefix} rebased to #{rebase_sha}") - - merge_request.update(rebase_commit_sha: rebase_sha) - - Gitlab::GitLogger.info("#{log_prefix} rebase SHA saved: #{rebase_sha}") + repository.rebase(current_user, merge_request) true rescue => e diff --git a/app/services/projects/housekeeping_service.rb b/app/services/projects/housekeeping_service.rb index 10bd5363b51..9428575591e 100644 --- a/app/services/projects/housekeeping_service.rb +++ b/app/services/projects/housekeeping_service.rb @@ -11,6 +11,7 @@ module Projects class HousekeepingService < BaseService # Timeout set to 24h LEASE_TIMEOUT = 86400 + PACK_REFS_PERIOD = 6 class LeaseTaken < StandardError def to_s @@ -76,13 +77,15 @@ module Projects :gc elsif pushes_since_gc % full_repack_period == 0 :full_repack - else + elsif pushes_since_gc % repack_period == 0 :incremental_repack + else + :pack_refs end end def period_match? - [gc_period, full_repack_period, repack_period].any? { |period| pushes_since_gc % period == 0 } + [gc_period, full_repack_period, repack_period, PACK_REFS_PERIOD].any? { |period| pushes_since_gc % period == 0 } end def housekeeping_enabled? diff --git a/app/services/projects/update_service.rb b/app/services/projects/update_service.rb index 864ce4fa9f5..dfa7bd20254 100644 --- a/app/services/projects/update_service.rb +++ b/app/services/projects/update_service.rb @@ -79,10 +79,7 @@ module Projects end def after_rename_service(project) - # The path slug the project was using, before the rename took place. - path_before = project.previous_changes['path'].first - - AfterRenameService.new(project, path_before: path_before, full_path_before: project.full_path_was) + AfterRenameService.new(project, path_before: project.path_before_last_save, full_path_before: project.full_path_before_last_save) end def changing_pages_related_config? diff --git a/app/services/system_hooks_service.rb b/app/services/system_hooks_service.rb index bd3907cdf8e..858e04f43b2 100644 --- a/app/services/system_hooks_service.rb +++ b/app/services/system_hooks_service.rb @@ -47,7 +47,7 @@ class SystemHooksService case event when :rename - data[:old_username] = model.username_was + data[:old_username] = model.username_before_last_save when :failed_login data[:state] = model.state end @@ -58,8 +58,8 @@ class SystemHooksService if event == :rename data.merge!( - old_path: model.path_was, - old_full_path: model.full_path_was + old_path: model.path_before_last_save, + old_full_path: model.full_path_before_last_save ) end when GroupMember diff --git a/app/uploaders/object_storage.rb b/app/uploaders/object_storage.rb index 5f8b89f2a24..0a44d60778d 100644 --- a/app/uploaders/object_storage.rb +++ b/app/uploaders/object_storage.rb @@ -117,7 +117,7 @@ module ObjectStorage next unless uploader next unless uploader.exists? - next unless send(:"#{mounted_as}_changed?") # rubocop:disable GitlabSecurity/PublicSend + next unless send(:"saved_change_to_#{mounted_as}?") # rubocop:disable GitlabSecurity/PublicSend mount end.keys diff --git a/app/views/admin/groups/show.html.haml b/app/views/admin/groups/show.html.haml index 00d255846f9..f524d35d79e 100644 --- a/app/views/admin/groups/show.html.haml +++ b/app/views/admin/groups/show.html.haml @@ -44,12 +44,10 @@ %li %span.light= _('Storage:') - - counter_storage = storage_counter(@group.storage_size) - - counter_repositories = storage_counter(@group.repository_size) - - counter_build_artifacts = storage_counter(@group.build_artifacts_size) - - counter_lfs_objects = storage_counter(@group.lfs_objects_size) - %strong - = _("%{counter_storage} (%{counter_repositories} repositories, %{counter_build_artifacts} build artifacts, %{counter_lfs_objects} LFS)") % { counter_storage: counter_storage, counter_repositories: counter_repositories, counter_build_artifacts: counter_build_artifacts, counter_lfs_objects: counter_lfs_objects } + %strong= storage_counter(@group.storage_size) + ( + = storage_counters_details(@group) + ) %li %span.light= _('Group Git LFS status:') diff --git a/app/views/admin/projects/show.html.haml b/app/views/admin/projects/show.html.haml index 03cce4745aa..bc34af88928 100644 --- a/app/views/admin/projects/show.html.haml +++ b/app/views/admin/projects/show.html.haml @@ -73,15 +73,10 @@ = @project.repository.relative_path %li - %span.light Storage used: + %span.light= _('Storage:') %strong= storage_counter(@project.statistics.storage_size) ( - = storage_counter(@project.statistics.repository_size) - repository, - = storage_counter(@project.statistics.build_artifacts_size) - build artifacts, - = storage_counter(@project.statistics.lfs_objects_size) - LFS + = storage_counters_details(@project.statistics) ) %li diff --git a/app/views/clusters/clusters/gcp/_form.html.haml b/app/views/clusters/clusters/gcp/_form.html.haml index 3e0f8955081..70e2eaeaf3b 100644 --- a/app/views/clusters/clusters/gcp/_form.html.haml +++ b/app/views/clusters/clusters/gcp/_form.html.haml @@ -74,6 +74,13 @@ = link_to _('More information'), help_page_path('user/project/clusters/index.md', anchor: 'role-based-access-control-rbac-core-only'), target: '_blank' - .form-group - = field.submit s_('ClusterIntegration|Create Kubernetes cluster'), - class: 'js-gke-cluster-creation-submit btn btn-success', disabled: true + .form-group + = field.check_box :managed, { label: s_('ClusterIntegration|GitLab-managed cluster'), + label_class: 'label-bold' } + .form-text.text-muted + = s_('ClusterIntegration|Allow GitLab to manage namespace and service accounts for this cluster.') + = link_to _('More information'), help_page_path('user/project/clusters/index.md', anchor: 'gitlab-managed-clusters'), target: '_blank' + + .form-group + = field.submit s_('ClusterIntegration|Create Kubernetes cluster'), + class: 'js-gke-cluster-creation-submit btn btn-success', disabled: true diff --git a/app/views/clusters/clusters/user/_form.html.haml b/app/views/clusters/clusters/user/_form.html.haml index 4dba0e530e7..f2fc5ac93fb 100644 --- a/app/views/clusters/clusters/user/_form.html.haml +++ b/app/views/clusters/clusters/user/_form.html.haml @@ -44,5 +44,12 @@ { class: 'qa-rbac-checkbox', label: s_('ClusterIntegration|RBAC-enabled cluster'), label_class: 'label-bold', inline: true }, 'rbac', 'abac' - .form-group - = field.submit s_('ClusterIntegration|Add Kubernetes cluster'), class: 'btn btn-success' + .form-group + = field.check_box :managed, { label: s_('ClusterIntegration|GitLab-managed cluster'), + label_class: 'label-bold' } + .form-text.text-muted + = s_('ClusterIntegration|Allow GitLab to manage namespace and service accounts for this cluster.') + = link_to _('More information'), help_page_path('user/project/clusters/index.md', anchor: 'gitlab-managed-clusters'), target: '_blank' + + .form-group + = field.submit s_('ClusterIntegration|Add Kubernetes cluster'), class: 'btn btn-success' diff --git a/app/views/clusters/platforms/kubernetes/_form.html.haml b/app/views/clusters/platforms/kubernetes/_form.html.haml index f9f8097cb38..8caa25a7b5e 100644 --- a/app/views/clusters/platforms/kubernetes/_form.html.haml +++ b/app/views/clusters/platforms/kubernetes/_form.html.haml @@ -47,5 +47,12 @@ = s_('ClusterIntegration|Enable this setting if using role-based access control (RBAC).') = s_('ClusterIntegration|This option will allow you to install applications on RBAC clusters.') + .form-group + = field.check_box :managed, { disabled: true, label: s_('ClusterIntegration|GitLab-managed cluster'), + label_class: 'label-bold' } + .form-text.text-muted + = s_('ClusterIntegration|Allow GitLab to manage namespace and service accounts for this cluster.') + = link_to _('More information'), help_page_path('user/project/clusters/index.md', anchor: 'gitlab-managed-clusters'), target: '_blank' + .form-group = field.submit s_('ClusterIntegration|Save changes'), class: 'btn btn-success' diff --git a/app/workers/cluster_configure_worker.rb b/app/workers/cluster_configure_worker.rb index 22681157b62..37ea7dde7a1 100644 --- a/app/workers/cluster_configure_worker.rb +++ b/app/workers/cluster_configure_worker.rb @@ -5,7 +5,7 @@ class ClusterConfigureWorker include ClusterQueue def perform(cluster_id) - Clusters::Cluster.find_by_id(cluster_id).try do |cluster| + Clusters::Cluster.managed.find_by_id(cluster_id).try do |cluster| if cluster.project_type? || Feature.disabled?(:ci_preparing_state, default_enabled: true) Clusters::RefreshService.create_or_update_namespaces_for_cluster(cluster) end diff --git a/app/workers/git_garbage_collect_worker.rb b/app/workers/git_garbage_collect_worker.rb index b33e9b1f718..d4a6f53dae5 100644 --- a/app/workers/git_garbage_collect_worker.rb +++ b/app/workers/git_garbage_collect_worker.rb @@ -29,7 +29,7 @@ class GitGarbageCollectWorker # Refresh the branch cache in case garbage collection caused a ref lookup to fail flush_ref_caches(project) if task == :gc - project.repository.expire_statistics_caches + project.repository.expire_statistics_caches if task != :pack_refs # In case pack files are deleted, release libgit2 cache and open file # descriptors ASAP instead of waiting for Ruby garbage collection @@ -58,7 +58,12 @@ class GitGarbageCollectWorker ## `repository` has to be a Gitlab::Git::Repository def gitaly_call(task, repository) - client = Gitlab::GitalyClient::RepositoryService.new(repository) + client = if task == :pack_refs + Gitlab::GitalyClient::RefService.new(repository) + else + Gitlab::GitalyClient::RepositoryService.new(repository) + end + case task when :gc client.garbage_collect(bitmaps_enabled?) @@ -66,6 +71,8 @@ class GitGarbageCollectWorker client.repack_full(bitmaps_enabled?) when :incremental_repack client.repack_incremental + when :pack_refs + client.pack_refs end rescue GRPC::NotFound => e Gitlab::GitLogger.error("#{__method__} failed:\nRepository not found") diff --git a/changelogs/unreleased/18432-switch-to-sassc-rails.yml b/changelogs/unreleased/18432-switch-to-sassc-rails.yml new file mode 100644 index 00000000000..1c9d515c52f --- /dev/null +++ b/changelogs/unreleased/18432-switch-to-sassc-rails.yml @@ -0,0 +1,5 @@ +--- +title: Switch to sassc-rails for faster stylesheet compilation +merge_request: 26224 +author: +type: changed diff --git a/changelogs/unreleased/28741-play-all-manual-jobs.yml b/changelogs/unreleased/28741-play-all-manual-jobs.yml new file mode 100644 index 00000000000..30b26e3c0ed --- /dev/null +++ b/changelogs/unreleased/28741-play-all-manual-jobs.yml @@ -0,0 +1,5 @@ +--- +title: Play all manual jobs in a stage +merge_request: 27188 +author: +type: added diff --git a/changelogs/unreleased/56557-disable-kubernetes-namespace-service-account-backend.yml b/changelogs/unreleased/56557-disable-kubernetes-namespace-service-account-backend.yml new file mode 100644 index 00000000000..6521eb9d1c0 --- /dev/null +++ b/changelogs/unreleased/56557-disable-kubernetes-namespace-service-account-backend.yml @@ -0,0 +1,5 @@ +--- +title: Disables kubernetes resources creation if a cluster is not managed +merge_request: 26565 +author: +type: added diff --git a/changelogs/unreleased/56838-allow-guest-access-to-releases.yml b/changelogs/unreleased/56838-allow-guest-access-to-releases.yml new file mode 100644 index 00000000000..701a015b9ac --- /dev/null +++ b/changelogs/unreleased/56838-allow-guest-access-to-releases.yml @@ -0,0 +1,5 @@ +--- +title: Allow guests users to access project releases +merge_request: 27247 +author: +type: changed diff --git a/changelogs/unreleased/5966-rebase-with-block.yml b/changelogs/unreleased/5966-rebase-with-block.yml new file mode 100644 index 00000000000..9272a02977f --- /dev/null +++ b/changelogs/unreleased/5966-rebase-with-block.yml @@ -0,0 +1,5 @@ +--- +title: Fix approvals sometimes being reset after a merge request is rebased +merge_request: 27446 +author: +type: fixed diff --git a/changelogs/unreleased/ac-package-storage-stats.yml b/changelogs/unreleased/ac-package-storage-stats.yml new file mode 100644 index 00000000000..fedffb41597 --- /dev/null +++ b/changelogs/unreleased/ac-package-storage-stats.yml @@ -0,0 +1,5 @@ +--- +title: Add packages_size to ProjectStatistics +merge_request: 27373 +author: +type: added diff --git a/changelogs/unreleased/expand-diff-performance.yml b/changelogs/unreleased/expand-diff-performance.yml new file mode 100644 index 00000000000..134ea4081e4 --- /dev/null +++ b/changelogs/unreleased/expand-diff-performance.yml @@ -0,0 +1,5 @@ +--- +title: Improve expanding diff to full file performance +merge_request: +author: +type: changed diff --git a/changelogs/unreleased/issue-61038-deploy-chat-message-update.yml b/changelogs/unreleased/issue-61038-deploy-chat-message-update.yml new file mode 100644 index 00000000000..c85ddc7b91c --- /dev/null +++ b/changelogs/unreleased/issue-61038-deploy-chat-message-update.yml @@ -0,0 +1,5 @@ +--- +title: Update deployment event chat notification message +merge_request: 27972 +author: +type: changed diff --git a/changelogs/unreleased/use-pg-10-7.yml b/changelogs/unreleased/use-pg-10-7.yml new file mode 100644 index 00000000000..aa57c3a6a17 --- /dev/null +++ b/changelogs/unreleased/use-pg-10-7.yml @@ -0,0 +1,5 @@ +--- +title: Use PostgreSQL 10.7 in tests +merge_request: 28020 +author: +type: other diff --git a/config/initializers/peek.rb b/config/initializers/peek.rb index eeb45fae753..cb108416b10 100644 --- a/config/initializers/peek.rb +++ b/config/initializers/peek.rb @@ -10,6 +10,18 @@ elsif Gitlab::Database.postgresql? require 'peek-pg' PEEK_DB_CLIENT = ::PG::Connection PEEK_DB_VIEW = Peek::Views::PG + + # Remove once we have https://github.com/peek/peek-pg/pull/10 + module ::Peek::PGInstrumented + def exec_params(*args) + start = Time.now + super(*args) + ensure + duration = (Time.now - start) + PEEK_DB_CLIENT.query_time.update { |value| value + duration } + PEEK_DB_CLIENT.query_count.update { |value| value + 1 } + end + end else raise "Unsupported database adapter for peek!" end diff --git a/config/routes/project.rb b/config/routes/project.rb index 61eb136f65b..93d746f3282 100644 --- a/config/routes/project.rb +++ b/config/routes/project.rb @@ -201,6 +201,12 @@ constraints(::Constraints::ProjectUrlConstrainer.new) do get :failures get :status end + + member do + resources :stages, only: [], param: :name do + post :play_manual + end + end end resources :pipeline_schedules, except: [:show] do diff --git a/db/migrate/20170503140202_turn_nested_groups_into_regular_groups_for_mysql.rb b/db/migrate/20170503140202_turn_nested_groups_into_regular_groups_for_mysql.rb index cfa63b65df4..65b2c6a57be 100644 --- a/db/migrate/20170503140202_turn_nested_groups_into_regular_groups_for_mysql.rb +++ b/db/migrate/20170503140202_turn_nested_groups_into_regular_groups_for_mysql.rb @@ -46,13 +46,6 @@ class TurnNestedGroupsIntoRegularGroupsForMysql < ActiveRecord::Migration[4.2] bulk_insert_members(rows) - # This method relies on the parent to determine the proper path. - # Because we reset "parent_id" this method will not return the right path - # when moving namespaces. - full_path_was = namespace.send(:full_path_was) - - namespace.define_singleton_method(:full_path_was) { full_path_was } - namespace.update!(parent_id: nil, path: new_path_for(namespace)) end end diff --git a/db/migrate/20190415095825_add_packages_size_to_project_statistics.rb b/db/migrate/20190415095825_add_packages_size_to_project_statistics.rb new file mode 100644 index 00000000000..99625981563 --- /dev/null +++ b/db/migrate/20190415095825_add_packages_size_to_project_statistics.rb @@ -0,0 +1,11 @@ +# frozen_string_literal: true + +class AddPackagesSizeToProjectStatistics < ActiveRecord::Migration[5.0] + include Gitlab::Database::MigrationHelpers + + DOWNTIME = false + + def change + add_column :project_statistics, :packages_size, :bigint + end +end diff --git a/db/post_migrate/20190424134256_drop_projects_ci_id.rb b/db/post_migrate/20190424134256_drop_projects_ci_id.rb new file mode 100644 index 00000000000..79fa9704f1f --- /dev/null +++ b/db/post_migrate/20190424134256_drop_projects_ci_id.rb @@ -0,0 +1,28 @@ +# 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 DropProjectsCiId < ActiveRecord::Migration[5.1] + include Gitlab::Database::MigrationHelpers + + # Set this constant to true if this migration requires downtime. + DOWNTIME = false + + disable_ddl_transaction! + + def up + if index_exists?(:projects, :ci_id) + remove_concurrent_index :projects, :ci_id + end + + if column_exists?(:projects, :ci_id) + remove_column :projects, :ci_id + end + end + + def down + add_column :projects, :ci_id, :integer + add_concurrent_index :projects, :ci_id + end +end diff --git a/db/schema.rb b/db/schema.rb index 2e77bbb51e5..de9e6f0b40d 100644 --- a/db/schema.rb +++ b/db/schema.rb @@ -1715,6 +1715,7 @@ ActiveRecord::Schema.define(version: 20190426180107) do t.bigint "repository_size", default: 0, null: false t.bigint "lfs_objects_size", default: 0, null: false t.bigint "build_artifacts_size", default: 0, null: false + t.bigint "packages_size" t.index ["namespace_id"], name: "index_project_statistics_on_namespace_id", using: :btree t.index ["project_id"], name: "index_project_statistics_on_project_id", unique: true, using: :btree end @@ -1737,7 +1738,6 @@ ActiveRecord::Schema.define(version: 20190426180107) do t.string "import_type" t.string "import_source" t.text "import_error" - t.integer "ci_id" t.boolean "shared_runners_enabled", default: true, null: false t.string "runners_token" t.string "build_coverage_regex" @@ -1776,7 +1776,6 @@ ActiveRecord::Schema.define(version: 20190426180107) do t.string "bfg_object_map" t.boolean "detected_repository_languages" t.string "external_authorization_classification_label" - t.index ["ci_id"], name: "index_projects_on_ci_id", using: :btree t.index ["created_at"], name: "index_projects_on_created_at", using: :btree t.index ["creator_id"], name: "index_projects_on_creator_id", using: :btree t.index ["description"], name: "index_projects_on_description_trigram", using: :gin, opclasses: {"description"=>"gin_trgm_ops"} diff --git a/doc/api/project_clusters.md b/doc/api/project_clusters.md index f36e352da67..c831cc52a93 100644 --- a/doc/api/project_clusters.md +++ b/doc/api/project_clusters.md @@ -161,6 +161,7 @@ Parameters: | `name` | String | yes | The name of the cluster | | `domain` | String | no | The [base domain](../user/project/clusters/index.md#base-domain) of the cluster | | `enabled` | Boolean | no | Determines if cluster is active or not, defaults to true | +| `managed` | Boolean | no | Determines if GitLab will manage namespaces and service accounts for this cluster, defaults to true | | `platform_kubernetes_attributes[api_url]` | String | yes | The URL to access the Kubernetes API | | `platform_kubernetes_attributes[token]` | String | yes | The token to authenticate against Kubernetes | | `platform_kubernetes_attributes[ca_cert]` | String | no | TLS certificate (needed if API is using a self-signed TLS certificate | diff --git a/doc/ci/README.md b/doc/ci/README.md index 440a79c7782..27bde2ac0f1 100644 --- a/doc/ci/README.md +++ b/doc/ci/README.md @@ -3,7 +3,7 @@ comments: false description: "Learn how to use GitLab CI/CD, the GitLab built-in Continuous Integration, Continuous Deployment, and Continuous Delivery toolset to build, test, and deploy your application." --- -# GitLab Continuous Integration (GitLab CI/CD) +# GitLab CI/CD GitLab CI/CD is a tool built into GitLab for software development through the [continuous methodologies](introduction/index.md#introduction-to-cicd-methodologies): @@ -61,12 +61,12 @@ for all the attributes you can set and use. NOTE: **Note:** GitLab CI/CD and [shared runners](runners/README.md#shared-specific-and-group-runners) are enabled in GitLab.com and available for all users, limited only to the [user's pipelines quota](https://docs.gitlab.com/ee/user/admin_area/settings/continuous_integration.html#extra-shared-runners-pipeline-minutes-quota). -## GitLab CI/CD configuration +## Configuration GitLab CI/CD supports numerous configuration options: -| Configuration | Description | -|:--- |:--- | +| Configuration | Description | +|:--------------|:-------------| | [Pipelines](pipelines.md) | Structure your CI/CD process through pipelines. | | [Environment variables](variables/README.md) | Reuse values based on a variable/value key pair. | | [Environments](environments.md) | Deploy your application to different environments (e.g., staging, production). | @@ -74,7 +74,7 @@ GitLab CI/CD supports numerous configuration options: | [Cache dependencies](caching/index.md) | Cache your dependencies for a faster execution. | | [Schedule pipelines](../user/project/pipelines/schedules.md) | Schedule pipelines to run as often as you need. | | [Custom path for `.gitlab-ci.yml`](../user/project/pipelines/settings.md#custom-ci-config-path) | Define a custom path for the CI/CD configuration file. | -| [Git submodules for CI/CD](git_submodules.md) | Configure jobs for using Git submodules. | +| [Git submodules for CI/CD](git_submodules.md) | Configure jobs for using Git submodules.| | [SSH keys for CI/CD](ssh_keys/README.md) | Using SSH keys in your CI pipelines. | | [Pipelines triggers](triggers/README.md) | Trigger pipelines through the API. | | [Integrate with Kubernetes clusters](../user/project/clusters/index.md) | Connect your project to Google Kubernetes Engine (GKE) or an existing Kubernetes cluster. | @@ -85,33 +85,43 @@ GitLab CI/CD supports numerous configuration options: Note that certain operations can only be performed according to the [user](../user/permissions.md#gitlab-cicd-permissions) and [job](../user/permissions.md#job-permissions) permissions. -## GitLab CI/CD feature set +## Feature set -You can also use the vast GitLab CI/CD feature set to easily configure -it for specific purposes: +Use the vast GitLab CI/CD to easily configure it for specific purposes. +Its feature set is listed on the table below according to DevOps stages. | Feature | Description | -|:--- |:--- | -| [Auto Deploy](../topics/autodevops/index.md#auto-deploy) | Deploy your application to a production environment in a Kubernetes cluster. | +|:--------|:------------| +| **Configure** || | [Auto DevOps](../topics/autodevops/index.md) | Set up your app's entire lifecycle. | +| [ChatOps](chatops/README.md) | Trigger CI jobs from chat, with results sent back to the channel. | +|---+---| +| **Verify** || +| [Browser Performance Testing](https://docs.gitlab.com/ee/user/project/merge_requests/browser_performance_testing.html) | Quickly determine the performance impact of pending code changes. | +| [CI services](services/README.md) | Link Docker containers with your base image.| +| [Code Quality](https://docs.gitlab.com/ee/user/project/merge_requests/code_quality.html) **[STARTER]** | Analyze your source code quality. | +| [GitLab CI/CD for external repositories](https://docs.gitlab.com/ee/ci/ci_cd_for_external_repos/) **[PREMIUM]** | Get the benefits of GitLab CI/CD combined with repositories in GitHub and BitBucket Cloud. | +| [Interactive Web Terminals](interactive_web_terminal/index.md) **[CORE ONLY]** | Open an interactive web terminal to debug the running jobs. | +| [JUnit tests](junit_test_reports.md) | Identify script failures directly on merge requests. | +| [Using Docker images](docker/using_docker_images.md) | Use GitLab and GitLab Runner with Docker to build and test applications. | +|---+---| +| **Release** || +| [Auto Deploy](../topics/autodevops/index.md#auto-deploy) | Deploy your application to a production environment in a Kubernetes cluster. | | [Building Docker images](docker/using_docker_build.md) | Maintain Docker-based projects using GitLab CI/CD. | | [Canary Deployments](https://docs.gitlab.com/ee/user/project/canary_deployments.html) **[PREMIUM]** | Ship features to only a portion of your pods and let a percentage of your user base to visit the temporarily deployed feature. | -| [ChatOps](chatops/README.md) | Trigger CI jobs from chat, with results sent back to the channel. | -| [CI services](services/README.md)| Link Docker containers with your base image. | -| [Container Scanning](https://docs.gitlab.com/ee/ci/examples/container_scanning.html) **[ULTIMATE]**| Check your Docker containers for known vulnerabilities. | -| [Dependency Scanning](https://docs.gitlab.com/ee/ci/examples/dependency_scanning.html) **[ULTIMATE]**| Analyze your dependencies for known vulnerabilities. | | [Deploy Boards](https://docs.gitlab.com/ee/user/project/deploy_boards.html) **[PREMIUM]** | Check the current health and status of each CI/CD environment running on Kubernetes. | | [Feature Flags](https://docs.gitlab.com/ee/user/project/operations/feature_flags.html) **[PREMIUM]** | Deploy your features behind Feature Flags. | -| [GitLab CI/CD for external repositories](https://docs.gitlab.com/ee/ci/ci_cd_for_external_repos/) **[PREMIUM]** | Get the benefits of GitLab CI/CD combined with repositories in GitHub and BitBucket Cloud. | | [GitLab Pages](../user/project/pages/index.md) | Deploy static websites. | | [GitLab Releases](../user/project/releases/index.md) | Add release notes to Git tags. | -| [Interactive Web Terminals](interactive_web_terminal/index.md) **[CORE ONLY]** | Open an interactive web terminal to debug the running jobs. | -| [JUnit tests](junit_test_reports.md)| Identify script failures directly on merge requests. | | [Review Apps](review_apps/index.md) | Configure GitLab CI/CD to preview code changes. | +|---+---| +| **Secure** || +| [Container Scanning](https://docs.gitlab.com/ee/ci/examples/container_scanning.html) **[ULTIMATE]** | Check your Docker containers for known vulnerabilities.| +| [Dependency Scanning](https://docs.gitlab.com/ee/ci/examples/dependency_scanning.html) **[ULTIMATE]** | Analyze your dependencies for known vulnerabilities. | +| [License Management](https://docs.gitlab.com/ee/user/application_security/license_management/) **[ULTIMATE]** | Search your project dependencies for their licenses. | | [Security Test reports](https://docs.gitlab.com/ee/user/project/merge_requests/#security-reports-ultimate) **[ULTIMATE]** | Check for app vulnerabilities. | -| [Using Docker images](docker/using_docker_images.md) | Use GitLab and GitLab Runner with Docker to build and test applications. | -## GitLab CI/CD examples +## Examples GitLab provides examples of configuring GitLab CI/CD in the form of: @@ -120,7 +130,7 @@ GitLab provides examples of configuring GitLab CI/CD in the form of: - [`multi-project-pipelines`](https://gitlab.com/gitlab-examples/multi-project-pipelines) for examples of implementing multi-project pipelines. - [`review-apps-nginx`](https://gitlab.com/gitlab-examples/review-apps-nginx/) provides an example of using Review Apps. -## GitLab CI/CD administration **[CORE ONLY]** +## Administration **[CORE ONLY]** As a GitLab administrator, you can change the default behavior of GitLab CI/CD for: diff --git a/doc/ci/variables/README.md b/doc/ci/variables/README.md index 6313ffc584d..ace439900ab 100644 --- a/doc/ci/variables/README.md +++ b/doc/ci/variables/README.md @@ -52,6 +52,33 @@ or directly in the `.gitlab-ci.yml` file and reuse them as you wish. That can be very powerful as it can be used for scripting without the need to specify the value itself. +#### Variable types + +> [Introduced](https://gitlab.com/gitlab-org/gitlab-ce/issues/46806) in GitLab 11.11. + +There are two types of variables supported by GitLab: + +- `env_var`: the runner will create environment variable named same as the variable key and set its value to the variable value. +- `file`: the runner will write the variable value to a temporary file and set the path to this file as the value of an environment variable named same as the variable key. + +#### Masked variables + +> [Introduced](https://gitlab.com/gitlab-org/gitlab-ce/issues/13784) in GitLab 11.10 + +By default, variables will be created as masked variables. +This means that the value of the variable will be hidden in job logs, +though it must match certain requirements to do so: + +- The value must be in a single line. +- The value must not have escape characters. +- The value must not use variables. +- The value must not have any whitespace. +- The value must be at least 8 characters long. + +If the value does not meet the requirements above, then the CI variable will fail to save. +In order to save, either alter the value to meet the masking requirements +or disable **Masked** for the variable. + ## Getting started To get started with environment variables in the scope of GitLab @@ -104,7 +131,10 @@ let's say you want to output `HELLO WORLD` for a `TEST` variable. You can either set the variable directly in the `.gitlab-ci.yml` file or through the UI. -#### Via [`.gitlab-ci.yml`](../yaml/README.md#variables) +#### Via `.gitlab-ci.yml` + +To create a new custom `env_var` variable via [`.gitlab-ci.yml`](../yaml/README.md#variables), define their variable/value pair under +`variables`: ```yaml variables: @@ -116,11 +146,13 @@ For a deeper look into them, see [`.gitlab-ci.yml` defined variables](#gitlab-ci #### Via the UI From the UI, navigate to your project's **Settings > CI/CD** and -expand **Environment variables**. Create a new variable by naming -it in the field **Input variable key**, and define its value in the +expand **Variables**. Create a new variable by choosing its **type**, naming +it in the field **Input variable key**, and defining its value in the **Input variable value** field: -![CI/CD settings - new variable](img/new_custom_variable_example.png) +![CI/CD settings - new variable](img/new_custom_variables_example.png) + +You'll also see the option to mask and/or protect your variables. Once you've set the variables, call them from the `.gitlab-ci.yml` file: @@ -129,30 +161,14 @@ test_variable: stage: test script: - echo $CI_JOB_STAGE # calls a predefined variable - - echo $TEST # calls a custom variable + - echo $TEST # calls a custom variable of type `env_var` + - echo $GREETING # calls a custom variable of type `file` that contains the path to the temp file + - cat $GREETING # the temp file itself contains the variable value ``` The output will be: -![Output custom variable](img/custom_variable_output.png) - -### Masked Variables - -By default, variables will be created as masked variables. -This means that the value of the variable will be hidden in job logs, -though it must match certain requirements to do so: - -- The value must be in a single line. -- The value must contain only letters, numbers, or underscores. -- The value must not have escape characters, such as `\"` -- The value must not use variables. -- The value must not have any whitespace. -- The value must be at least 8 characters long. - -The above rules are validated using the regex `/\A\w{8,}\z/`. If the value -does not meet the requirements above, then the CI variable will fail to save. -In order to save, either alter the value to meet the masking requirements or -disable `Masked` for the variable. +![Output custom variable](img/custom_variables_output.png) ### Syntax of environment variables in job scripts @@ -299,7 +315,7 @@ use for storing things like passwords, SSH keys, and credentials. Group-level variables can be added by: 1. Navigating to your group's **Settings > CI/CD** page. -1. Inputing variable keys and values in the **Environment variables** section. +1. Inputing variable types, keys, and values in the **Variables** section. Any variables of [subgroups](../../user/group/subgroups/index.md) will be inherited recursively. Once you set them, they will be available for all subsequent pipelines. @@ -390,8 +406,7 @@ For instance, suppose you added a [custom variable `$TEST`](#creating-a-custom-environment-variable) as exemplified above and you want to override it in a manual pipeline. Navigate to your project's **CI/CD > Pipelines** and click **Run pipeline**. -Choose the branch you want to run the pipeline for, then add a new variable -pair through the UI: +Choose the branch you want to run the pipeline for, then add a new variable through the UI: ![Override variable value](img/override_variable_manual_pipeline.png) diff --git a/doc/ci/variables/img/custom_variable_output.png b/doc/ci/variables/img/custom_variable_output.png Binary files differdeleted file mode 100644 index 50f3bceff9a..00000000000 --- a/doc/ci/variables/img/custom_variable_output.png +++ /dev/null diff --git a/doc/ci/variables/img/custom_variables_output.png b/doc/ci/variables/img/custom_variables_output.png Binary files differnew file mode 100644 index 00000000000..29f5c63b3d9 --- /dev/null +++ b/doc/ci/variables/img/custom_variables_output.png diff --git a/doc/ci/variables/img/new_custom_variable_example.png b/doc/ci/variables/img/new_custom_variable_example.png Binary files differdeleted file mode 100644 index d169c5f1806..00000000000 --- a/doc/ci/variables/img/new_custom_variable_example.png +++ /dev/null diff --git a/doc/ci/variables/img/new_custom_variables_example.png b/doc/ci/variables/img/new_custom_variables_example.png Binary files differnew file mode 100644 index 00000000000..4b78e0ff587 --- /dev/null +++ b/doc/ci/variables/img/new_custom_variables_example.png diff --git a/doc/ci/variables/img/override_variable_manual_pipeline.png b/doc/ci/variables/img/override_variable_manual_pipeline.png Binary files differindex 3bcd354e096..3c8c59720cf 100644 --- a/doc/ci/variables/img/override_variable_manual_pipeline.png +++ b/doc/ci/variables/img/override_variable_manual_pipeline.png diff --git a/doc/development/documentation/styleguide.md b/doc/development/documentation/styleguide.md index 9d8d5afedad..5caca846cc9 100644 --- a/doc/development/documentation/styleguide.md +++ b/doc/development/documentation/styleguide.md @@ -604,41 +604,49 @@ The following are recommended verbs for specific uses. ## GitLab versions and tiers -- Every piece of documentation that comes with a new feature should declare the - GitLab version that feature got introduced. Right below the heading add a - blockquote: +Tagged and released versions of GitLab documentation are available: + +- In the [documentation archives](https://docs.gitlab.com/archives/). +- At the `/help` URL for any GitLab installation. + +The version introducing a new feature is added to the top of the topic in the documentation to provide +a helpful link back to how the feature was developed. + +### Text for documentation requiring version text + +- For features that need to declare the GitLab version that the feature was introduced. Text similar + to the following should be added immediately below the heading as a blockquote: ```md - > Introduced in GitLab 8.3. + > Introduced in GitLab 11.3. ``` -- Whenever possible, every feature should have a link to the issue, MR or epic - (in that order) that introduced it. The above quote would be then transformed to: +- Whenever possible, version text should have a link to the issue, merge request, or epic that introduced the feature. + An issue is preferred over a merge request, and a merge request is preferred over an epic. For example: ```md - > [Introduced](<link-to-issue>) in GitLab 8.3. + > [Introduced](<link-to-issue>) in GitLab 11.3. ``` -- If the feature is only available in GitLab Enterprise Edition, don't forget to mention +- If the feature is only available in GitLab Enterprise Edition, mention the [paid tier](https://about.gitlab.com/handbook/marketing/product-marketing/#tiers) the feature is available in: ```md - > [Introduced](<link-to-issue>) in [GitLab Starter](https://about.gitlab.com/pricing/) 10.3. + > [Introduced](<link-to-issue>) in [GitLab Starter](https://about.gitlab.com/pricing/) 11.3. ``` -### Early versions of EE +### Removing version text -If the feature was created before GitLab 9.2 (before [different EE tiers were introduced](https://gitlab.com/gitlab-org/gitlab-ee/merge_requests/1851)): +Over time, version text will reference a progressively older version of GitLab. In cases where version text +refers to versions of GitLab four or more major versions back, consider removing the text. -- Declare it as "Introduced in GitLab Enterprise Edition X.Y". -- Note which tier the feature is available in. +For example, if the current major version is 11.x, version text referencing versions of GitLab 7.x +and older are candidates for removal. -For example: - -```md -> [Introduced](<link-to-issue>) in GitLab Enterprise Edition 9.0. Available in [GitLab Premium](https://about.gitlab.com/pricing/). -``` +NOTE: **Note:** +This guidance applies to any text that mentions a GitLab version, not just "Introduced in... " text. +Other text includes deprecation notices and version-specific how-to information. ## Product badges diff --git a/doc/development/gitaly.md b/doc/development/gitaly.md index d7b6b6aaae5..4ef20d5fd74 100644 --- a/doc/development/gitaly.md +++ b/doc/development/gitaly.md @@ -63,6 +63,8 @@ If your test-suite is failing with Gitaly issues, as a first step, try running: rm -rf tmp/tests/gitaly ``` +During rspec tests, the Gitaly instance will write logs to `gitlab/log/gitaly-test.log`. + ## Legacy Rugged code While Gitaly can handle all Git access, many of GitLab customers still diff --git a/doc/development/i18n/proofreader.md b/doc/development/i18n/proofreader.md index aa463e467d4..ac04a21b37a 100644 --- a/doc/development/i18n/proofreader.md +++ b/doc/development/i18n/proofreader.md @@ -43,6 +43,7 @@ are very appreciative of the work done by translators and proofreaders! - Pedro Garcia - [GitLab](https://gitlab.com/pedgarrod), [Crowdin](https://crowdin.com/profile/breaking_pitt) - German - Michael Hahnle - [GitLab](https://gitlab.com/mhah), [Crowdin](https://crowdin.com/profile/mhah) + - Katrin Leinweber - [GitLab](https://gitlab.com/katrinleinweber/), [Crowdin](https://crowdin.com/profile/katrinleinweber) - Greek - Proofreaders needed. - Hebrew diff --git a/doc/user/group/clusters/index.md b/doc/user/group/clusters/index.md index 984881ef26c..0f71587830f 100644 --- a/doc/user/group/clusters/index.md +++ b/doc/user/group/clusters/index.md @@ -72,6 +72,29 @@ Add another cluster similar to the first one and make sure to [set an environment scope](#environment-scopes-premium) that will differentiate the new cluster from the rest. +## Gitlab-managed clusters + +> [Introduced](https://gitlab.com/gitlab-org/gitlab-ce/merge_requests/22011) in GitLab 11.5. +> Became [optional](https://gitlab.com/gitlab-org/gitlab-ce/merge_requests/26565) in GitLab 11.11. + +NOTE: **Note:** +Only available when creating clusters. Existing clusters not managed by GitLab +cannot become GitLab-managed later. + +You can choose to allow GitLab to manage your cluster for you. If your cluster is +managed by GitLab, resources for your projects will be automatically created. See the +[Access controls](../../project/clusters/index.md#access-controls) section for details on which resources will +be created. + +If you choose to manage your own cluster, project-specific resources will not be created +automatically. If you are using [Auto DevOps](../../../topics/autodevops/index.md), you will +need to explicitly provide the `KUBE_NAMESPACE` [deployment variable](../../project/clusters/index.md#deployment-variables) +that will be used by your deployment jobs. + +NOTE: **Note:** +If you [install applications](#installing-applications) on your cluster, GitLab will create +the resources required to run these even if you have chosen to manage your own cluster. + ## Base domain > [Introduced](https://gitlab.com/gitlab-org/gitlab-ce/merge_requests/24580) in GitLab 11.8. diff --git a/doc/user/permissions.md b/doc/user/permissions.md index a340dd063e4..f03d2ca98ce 100644 --- a/doc/user/permissions.md +++ b/doc/user/permissions.md @@ -172,8 +172,12 @@ read through the documentation on [permissions and access to confidential issues ### Releases permissions -[Project Releases](project/releases/index.md) can be read by all project -members (Reporters, Developers, Maintainers, Owners) **except Guests**. +[Project Releases](project/releases/index.md) can be read by project +members with Reporter, Developer, Maintainer, and Owner permissions. +Guest users can access Release pages for downloading assets but +are not allowed to download the source code nor see repository +information such as tags and commits. + Releases can be created, updated, or deleted via [Releases APIs](../api/releases/index.md) by project Developers, Maintainers, and Owners. diff --git a/doc/user/project/clusters/index.md b/doc/user/project/clusters/index.md index 0677fe622f2..52b1708fe2d 100644 --- a/doc/user/project/clusters/index.md +++ b/doc/user/project/clusters/index.md @@ -70,6 +70,7 @@ new Kubernetes cluster to your project: - **Machine type** - The [machine type](https://cloud.google.com/compute/docs/machine-types) of the Virtual Machine instance that the cluster will be based on. - **RBAC-enabled cluster** - Leave this checked if using default GKE creation options, see the [RBAC section](#role-based-access-control-rbac) for more information. + - **GitLab-managed cluster** - Leave this checked if you want GitLab to manage namespaces and service accounts for this cluster. See the [Managed clusters section](#gitlab-managed-clusters) for more information. 1. Finally, click the **Create Kubernetes cluster** button. After a couple of minutes, your cluster will be ready to go. You can now proceed @@ -188,6 +189,9 @@ To add an existing Kubernetes cluster to your project: role binding. You can follow the [Google Cloud documentation](https://cloud.google.com/iam/docs/granting-changing-revoking-access) to grant access. + + - **GitLab-managed cluster** - Leave this checked if you want GitLab to manage namespaces and service accounts for this cluster. See the [Managed clusters section](#gitlab-managed-clusters) for more information. + - **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. @@ -214,6 +218,29 @@ functionalities needed to successfully build and deploy a containerized application. Bear in mind that the same credentials are used for all the applications running on the cluster. +## Gitlab-managed clusters + +> [Introduced](https://gitlab.com/gitlab-org/gitlab-ce/merge_requests/22011) in GitLab 11.5. +> Became [optional](https://gitlab.com/gitlab-org/gitlab-ce/merge_requests/26565) in GitLab 11.11. + +NOTE: **Note:** +Only available when creating clusters. Existing clusters not managed by GitLab +cannot become GitLab-managed later. + +You can choose to allow GitLab to manage your cluster for you. If your cluster is +managed by GitLab, resources for your projects will be automatically created. See the +[Access controls](#access-controls) section for details on which resources will +be created. + +If you choose to manage your own cluster, project-specific resources will not be created +automatically. If you are using [Auto DevOps](../../../topics/autodevops/index.md), you will +need to explicitly provide the `KUBE_NAMESPACE` [deployment variable](#deployment-variables) +that will be used by your deployment jobs, otherwise a namespace will be created for you. + +NOTE: **Note:** +If you [install applications](#installing-applications) on your cluster, GitLab will create +the resources required to run these even if you have chosen to manage your own cluster. + ## Base domain > [Introduced](https://gitlab.com/gitlab-org/gitlab-ce/merge_requests/24580) in GitLab 11.8. @@ -278,8 +305,8 @@ The following sections summarize which resources will be created on ABAC/RBAC cl | `gitlab-token` | `Secret` | Token for `gitlab` ServiceAccount | Creating a new GKE Cluster | | `tiller` | `ServiceAccount` | `gitlab-managed-apps` namespace | Installing Helm Tiller | | `tiller-admin` | `ClusterRoleBinding` | `cluster-admin` roleRef | Installing Helm Tiller | -| Project namespace | `ServiceAccount` | Uses namespace of Project | Creating/Adding a new GKE Cluster | -| Project namespace | `Secret` | Token for project ServiceAccount | Creating/Adding a new GKE Cluster | +| Project namespace | `ServiceAccount` | Uses namespace of Project | Deploying to a cluster | +| Project namespace | `Secret` | Token for project ServiceAccount | Deploying to a cluster | ### Role-based access control (RBAC) @@ -290,9 +317,12 @@ The following sections summarize which resources will be created on ABAC/RBAC cl | `gitlab-token` | `Secret` | Token for `gitlab` ServiceAccount | Creating a new GKE Cluster | | `tiller` | `ServiceAccount` | `gitlab-managed-apps` namespace | Installing Helm Tiller | | `tiller-admin` | `ClusterRoleBinding` | `cluster-admin` roleRef | Installing Helm Tiller | -| Project namespace | `ServiceAccount` | Uses namespace of Project | Creating/Adding a new GKE Cluster | -| Project namespace | `Secret` | Token for project ServiceAccount | Creating/Adding a new GKE Cluster | -| Project namespace | `RoleBinding` | [`edit`](https://kubernetes.io/docs/reference/access-authn-authz/rbac/#user-facing-roles) roleRef | Creating/Adding a new GKE Cluster | +| Project namespace | `ServiceAccount` | Uses namespace of Project | Deploying to a cluster | +| Project namespace | `Secret` | Token for project ServiceAccount | Deploying to a cluster | +| Project namespace | `RoleBinding` | [`edit`](https://kubernetes.io/docs/reference/access-authn-authz/rbac/#user-facing-roles) roleRef | Deploying to a cluster | + +NOTE: **Note:** +Project-specific resources are only created if your cluster is [managed by GitLab](#gitlab-managed-clusters). ### Security of GitLab Runners diff --git a/doc/user/project/pages/getting_started_part_two.md b/doc/user/project/pages/getting_started_part_two.md index 1034ba1733d..faf51154e3b 100644 --- a/doc/user/project/pages/getting_started_part_two.md +++ b/doc/user/project/pages/getting_started_part_two.md @@ -94,8 +94,9 @@ You can also take some **optional** further steps: - _Make it a user or group website._ To turn a **project website** forked from the Pages group into a **user/group** website, you'll need to: - - Rename it to `namespace.gitlab.io`: navigate to project's **Settings** > - expand **Advanced settings** > and scroll down to **Rename repository**. + - Rename it to `namespace.gitlab.io`: go to your project's + **Settings > General** and expand **Advanced**. Scroll down to + **Rename repository** and change the path to `namespace.gitlab.io`. - Adjust your SSG's [base URL](#urls-and-baseurls) from `"project-name"` to `""`. This setting will be at a different place for each SSG, as each of them have their own structure and file tree. Most likely, it will be in the SSG's diff --git a/doc/user/project/pipelines/img/pipeline_schedule_variables.png b/doc/user/project/pipelines/img/pipeline_schedule_variables.png Binary files differindex 74692add93a..29846206491 100644 --- a/doc/user/project/pipelines/img/pipeline_schedule_variables.png +++ b/doc/user/project/pipelines/img/pipeline_schedule_variables.png diff --git a/doc/user/project/pipelines/img/pipeline_schedules_new_form.png b/doc/user/project/pipelines/img/pipeline_schedules_new_form.png Binary files differindex 95203ec861b..e135dd51070 100644 --- a/doc/user/project/pipelines/img/pipeline_schedules_new_form.png +++ b/doc/user/project/pipelines/img/pipeline_schedules_new_form.png diff --git a/doc/user/project/pipelines/schedules.md b/doc/user/project/pipelines/schedules.md index 89f1beb6d1f..dd5427ab35a 100644 --- a/doc/user/project/pipelines/schedules.md +++ b/doc/user/project/pipelines/schedules.md @@ -79,7 +79,7 @@ For example, only two pipelines will be created per day if: To change the Sidekiq worker's frequency: -1. Edit the `pipeline_schedule_worker_cron` value in your instance's `gitlab.rb` file. +1. Edit the `gitlab_rails['pipeline_schedule_worker_cron']` value in your instance's `gitlab.rb` file. 1. [Restart GitLab](../../../administration/restart_gitlab.md). For GitLab.com, refer to the [dedicated settings page](../../gitlab_com/index.md#cron-jobs). diff --git a/jest.config.js b/jest.config.js index 0868547e654..84481642250 100644 --- a/jest.config.js +++ b/jest.config.js @@ -2,6 +2,10 @@ const IS_EE = require('./config/helpers/is_ee_env'); const reporters = ['default']; +// To have consistent date time parsing both in local and CI environments we set +// the timezone of the Node process. https://gitlab.com/gitlab-org/gitlab-ce/merge_requests/27738 +process.env.TZ = 'GMT'; + if (process.env.CI) { reporters.push([ 'jest-junit', diff --git a/lib/api/entities.rb b/lib/api/entities.rb index ee8480122c4..a228614f684 100644 --- a/lib/api/entities.rb +++ b/lib/api/entities.rb @@ -1156,22 +1156,33 @@ module API end end - class Release < TagRelease + class Release < Grape::Entity expose :name + expose :tag, as: :tag_name, if: lambda { |_, _| can_download_code? } + expose :description expose :description_html do |entity| MarkupHelper.markdown_field(entity, :description) end expose :created_at expose :author, using: Entities::UserBasic, if: -> (release, _) { release.author.present? } - expose :commit, using: Entities::Commit + expose :commit, using: Entities::Commit, if: lambda { |_, _| can_download_code? } expose :assets do - expose :assets_count, as: :count - expose :sources, using: Entities::Releases::Source + expose :assets_count, as: :count do |release, _| + assets_to_exclude = can_download_code? ? [] : [:sources] + release.assets_count(except: assets_to_exclude) + end + expose :sources, using: Entities::Releases::Source, if: lambda { |_, _| can_download_code? } expose :links, using: Entities::Releases::Link do |release, options| release.links.sorted end end + + private + + def can_download_code? + Ability.allowed?(options[:current_user], :download_code, object.project) + end end class Tag < Grape::Entity diff --git a/lib/api/internal.rb b/lib/api/internal.rb index 00f0bbab231..c82fd230d7a 100644 --- a/lib/api/internal.rb +++ b/lib/api/internal.rb @@ -265,6 +265,8 @@ module API params[:changes], push_options.as_json) if Feature.enabled?(:mr_push_options, default_enabled: true) + Gitlab::QueryLimiting.whitelist('https://gitlab.com/gitlab-org/gitlab-ce/issues/61359') + mr_options = push_options.get(:merge_request) output.merge!(process_mr_push_options(mr_options, project, user, params[:changes])) if mr_options.present? end diff --git a/lib/api/project_clusters.rb b/lib/api/project_clusters.rb index b62ec887183..dcc8d94fb79 100644 --- a/lib/api/project_clusters.rb +++ b/lib/api/project_clusters.rb @@ -54,6 +54,7 @@ module API requires :name, type: String, desc: 'Cluster name' optional :enabled, type: Boolean, default: true, desc: 'Determines if cluster is active or not, defaults to true' optional :domain, type: String, desc: 'Cluster base domain' + optional :managed, type: Boolean, default: true, desc: 'Determines if GitLab will manage namespaces and service accounts for this cluster, defaults to true' requires :platform_kubernetes_attributes, type: Hash, desc: %q(Platform Kubernetes data) do requires :api_url, type: String, allow_blank: false, desc: 'URL to access the Kubernetes API' requires :token, type: String, desc: 'Token to authenticate against Kubernetes' diff --git a/lib/api/releases.rb b/lib/api/releases.rb index cb85028f22c..6b17f4317db 100644 --- a/lib/api/releases.rb +++ b/lib/api/releases.rb @@ -23,7 +23,7 @@ module API get ':id/releases' do releases = ::ReleasesFinder.new(user_project, current_user).execute - present paginate(releases), with: Entities::Release + present paginate(releases), with: Entities::Release, current_user: current_user end desc 'Get a single project release' do @@ -34,9 +34,9 @@ module API requires :tag_name, type: String, desc: 'The name of the tag', as: :tag end get ':id/releases/:tag_name', requirements: RELEASE_ENDPOINT_REQUIREMETS do - authorize_read_release! + authorize_download_code! - present release, with: Entities::Release + present release, with: Entities::Release, current_user: current_user end desc 'Create a new release' do @@ -63,7 +63,7 @@ module API .execute if result[:status] == :success - present result[:release], with: Entities::Release + present result[:release], with: Entities::Release, current_user: current_user else render_api_error!(result[:message], result[:http_status]) end @@ -86,7 +86,7 @@ module API .execute if result[:status] == :success - present result[:release], with: Entities::Release + present result[:release], with: Entities::Release, current_user: current_user else render_api_error!(result[:message], result[:http_status]) end @@ -107,7 +107,7 @@ module API .execute if result[:status] == :success - present result[:release], with: Entities::Release + present result[:release], with: Entities::Release, current_user: current_user else render_api_error!(result[:message], result[:http_status]) end @@ -135,6 +135,10 @@ module API authorize! :destroy_release, release end + def authorize_download_code! + authorize! :download_code, release + end + def release @release ||= user_project.releases.find_by_tag(params[:tag]) end diff --git a/lib/gitlab/ci/build/prerequisite/kubernetes_namespace.rb b/lib/gitlab/ci/build/prerequisite/kubernetes_namespace.rb index bb2b209e793..dbdc59505ac 100644 --- a/lib/gitlab/ci/build/prerequisite/kubernetes_namespace.rb +++ b/lib/gitlab/ci/build/prerequisite/kubernetes_namespace.rb @@ -7,6 +7,7 @@ module Gitlab class KubernetesNamespace < Base def unmet? deployment_cluster.present? && + deployment_cluster.managed? && !deployment_cluster.project_type? && kubernetes_namespace.new_record? end diff --git a/lib/gitlab/ci/status/stage/factory.rb b/lib/gitlab/ci/status/stage/factory.rb index 58f4642510b..e50b0853725 100644 --- a/lib/gitlab/ci/status/stage/factory.rb +++ b/lib/gitlab/ci/status/stage/factory.rb @@ -6,7 +6,8 @@ module Gitlab module Stage class Factory < Status::Factory def self.extended_statuses - [Status::SuccessWarning] + [[Status::SuccessWarning], + [Status::Stage::PlayManual]] end def self.common_helpers diff --git a/lib/gitlab/ci/status/stage/play_manual.rb b/lib/gitlab/ci/status/stage/play_manual.rb new file mode 100644 index 00000000000..ac3fc0912fa --- /dev/null +++ b/lib/gitlab/ci/status/stage/play_manual.rb @@ -0,0 +1,43 @@ +# frozen_string_literal: true + +module Gitlab + module Ci + module Status + module Stage + class PlayManual < Status::Extended + include Gitlab::Routing + + def action_icon + 'play' + end + + def action_title + 'Play all manual' + end + + def action_path + pipeline = subject.pipeline + + project_stage_play_manual_path(pipeline.project, pipeline, subject.name) + end + + def action_method + :post + end + + def action_button_title + _('Play all manual') + end + + def self.matches?(stage, user) + stage.manual_playable? + end + + def has_action? + true + end + end + end + end + end +end diff --git a/lib/gitlab/ci/templates/Security/Dependency-Scanning.gitlab-ci.yml b/lib/gitlab/ci/templates/Security/Dependency-Scanning.gitlab-ci.yml index 263221329ab..8dd9775c583 100644 --- a/lib/gitlab/ci/templates/Security/Dependency-Scanning.gitlab-ci.yml +++ b/lib/gitlab/ci/templates/Security/Dependency-Scanning.gitlab-ci.yml @@ -35,6 +35,7 @@ dependency_scanning: DS_ANALYZER_IMAGE_PREFIX \ DS_ANALYZER_IMAGE_TAG \ DS_DEFAULT_ANALYZERS \ + DS_EXCLUDED_PATHS \ DEP_SCAN_DISABLE_REMOTE_CHECKS \ DS_DOCKER_CLIENT_NEGOTIATION_TIMEOUT \ DS_PULL_ANALYZER_IMAGE_TIMEOUT \ diff --git a/lib/gitlab/ci/templates/Security/SAST.gitlab-ci.yml b/lib/gitlab/ci/templates/Security/SAST.gitlab-ci.yml index f0152cd4537..706692e063b 100644 --- a/lib/gitlab/ci/templates/Security/SAST.gitlab-ci.yml +++ b/lib/gitlab/ci/templates/Security/SAST.gitlab-ci.yml @@ -35,6 +35,8 @@ sast: SAST_ANALYZER_IMAGE_PREFIX \ SAST_ANALYZER_IMAGE_TAG \ SAST_DEFAULT_ANALYZERS \ + SAST_EXCLUDED_PATHS \ + SAST_BANDIT_EXCLUDED_PATHS \ SAST_BRAKEMAN_LEVEL \ SAST_GOSEC_LEVEL \ SAST_FLAWFINDER_LEVEL \ diff --git a/lib/gitlab/ci/trace.rb b/lib/gitlab/ci/trace.rb index bf5f2a31f0e..dfae260239e 100644 --- a/lib/gitlab/ci/trace.rb +++ b/lib/gitlab/ci/trace.rb @@ -209,10 +209,7 @@ module Gitlab end def paths - [ - default_path, - deprecated_path - ].compact + [default_path] end def default_directory @@ -227,15 +224,6 @@ module Gitlab File.join(default_directory, "#{job.id}.log") end - def deprecated_path - File.join( - Settings.gitlab_ci.builds_path, - job.created_at.utc.strftime("%Y_%m"), - job.project.ci_id.to_s, - "#{job.id}.log" - ) if job.project&.ci_id - end - def trace_artifact job.job_artifacts_trace end diff --git a/lib/gitlab/data_builder/deployment.rb b/lib/gitlab/data_builder/deployment.rb index 26705dd1f6f..f11e032ab84 100644 --- a/lib/gitlab/data_builder/deployment.rb +++ b/lib/gitlab/data_builder/deployment.rb @@ -15,7 +15,9 @@ module Gitlab project: deployment.project.hook_attrs, short_sha: deployment.short_sha, user: deployment.user.hook_attrs, - commit_url: Gitlab::UrlBuilder.build(deployment.commit) + user_url: Gitlab::UrlBuilder.build(deployment.user), + commit_url: Gitlab::UrlBuilder.build(deployment.commit), + commit_title: deployment.commit.title } end end diff --git a/lib/gitlab/git/repository.rb b/lib/gitlab/git/repository.rb index 55bd77f6c4a..508499f227c 100644 --- a/lib/gitlab/git/repository.rb +++ b/lib/gitlab/git/repository.rb @@ -834,7 +834,8 @@ module Gitlab gitaly_repository_client.create_from_snapshot(url, auth) end - def rebase(user, rebase_id, branch:, branch_sha:, remote_repository:, remote_branch:) + # DEPRECATED: https://gitlab.com/gitlab-org/gitaly/issues/1628 + def rebase_deprecated(user, rebase_id, branch:, branch_sha:, remote_repository:, remote_branch:) wrapped_gitaly_errors do gitaly_operation_client.user_rebase(user, rebase_id, branch: branch, @@ -844,6 +845,20 @@ module Gitlab end end + def rebase(user, rebase_id, branch:, branch_sha:, remote_repository:, remote_branch:, &block) + wrapped_gitaly_errors do + gitaly_operation_client.rebase( + user, + rebase_id, + branch: branch, + branch_sha: branch_sha, + remote_repository: remote_repository, + remote_branch: remote_branch, + &block + ) + end + end + def rebase_in_progress?(rebase_id) wrapped_gitaly_errors do gitaly_repository_client.rebase_in_progress?(rebase_id) diff --git a/lib/gitlab/gitaly_client/operation_service.rb b/lib/gitlab/gitaly_client/operation_service.rb index b0f328ce3d4..e4a59ee3f9b 100644 --- a/lib/gitlab/gitaly_client/operation_service.rb +++ b/lib/gitlab/gitaly_client/operation_service.rb @@ -197,6 +197,7 @@ module Gitlab start_repository: start_repository) end + # DEPRECATED: https://gitlab.com/gitlab-org/gitaly/issues/1628 def user_rebase(user, rebase_id, branch:, branch_sha:, remote_repository:, remote_branch:) request = Gitaly::UserRebaseRequest.new( repository: @gitaly_repo, @@ -225,6 +226,49 @@ module Gitlab end end + def rebase(user, rebase_id, branch:, branch_sha:, remote_repository:, remote_branch:) + request_enum = QueueEnumerator.new + rebase_sha = nil + + response_enum = GitalyClient.call( + @repository.storage, + :operation_service, + :user_rebase_confirmable, + request_enum.each, + remote_storage: remote_repository.storage + ) + + # First request + request_enum.push( + Gitaly::UserRebaseConfirmableRequest.new( + header: Gitaly::UserRebaseConfirmableRequest::Header.new( + repository: @gitaly_repo, + user: Gitlab::Git::User.from_gitlab(user).to_gitaly, + rebase_id: rebase_id.to_s, + branch: encode_binary(branch), + branch_sha: branch_sha, + remote_repository: remote_repository.gitaly_repository, + remote_branch: encode_binary(remote_branch) + ) + ) + ) + + perform_next_gitaly_rebase_request(response_enum) do |response| + rebase_sha = response.rebase_sha + end + + yield rebase_sha + + # Second request confirms with gitaly to finalize the rebase + request_enum.push(Gitaly::UserRebaseConfirmableRequest.new(apply: true)) + + perform_next_gitaly_rebase_request(response_enum) + + rebase_sha + ensure + request_enum.close + end + def user_squash(user, squash_id, branch, start_sha, end_sha, author, message) request = Gitaly::UserSquashRequest.new( repository: @gitaly_repo, @@ -346,6 +390,20 @@ module Gitlab private + def perform_next_gitaly_rebase_request(response_enum) + response = response_enum.next + + if response.pre_receive_error.present? + raise Gitlab::Git::PreReceiveError, response.pre_receive_error + elsif response.git_error.present? + raise Gitlab::Git::Repository::GitError, response.git_error + end + + yield response if block_given? + + response + end + def call_cherry_pick_or_revert(rpc, user:, commit:, branch_name:, message:, start_branch_name:, start_repository:) request_class = "Gitaly::User#{rpc.to_s.camelcase}Request".constantize diff --git a/lib/gitlab/gitaly_client/ref_service.rb b/lib/gitlab/gitaly_client/ref_service.rb index 6f6698607d9..b7d509dfa48 100644 --- a/lib/gitlab/gitaly_client/ref_service.rb +++ b/lib/gitlab/gitaly_client/ref_service.rb @@ -239,6 +239,12 @@ module Gitlab messages end + def pack_refs + request = Gitaly::PackRefsRequest.new(repository: @gitaly_repo) + + GitalyClient.call(@storage, :ref_service, :pack_refs, request) + end + private def consume_refs_response(response) diff --git a/lib/gitlab/url_builder.rb b/lib/gitlab/url_builder.rb index 169ce8ab026..42cf1ec1f0e 100644 --- a/lib/gitlab/url_builder.rb +++ b/lib/gitlab/url_builder.rb @@ -32,6 +32,8 @@ module Gitlab milestone_url(object) when ::Ci::Build project_job_url(object.project, object) + when User + user_url(object) else raise NotImplementedError.new("No URL builder defined for #{object.class}") end diff --git a/locale/gitlab.pot b/locale/gitlab.pot index cae94fb2af4..0ac7282baac 100644 --- a/locale/gitlab.pot +++ b/locale/gitlab.pot @@ -110,7 +110,7 @@ msgstr "" msgid "%{commit_author_link} authored %{commit_timeago}" msgstr "" -msgid "%{counter_storage} (%{counter_repositories} repositories, %{counter_build_artifacts} build artifacts, %{counter_lfs_objects} LFS)" +msgid "%{counter_repositories} repositories, %{counter_build_artifacts} build artifacts, %{counter_lfs_objects} LFS" msgstr "" msgid "%{count} more" @@ -172,6 +172,12 @@ msgstr "" msgid "%{mrText}, this issue will be closed automatically." msgstr "" +msgid "%{name} contained %{resultsString}" +msgstr "" + +msgid "%{name} found %{resultsString}" +msgstr "" + msgid "%{number_commits_behind} commits behind %{default_branch}, %{number_commits_ahead} commits ahead" msgstr "" @@ -529,6 +535,9 @@ msgstr "" msgid "Add bold text" msgstr "" +msgid "Add comment now" +msgstr "" + msgid "Add header and footer to emails. Please note that color settings will only be applied within the application interface" msgstr "" @@ -562,6 +571,9 @@ msgstr "" msgid "Add to project" msgstr "" +msgid "Add to review" +msgstr "" + msgid "Add todo" msgstr "" @@ -844,6 +856,9 @@ msgstr "" msgid "An error occurred while fetching markdown preview" msgstr "" +msgid "An error occurred while fetching projects autocomplete." +msgstr "" + msgid "An error occurred while fetching sidebar data" msgstr "" @@ -889,6 +904,9 @@ msgstr "" msgid "An error occurred while making the request." msgstr "" +msgid "An error occurred while moving the issue." +msgstr "" + msgid "An error occurred while parsing recent searches" msgstr "" @@ -2017,6 +2035,9 @@ msgstr "" msgid "ClusterIntegration|All data will be deleted and cannot be restored." msgstr "" +msgid "ClusterIntegration|Allow GitLab to manage namespace and service accounts for this cluster." +msgstr "" + msgid "ClusterIntegration|Alternatively" msgstr "" @@ -2140,6 +2161,9 @@ msgstr "" msgid "ClusterIntegration|GitLab Runner connects to the repository and executes CI/CD jobs, pushing results back and deploying applications to production." msgstr "" +msgid "ClusterIntegration|GitLab-managed cluster" +msgstr "" + msgid "ClusterIntegration|Google Cloud Platform project" msgstr "" @@ -3860,6 +3884,9 @@ msgstr "" msgid "Error loading viewer" msgstr "" +msgid "Error occurred when fetching sidebar data" +msgstr "" + msgid "Error occurred when toggling the notification subscription" msgstr "" @@ -6821,6 +6848,9 @@ msgstr "" msgid "Play" msgstr "" +msgid "Play all manual" +msgstr "" + msgid "Please %{link_to_register} or %{link_to_sign_in} to comment" msgstr "" @@ -8855,6 +8885,9 @@ msgstr "" msgid "Start a new merge request" msgstr "" +msgid "Start a review" +msgstr "" + msgid "Start and due date" msgstr "" diff --git a/spec/controllers/groups/clusters_controller_spec.rb b/spec/controllers/groups/clusters_controller_spec.rb index e5180ec5c5c..7349cb7094c 100644 --- a/spec/controllers/groups/clusters_controller_spec.rb +++ b/spec/controllers/groups/clusters_controller_spec.rb @@ -189,6 +189,7 @@ describe Groups::ClustersController do { cluster: { name: 'new-cluster', + managed: '1', provider_gcp_attributes: { gcp_project_id: 'gcp-project-12345', legacy_abac: legacy_abac_param @@ -218,6 +219,7 @@ describe Groups::ClustersController do expect(cluster).to be_gcp expect(cluster).to be_kubernetes expect(cluster.provider_gcp).to be_legacy_abac + expect(cluster).to be_managed end context 'when legacy_abac param is false' do @@ -278,6 +280,7 @@ describe Groups::ClustersController do { cluster: { name: 'new-cluster', + managed: '1', platform_kubernetes_attributes: { api_url: 'http://my-url', token: 'test' @@ -303,6 +306,7 @@ describe Groups::ClustersController do expect(response).to redirect_to(group_cluster_path(group, cluster)) expect(cluster).to be_user expect(cluster).to be_kubernetes + expect(cluster).to be_managed end end @@ -334,6 +338,29 @@ describe Groups::ClustersController do expect(cluster).to be_platform_kubernetes_rbac end end + + context 'when creates a user-managed cluster' do + let(:params) do + { + cluster: { + name: 'new-cluster', + managed: '0', + platform_kubernetes_attributes: { + api_url: 'http://my-url', + token: 'test', + authorization_type: 'rbac' + } + } + } + end + + it 'creates a new user-managed cluster' do + go + + cluster = group.clusters.first + expect(cluster.managed?).to be_falsy + end + end end describe 'security' do diff --git a/spec/controllers/projects/clusters_controller_spec.rb b/spec/controllers/projects/clusters_controller_spec.rb index d94c18ddc02..8d37bd82d21 100644 --- a/spec/controllers/projects/clusters_controller_spec.rb +++ b/spec/controllers/projects/clusters_controller_spec.rb @@ -165,6 +165,7 @@ describe Projects::ClustersController do { cluster: { name: 'new-cluster', + managed: '1', provider_gcp_attributes: { gcp_project_id: 'gcp-project-12345', legacy_abac: legacy_abac_param @@ -191,6 +192,7 @@ describe Projects::ClustersController do expect(project.clusters.first).to be_gcp expect(project.clusters.first).to be_kubernetes expect(project.clusters.first.provider_gcp).to be_legacy_abac + expect(project.clusters.first.managed?).to be_truthy end context 'when legacy_abac param is false' do @@ -251,6 +253,7 @@ describe Projects::ClustersController do { cluster: { name: 'new-cluster', + managed: '1', platform_kubernetes_attributes: { api_url: 'http://my-url', token: 'test', @@ -302,9 +305,35 @@ describe Projects::ClustersController do expect(response).to redirect_to(project_cluster_path(project, project.clusters.first)) - expect(project.clusters.first).to be_user - expect(project.clusters.first).to be_kubernetes - expect(project.clusters.first).to be_platform_kubernetes_rbac + cluster = project.clusters.first + + expect(cluster).to be_user + expect(cluster).to be_kubernetes + expect(cluster).to be_platform_kubernetes_rbac + end + end + + context 'when creates a user-managed cluster' do + let(:params) do + { + cluster: { + name: 'new-cluster', + managed: '0', + platform_kubernetes_attributes: { + api_url: 'http://my-url', + token: 'test', + namespace: 'aaa', + authorization_type: 'rbac' + } + } + } + end + + it 'creates a new user-managed cluster' do + go + cluster = project.clusters.first + + expect(cluster.managed?).to be_falsy end end end diff --git a/spec/controllers/projects/stages_controller_spec.rb b/spec/controllers/projects/stages_controller_spec.rb new file mode 100644 index 00000000000..a91e3523fd7 --- /dev/null +++ b/spec/controllers/projects/stages_controller_spec.rb @@ -0,0 +1,72 @@ +# frozen_string_literal: true + +require 'spec_helper' + +describe Projects::StagesController do + let(:user) { create(:user) } + let(:project) { create(:project, :repository) } + + before do + sign_in(user) + end + + describe 'POST #play_manual.json' do + let(:pipeline) { create(:ci_pipeline, project: project) } + let(:stage_name) { 'test' } + + before do + create_manual_build(pipeline, 'test', 'rspec 1/2') + create_manual_build(pipeline, 'test', 'rspec 2/2') + + pipeline.reload + end + + context 'when user does not have access' do + it 'returns not authorized' do + play_manual_stage! + + expect(response).to have_gitlab_http_status(404) + end + end + + context 'when user has access' do + before do + project.add_maintainer(user) + end + + context 'when the stage does not exists' do + let(:stage_name) { 'deploy' } + + it 'fails to play all manual' do + play_manual_stage! + + expect(response).to have_gitlab_http_status(:not_found) + end + end + + context 'when the stage exists' do + it 'starts all manual jobs' do + expect(pipeline.builds.manual.count).to eq(2) + + play_manual_stage! + + expect(response).to have_gitlab_http_status(:ok) + expect(pipeline.builds.manual.count).to eq(0) + end + end + end + + def play_manual_stage! + post :play_manual, params: { + namespace_id: project.namespace, + project_id: project, + id: pipeline.id, + stage_name: stage_name + }, format: :json + end + + def create_manual_build(pipeline, stage, name) + create(:ci_build, :manual, pipeline: pipeline, stage: stage, name: name) + end + end +end diff --git a/spec/factories/clusters/clusters.rb b/spec/factories/clusters/clusters.rb index 97405ec7c58..6eb0194b710 100644 --- a/spec/factories/clusters/clusters.rb +++ b/spec/factories/clusters/clusters.rb @@ -65,7 +65,7 @@ FactoryBot.define do domain 'example.com' end - trait :user_managed do + trait :not_managed do managed false end end diff --git a/spec/features/projects/pipelines/pipeline_spec.rb b/spec/features/projects/pipelines/pipeline_spec.rb index 3825468cf71..a1115b514d3 100644 --- a/spec/features/projects/pipelines/pipeline_spec.rb +++ b/spec/features/projects/pipelines/pipeline_spec.rb @@ -236,6 +236,20 @@ describe 'Pipeline', :js do end end + context 'when the pipeline has manual stage' do + before do + create(:ci_build, :manual, pipeline: pipeline, stage: 'publish', name: 'CentOS') + create(:ci_build, :manual, pipeline: pipeline, stage: 'publish', name: 'Debian') + create(:ci_build, :manual, pipeline: pipeline, stage: 'publish', name: 'OpenSUDE') + + visit_pipeline + end + + it 'displays play all button' do + expect(page).to have_selector('.js-stage-action') + end + end + context 'page tabs' do before do visit_pipeline diff --git a/spec/fixtures/api/schemas/public_api/v4/release.json b/spec/fixtures/api/schemas/public_api/v4/release.json index 6612c2a9911..6ea0781c1ed 100644 --- a/spec/fixtures/api/schemas/public_api/v4/release.json +++ b/spec/fixtures/api/schemas/public_api/v4/release.json @@ -1,12 +1,33 @@ { "type": "object", - "required" : [ - "tag_name", - "description" - ], - "properties" : { - "tag_name": { "type": ["string", "null"] }, - "description": { "type": "string" } + "required": ["name", "tag_name", "commit"], + "properties": { + "name": { "type": "string" }, + "tag_name": { "type": "string" }, + "description": { "type": "string" }, + "description_html": { "type": "string" }, + "created_at": { "type": "date" }, + "commit": { + "oneOf": [{ "type": "null" }, { "$ref": "commit/basic.json" }] + }, + "author": { + "oneOf": [{ "type": "null" }, { "$ref": "user/basic.json" }] + }, + "assets": { + "required": ["count", "links", "sources"], + "properties": { + "count": { "type": "integer" }, + "links": { "$ref": "../../release/links.json" }, + "sources": { + "type": "array", + "items": { + "format": "zip", + "url": "string" + } + } + }, + "additionalProperties": false + } }, "additionalProperties": false } diff --git a/spec/fixtures/api/schemas/public_api/v4/release/release_for_guest.json b/spec/fixtures/api/schemas/public_api/v4/release/release_for_guest.json new file mode 100644 index 00000000000..e78398ad1d5 --- /dev/null +++ b/spec/fixtures/api/schemas/public_api/v4/release/release_for_guest.json @@ -0,0 +1,22 @@ +{ + "type": "object", + "required": ["name"], + "properties": { + "name": { "type": "string" }, + "description": { "type": "string" }, + "description_html": { "type": "string" }, + "created_at": { "type": "date" }, + "author": { + "oneOf": [{ "type": "null" }, { "$ref": "../user/basic.json" }] + }, + "assets": { + "required": ["count", "links"], + "properties": { + "count": { "type": "integer" }, + "links": { "$ref": "../../../release/links.json" } + }, + "additionalProperties": false + } + }, + "additionalProperties": false +} diff --git a/spec/fixtures/api/schemas/public_api/v4/release/releases_for_guest.json b/spec/fixtures/api/schemas/public_api/v4/release/releases_for_guest.json new file mode 100644 index 00000000000..c13966b28e9 --- /dev/null +++ b/spec/fixtures/api/schemas/public_api/v4/release/releases_for_guest.json @@ -0,0 +1,4 @@ +{ + "type": "array", + "items": { "$ref": "release_for_guest.json" } +} diff --git a/spec/fixtures/api/schemas/public_api/v4/release/tag_release.json b/spec/fixtures/api/schemas/public_api/v4/release/tag_release.json new file mode 100644 index 00000000000..6612c2a9911 --- /dev/null +++ b/spec/fixtures/api/schemas/public_api/v4/release/tag_release.json @@ -0,0 +1,12 @@ +{ + "type": "object", + "required" : [ + "tag_name", + "description" + ], + "properties" : { + "tag_name": { "type": ["string", "null"] }, + "description": { "type": "string" } + }, + "additionalProperties": false +} diff --git a/spec/fixtures/api/schemas/public_api/v4/releases.json b/spec/fixtures/api/schemas/public_api/v4/releases.json new file mode 100644 index 00000000000..e26215707fe --- /dev/null +++ b/spec/fixtures/api/schemas/public_api/v4/releases.json @@ -0,0 +1,4 @@ +{ + "type": "array", + "items": { "$ref": "release.json" } +} diff --git a/spec/fixtures/api/schemas/public_api/v4/tag.json b/spec/fixtures/api/schemas/public_api/v4/tag.json index 10d4edb7ffb..5713ea1f526 100644 --- a/spec/fixtures/api/schemas/public_api/v4/tag.json +++ b/spec/fixtures/api/schemas/public_api/v4/tag.json @@ -14,7 +14,7 @@ "release": { "oneOf": [ { "type": "null" }, - { "$ref": "release.json" } + { "$ref": "release/tag_release.json" } ] } }, diff --git a/spec/helpers/storage_helper_spec.rb b/spec/helpers/storage_helper_spec.rb index 03df9deafa1..50c74a7c2f9 100644 --- a/spec/helpers/storage_helper_spec.rb +++ b/spec/helpers/storage_helper_spec.rb @@ -18,4 +18,28 @@ describe StorageHelper do expect(helper.storage_counter(100_000_000_000_000_000_000_000)).to eq("86,736.2 EB") end end + + describe "#storage_counters_details" do + let(:namespace) { create :namespace } + let(:project) do + create(:project, + namespace: namespace, + statistics: build(:project_statistics, + repository_size: 10.kilobytes, + lfs_objects_size: 20.gigabytes, + build_artifacts_size: 30.megabytes)) + end + + let(:message) { '10 KB repositories, 30 MB build artifacts, 20 GB LFS' } + + it 'works on ProjectStatistics' do + expect(helper.storage_counters_details(project.statistics)).to eq(message) + end + + it 'works on Namespace.with_statistics' do + namespace_stats = Namespace.with_statistics.find(project.namespace.id) + + expect(helper.storage_counters_details(namespace_stats)).to eq(message) + end + end end diff --git a/spec/javascripts/diffs/components/diff_content_spec.js b/spec/javascripts/diffs/components/diff_content_spec.js index bc9288e4150..124bdeea45d 100644 --- a/spec/javascripts/diffs/components/diff_content_spec.js +++ b/spec/javascripts/diffs/components/diff_content_spec.js @@ -29,6 +29,10 @@ describe('DiffContent', () => { }); }); + afterEach(() => { + vm.$destroy(); + }); + describe('text based files', () => { it('should render diff inline view', done => { vm.$store.state.diffs.diffViewType = 'inline'; @@ -49,6 +53,16 @@ describe('DiffContent', () => { done(); }); }); + + it('renders rendering more lines loading icon', done => { + vm.diffFile.renderingLines = true; + + vm.$nextTick(() => { + expect(vm.$el.querySelector('.loading-container')).not.toBe(null); + + done(); + }); + }); }); describe('empty files', () => { diff --git a/spec/javascripts/diffs/mock_data/diff_file.js b/spec/javascripts/diffs/mock_data/diff_file.js index 32af9ea8ddd..27428197c1c 100644 --- a/spec/javascripts/diffs/mock_data/diff_file.js +++ b/spec/javascripts/diffs/mock_data/diff_file.js @@ -240,4 +240,5 @@ export default { }, ], discussions: [], + renderingLines: false, }; diff --git a/spec/javascripts/diffs/store/actions_spec.js b/spec/javascripts/diffs/store/actions_spec.js index bca99caa920..c82dcadd2f1 100644 --- a/spec/javascripts/diffs/store/actions_spec.js +++ b/spec/javascripts/diffs/store/actions_spec.js @@ -36,6 +36,7 @@ import actions, { fetchFullDiff, toggleFullDiff, setFileCollapsed, + setExpandedDiffLines, } from '~/diffs/store/actions'; import eventHub from '~/notes/event_hub'; import * as types from '~/diffs/store/mutation_types'; @@ -879,9 +880,9 @@ describe('DiffsStoreActions', () => { it('commits REQUEST_FULL_DIFF', done => { testAction( receiveFullDiffSucess, - { filePath: 'test', data: 'test' }, + { filePath: 'test' }, {}, - [{ type: types.RECEIVE_FULL_DIFF_SUCCESS, payload: { filePath: 'test', data: 'test' } }], + [{ type: types.RECEIVE_FULL_DIFF_SUCCESS, payload: { filePath: 'test' } }], [], done, ); @@ -903,11 +904,8 @@ describe('DiffsStoreActions', () => { describe('fetchFullDiff', () => { let mock; - let scrollToElementSpy; beforeEach(() => { - scrollToElementSpy = spyOnDependency(actions, 'scrollToElement').and.stub(); - mock = new MockAdapter(axios); }); @@ -921,28 +919,23 @@ describe('DiffsStoreActions', () => { }); it('dispatches receiveFullDiffSucess', done => { + const file = { + context_lines_path: `${gl.TEST_HOST}/context`, + file_path: 'test', + file_hash: 'test', + }; testAction( fetchFullDiff, - { context_lines_path: `${gl.TEST_HOST}/context`, file_path: 'test', file_hash: 'test' }, + file, null, [], - [{ type: 'receiveFullDiffSucess', payload: { filePath: 'test', data: ['test'] } }], + [ + { type: 'receiveFullDiffSucess', payload: { filePath: 'test' } }, + { type: 'setExpandedDiffLines', payload: { file, data: ['test'] } }, + ], done, ); }); - - it('scrolls to element', done => { - fetchFullDiff( - { dispatch() {} }, - { context_lines_path: `${gl.TEST_HOST}/context`, file_path: 'test', file_hash: 'test' }, - ) - .then(() => { - expect(scrollToElementSpy).toHaveBeenCalledWith('#test'); - - done(); - }) - .catch(done.fail); - }); }); describe('error', () => { @@ -999,4 +992,63 @@ describe('DiffsStoreActions', () => { ); }); }); + + describe('setExpandedDiffLines', () => { + beforeEach(() => { + spyOnDependency(actions, 'idleCallback').and.callFake(cb => { + cb({ timeRemaining: () => 50 }); + }); + }); + + it('commits SET_CURRENT_VIEW_DIFF_FILE_LINES when lines less than MAX_RENDERING_DIFF_LINES', done => { + spyOnDependency(actions, 'convertExpandLines').and.callFake(() => ['test']); + + testAction( + setExpandedDiffLines, + { file: { file_path: 'path' }, data: [] }, + { diffViewType: 'inline' }, + [ + { + type: 'SET_HIDDEN_VIEW_DIFF_FILE_LINES', + payload: { filePath: 'path', lines: ['test'] }, + }, + { + type: 'SET_CURRENT_VIEW_DIFF_FILE_LINES', + payload: { filePath: 'path', lines: ['test'] }, + }, + ], + [], + done, + ); + }); + + it('commits ADD_CURRENT_VIEW_DIFF_FILE_LINES when lines more than MAX_RENDERING_DIFF_LINES', done => { + const lines = new Array(501).fill().map((_, i) => `line-${i}`); + spyOnDependency(actions, 'convertExpandLines').and.callFake(() => lines); + + testAction( + setExpandedDiffLines, + { file: { file_path: 'path' }, data: [] }, + { diffViewType: 'inline' }, + [ + { + type: 'SET_HIDDEN_VIEW_DIFF_FILE_LINES', + payload: { filePath: 'path', lines }, + }, + { + type: 'SET_CURRENT_VIEW_DIFF_FILE_LINES', + payload: { filePath: 'path', lines: lines.slice(0, 200) }, + }, + { type: 'TOGGLE_DIFF_FILE_RENDERING_MORE', payload: 'path' }, + ...new Array(301).fill().map((_, i) => ({ + type: 'ADD_CURRENT_VIEW_DIFF_FILE_LINES', + payload: { filePath: 'path', line: `line-${i + 200}` }, + })), + { type: 'TOGGLE_DIFF_FILE_RENDERING_MORE', payload: 'path' }, + ], + [], + done, + ); + }); + }); }); diff --git a/spec/javascripts/diffs/store/mutations_spec.js b/spec/javascripts/diffs/store/mutations_spec.js index 5556a994a14..fa193e1d3b9 100644 --- a/spec/javascripts/diffs/store/mutations_spec.js +++ b/spec/javascripts/diffs/store/mutations_spec.js @@ -756,4 +756,98 @@ describe('DiffsStoreMutations', () => { expect(state.diffFiles[0].viewer.collapsed).toBe(true); }); }); + + describe('SET_HIDDEN_VIEW_DIFF_FILE_LINES', () => { + [ + { current: 'highlighted', hidden: 'parallel', diffViewType: 'inline' }, + { current: 'parallel', hidden: 'highlighted', diffViewType: 'parallel' }, + ].forEach(({ current, hidden, diffViewType }) => { + it(`sets the ${hidden} lines when diff view is ${diffViewType}`, () => { + const file = { file_path: 'test', parallel_diff_lines: [], highlighted_diff_lines: [] }; + const state = { + diffFiles: [file], + diffViewType, + }; + + mutations[types.SET_HIDDEN_VIEW_DIFF_FILE_LINES](state, { + filePath: 'test', + lines: ['test'], + }); + + expect(file[`${current}_diff_lines`]).toEqual([]); + expect(file[`${hidden}_diff_lines`]).toEqual(['test']); + }); + }); + }); + + describe('SET_CURRENT_VIEW_DIFF_FILE_LINES', () => { + [ + { current: 'highlighted', hidden: 'parallel', diffViewType: 'inline' }, + { current: 'parallel', hidden: 'highlighted', diffViewType: 'parallel' }, + ].forEach(({ current, hidden, diffViewType }) => { + it(`sets the ${current} lines when diff view is ${diffViewType}`, () => { + const file = { file_path: 'test', parallel_diff_lines: [], highlighted_diff_lines: [] }; + const state = { + diffFiles: [file], + diffViewType, + }; + + mutations[types.SET_CURRENT_VIEW_DIFF_FILE_LINES](state, { + filePath: 'test', + lines: ['test'], + }); + + expect(file[`${current}_diff_lines`]).toEqual(['test']); + expect(file[`${hidden}_diff_lines`]).toEqual([]); + }); + }); + }); + + describe('ADD_CURRENT_VIEW_DIFF_FILE_LINES', () => { + [ + { current: 'highlighted', hidden: 'parallel', diffViewType: 'inline' }, + { current: 'parallel', hidden: 'highlighted', diffViewType: 'parallel' }, + ].forEach(({ current, hidden, diffViewType }) => { + it(`pushes to ${current} lines when diff view is ${diffViewType}`, () => { + const file = { file_path: 'test', parallel_diff_lines: [], highlighted_diff_lines: [] }; + const state = { + diffFiles: [file], + diffViewType, + }; + + mutations[types.ADD_CURRENT_VIEW_DIFF_FILE_LINES](state, { + filePath: 'test', + line: 'test', + }); + + expect(file[`${current}_diff_lines`]).toEqual(['test']); + expect(file[`${hidden}_diff_lines`]).toEqual([]); + + mutations[types.ADD_CURRENT_VIEW_DIFF_FILE_LINES](state, { + filePath: 'test', + line: 'test2', + }); + + expect(file[`${current}_diff_lines`]).toEqual(['test', 'test2']); + expect(file[`${hidden}_diff_lines`]).toEqual([]); + }); + }); + }); + + describe('TOGGLE_DIFF_FILE_RENDERING_MORE', () => { + it('toggles renderingLines on file', () => { + const file = { file_path: 'test', renderingLines: false }; + const state = { + diffFiles: [file], + }; + + mutations[types.TOGGLE_DIFF_FILE_RENDERING_MORE](state, 'test'); + + expect(file.renderingLines).toBe(true); + + mutations[types.TOGGLE_DIFF_FILE_RENDERING_MORE](state, 'test'); + + expect(file.renderingLines).toBe(false); + }); + }); }); diff --git a/spec/javascripts/pipelines/graph/action_component_spec.js b/spec/javascripts/pipelines/graph/action_component_spec.js index 95717d659b8..321497b35b5 100644 --- a/spec/javascripts/pipelines/graph/action_component_spec.js +++ b/spec/javascripts/pipelines/graph/action_component_spec.js @@ -66,5 +66,16 @@ describe('pipeline graph action component', () => { done(); }, 0); }); + + it('renders a loading icon while waiting for request', done => { + component.$el.click(); + + component.$nextTick(() => { + expect(component.$el.querySelector('.js-action-icon-loading')).not.toBeNull(); + setTimeout(() => { + done(); + }); + }); + }); }); }); diff --git a/spec/javascripts/pipelines/graph/stage_column_component_spec.js b/spec/javascripts/pipelines/graph/stage_column_component_spec.js index 3240e8e4c1b..5183f8dd2d6 100644 --- a/spec/javascripts/pipelines/graph/stage_column_component_spec.js +++ b/spec/javascripts/pipelines/graph/stage_column_component_spec.js @@ -70,4 +70,53 @@ describe('stage column component', () => { ); }); }); + + describe('with action', () => { + it('renders action button', () => { + component = mountComponent(StageColumnComponent, { + groups: [ + { + id: 4259, + name: '<img src=x onerror=alert(document.domain)>', + status: { + icon: 'status_success', + label: 'success', + tooltip: '<img src=x onerror=alert(document.domain)>', + }, + }, + ], + title: 'test', + hasTriggeredBy: false, + action: { + icon: 'play', + title: 'Play all', + path: 'action', + }, + }); + + expect(component.$el.querySelector('.js-stage-action')).not.toBeNull(); + }); + }); + + describe('without action', () => { + it('does not render action button', () => { + component = mountComponent(StageColumnComponent, { + groups: [ + { + id: 4259, + name: '<img src=x onerror=alert(document.domain)>', + status: { + icon: 'status_success', + label: 'success', + tooltip: '<img src=x onerror=alert(document.domain)>', + }, + }, + ], + title: 'test', + hasTriggeredBy: false, + }); + + expect(component.$el.querySelector('.js-stage-action')).toBeNull(); + }); + }); }); diff --git a/spec/lib/gitlab/ci/build/prerequisite/kubernetes_namespace_spec.rb b/spec/lib/gitlab/ci/build/prerequisite/kubernetes_namespace_spec.rb index e8332b14627..5387863bd07 100644 --- a/spec/lib/gitlab/ci/build/prerequisite/kubernetes_namespace_spec.rb +++ b/spec/lib/gitlab/ci/build/prerequisite/kubernetes_namespace_spec.rb @@ -28,6 +28,12 @@ describe Gitlab::Ci::Build::Prerequisite::KubernetesNamespace do it { is_expected.to be_truthy } + context 'and the cluster is not managed' do + let(:cluster) { create(:cluster, :not_managed, projects: [build.project]) } + + it { is_expected.to be_falsey } + end + context 'and a namespace is already created for this project' do let!(:kubernetes_namespace) { create(:cluster_kubernetes_namespace, cluster: cluster, project: build.project) } diff --git a/spec/lib/gitlab/ci/status/stage/factory_spec.rb b/spec/lib/gitlab/ci/status/stage/factory_spec.rb index dee4f4efd1b..4b299170c87 100644 --- a/spec/lib/gitlab/ci/status/stage/factory_spec.rb +++ b/spec/lib/gitlab/ci/status/stage/factory_spec.rb @@ -22,7 +22,7 @@ describe Gitlab::Ci::Status::Stage::Factory do end context 'when stage has a core status' do - HasStatus::AVAILABLE_STATUSES.each do |core_status| + (HasStatus::AVAILABLE_STATUSES - %w(manual skipped scheduled)).each do |core_status| context "when core status is #{core_status}" do before do create(:ci_build, pipeline: pipeline, stage: 'test', status: core_status) @@ -64,4 +64,20 @@ describe Gitlab::Ci::Status::Stage::Factory do expect(status.details_path).to include "pipelines/#{pipeline.id}##{stage.name}" end end + + context 'when stage has manual builds' do + (HasStatus::BLOCKED_STATUS + ['skipped']).each do |core_status| + context "when status is #{core_status}" do + before do + create(:ci_build, pipeline: pipeline, stage: 'test', status: core_status) + create(:commit_status, pipeline: pipeline, stage: 'test', status: core_status) + create(:ci_build, pipeline: pipeline, stage: 'build', status: :manual) + end + + it 'fabricates a play manual status' do + expect(status).to be_a(Gitlab::Ci::Status::Stage::PlayManual) + end + end + end + end end diff --git a/spec/lib/gitlab/ci/status/stage/play_manual_spec.rb b/spec/lib/gitlab/ci/status/stage/play_manual_spec.rb new file mode 100644 index 00000000000..b0113b00ef0 --- /dev/null +++ b/spec/lib/gitlab/ci/status/stage/play_manual_spec.rb @@ -0,0 +1,74 @@ +# frozen_string_literal: true + +require 'spec_helper' + +describe Gitlab::Ci::Status::Stage::PlayManual do + let(:stage) { double('stage') } + let(:play_manual) { described_class.new(stage) } + + describe '#action_icon' do + subject { play_manual.action_icon } + + it { is_expected.to eq('play') } + end + + describe '#action_button_title' do + subject { play_manual.action_button_title } + + it { is_expected.to eq('Play all manual') } + end + + describe '#action_title' do + subject { play_manual.action_title } + + it { is_expected.to eq('Play all manual') } + end + + describe '#action_path' do + let(:stage) { create(:ci_stage_entity, status: 'manual') } + let(:pipeline) { stage.pipeline } + let(:play_manual) { stage.detailed_status(create(:user)) } + + subject { play_manual.action_path } + + it { is_expected.to eq("/#{pipeline.project.full_path}/pipelines/#{pipeline.id}/stages/#{stage.name}/play_manual") } + end + + describe '#action_method' do + subject { play_manual.action_method } + + it { is_expected.to eq(:post) } + end + + describe '.matches?' do + let(:user) { double('user') } + + subject { described_class.matches?(stage, user) } + + context 'when stage is skipped' do + let(:stage) { create(:ci_stage_entity, status: :skipped) } + + it { is_expected.to be_truthy } + end + + context 'when stage is manual' do + let(:stage) { create(:ci_stage_entity, status: :manual) } + + it { is_expected.to be_truthy } + end + + context 'when stage is scheduled' do + let(:stage) { create(:ci_stage_entity, status: :scheduled) } + + it { is_expected.to be_truthy } + end + + context 'when stage is success' do + let(:stage) { create(:ci_stage_entity, status: :success) } + + context 'and does not have manual builds' do + it { is_expected.to be_falsy } + end + end + end +end diff --git a/spec/lib/gitlab/data_builder/deployment_spec.rb b/spec/lib/gitlab/data_builder/deployment_spec.rb index b89a44e178b..0a6e2302b09 100644 --- a/spec/lib/gitlab/data_builder/deployment_spec.rb +++ b/spec/lib/gitlab/data_builder/deployment_spec.rb @@ -19,6 +19,7 @@ describe Gitlab::DataBuilder::Deployment do deployment = create(:deployment, status: :failed, environment: environment, sha: commit.sha, project: project) deployable = deployment.deployable expected_deployable_url = Gitlab::Routing.url_helpers.project_job_url(deployable.project, deployable) + expected_user_url = Gitlab::Routing.url_helpers.user_url(deployment.user) expected_commit_url = Gitlab::UrlBuilder.build(commit) data = described_class.build(deployment) @@ -30,7 +31,9 @@ describe Gitlab::DataBuilder::Deployment do expect(data[:project]).to eq(project.hook_attrs) expect(data[:short_sha]).to eq(deployment.short_sha) expect(data[:user]).to eq(deployment.user.hook_attrs) + expect(data[:user_url]).to eq(expected_user_url) expect(data[:commit_url]).to eq(expected_commit_url) + expect(data[:commit_title]).to eq(commit.title) end end end diff --git a/spec/lib/gitlab/gitaly_client/ref_service_spec.rb b/spec/lib/gitlab/gitaly_client/ref_service_spec.rb index 0dab39575b9..0bb6e582159 100644 --- a/spec/lib/gitlab/gitaly_client/ref_service_spec.rb +++ b/spec/lib/gitlab/gitaly_client/ref_service_spec.rb @@ -165,4 +165,15 @@ describe Gitlab::GitalyClient::RefService do client.delete_refs(except_with_prefixes: prefixes) end end + + describe '#pack_refs' do + it 'sends a pack_refs message' do + expect_any_instance_of(Gitaly::RefService::Stub) + .to receive(:pack_refs) + .with(gitaly_request_with_path(storage_name, relative_path), kind_of(Hash)) + .and_return(double(:pack_refs_response)) + + client.pack_refs + end + end end diff --git a/spec/models/ci/legacy_stage_spec.rb b/spec/models/ci/legacy_stage_spec.rb index be0307518eb..bb812cc0533 100644 --- a/spec/models/ci/legacy_stage_spec.rb +++ b/spec/models/ci/legacy_stage_spec.rb @@ -272,4 +272,6 @@ describe Ci::LegacyStage do def create_job(type, status: 'success', stage: stage_name, **opts) create(type, pipeline: pipeline, stage: stage, status: status, **opts) end + + it_behaves_like 'manual playable stage', :ci_stage end diff --git a/spec/models/ci/pipeline_spec.rb b/spec/models/ci/pipeline_spec.rb index 9d0cd654f13..af455a72f50 100644 --- a/spec/models/ci/pipeline_spec.rb +++ b/spec/models/ci/pipeline_spec.rb @@ -2942,4 +2942,36 @@ describe Ci::Pipeline, :mailer do end end end + + describe '#find_stage_by_name' do + let(:pipeline) { create(:ci_pipeline) } + let(:stage_name) { 'test' } + + let(:stage) do + create(:ci_stage_entity, + pipeline: pipeline, + project: pipeline.project, + name: 'test') + end + + before do + create_list(:ci_build, 2, pipeline: pipeline, stage: stage.name) + end + + subject { pipeline.find_stage_by_name!(stage_name) } + + context 'when stage exists' do + it { is_expected.to eq(stage) } + end + + context 'when stage does not exist' do + let(:stage_name) { 'build' } + + it 'raises an ActiveRecord exception' do + expect do + subject + end.to raise_exception(ActiveRecord::RecordNotFound) + end + end + end end diff --git a/spec/models/ci/stage_spec.rb b/spec/models/ci/stage_spec.rb index 661958390e2..85cd32fb03a 100644 --- a/spec/models/ci/stage_spec.rb +++ b/spec/models/ci/stage_spec.rb @@ -285,4 +285,6 @@ describe Ci::Stage, :models do end end end + + it_behaves_like 'manual playable stage', :ci_stage_entity end diff --git a/spec/models/clusters/cluster_spec.rb b/spec/models/clusters/cluster_spec.rb index 894ef3fb956..e1506c06044 100644 --- a/spec/models/clusters/cluster_spec.rb +++ b/spec/models/clusters/cluster_spec.rb @@ -95,6 +95,24 @@ describe Clusters::Cluster do it { is_expected.to contain_exactly(cluster) } end + describe '.managed' do + subject do + described_class.managed + end + + context 'cluster is not managed' do + let!(:cluster) { create(:cluster, :not_managed) } + + it { is_expected.not_to include(cluster) } + end + + context 'cluster is managed' do + let!(:cluster) { create(:cluster) } + + it { is_expected.to include(cluster) } + end + end + describe '.missing_kubernetes_namespace' do let!(:cluster) { create(:cluster, :provided_by_gcp, :project) } let(:project) { cluster.project } diff --git a/spec/models/clusters/platforms/kubernetes_spec.rb b/spec/models/clusters/platforms/kubernetes_spec.rb index 0281dd2c303..e35d14f2282 100644 --- a/spec/models/clusters/platforms/kubernetes_spec.rb +++ b/spec/models/clusters/platforms/kubernetes_spec.rb @@ -331,6 +331,18 @@ describe Clusters::Platforms::Kubernetes, :use_clean_rails_memory_store_caching { key: 'KUBE_TOKEN', value: kubernetes.token, public: false } ) end + + context 'the cluster is not managed' do + let!(:cluster) { create(:cluster, :group, :not_managed, platform_kubernetes: kubernetes) } + + it_behaves_like 'setting variables' + + it 'sets KUBE_TOKEN' do + expect(subject).to include( + { key: 'KUBE_TOKEN', value: kubernetes.token, public: false, masked: true } + ) + end + end end context 'kubernetes namespace exists for the project' do diff --git a/spec/models/commit_status_spec.rb b/spec/models/commit_status_spec.rb index ca2f9e36c98..017cca0541e 100644 --- a/spec/models/commit_status_spec.rb +++ b/spec/models/commit_status_spec.rb @@ -449,7 +449,7 @@ describe CommitStatus do end it "lock" do - is_expected.to be true + is_expected.to be_truthy end it "raise exception when trying to update" do @@ -463,7 +463,7 @@ describe CommitStatus do end it "do not lock" do - is_expected.to be false + is_expected.to be_falsey end it "save correctly" do diff --git a/spec/models/namespace_spec.rb b/spec/models/namespace_spec.rb index 95a4b0f6d71..bfde367c47f 100644 --- a/spec/models/namespace_spec.rb +++ b/spec/models/namespace_spec.rb @@ -146,20 +146,20 @@ describe Namespace do create(:project, namespace: namespace, statistics: build(:project_statistics, - storage_size: 606, repository_size: 101, lfs_objects_size: 202, - build_artifacts_size: 303)) + build_artifacts_size: 303, + packages_size: 404)) end let(:project2) do create(:project, namespace: namespace, statistics: build(:project_statistics, - storage_size: 60, repository_size: 10, lfs_objects_size: 20, - build_artifacts_size: 30)) + build_artifacts_size: 30, + packages_size: 40)) end it "sums all project storage counters in the namespace" do @@ -167,10 +167,11 @@ describe Namespace do project2 statistics = described_class.with_statistics.find(namespace.id) - expect(statistics.storage_size).to eq 666 + expect(statistics.storage_size).to eq 1110 expect(statistics.repository_size).to eq 111 expect(statistics.lfs_objects_size).to eq 222 expect(statistics.build_artifacts_size).to eq 333 + expect(statistics.packages_size).to eq 444 end it "correctly handles namespaces without projects" do @@ -180,6 +181,7 @@ describe Namespace do expect(statistics.repository_size).to eq 0 expect(statistics.lfs_objects_size).to eq 0 expect(statistics.build_artifacts_size).to eq 0 + expect(statistics.packages_size).to eq 0 end end @@ -743,22 +745,25 @@ describe Namespace do end end - describe '#full_path_was' do + describe '#full_path_before_last_save' do context 'when the group has no parent' do - it 'returns the path was' do - group = create(:group, parent: nil) - expect(group.full_path_was).to eq(group.path_was) + it 'returns the path before last save' do + group = create(:group) + + group.update(parent: nil) + + expect(group.full_path_before_last_save).to eq(group.path_before_last_save) end end context 'when a parent is assigned to a group with no previous parent' do - it 'returns the path was' do + it 'returns the path before last save' do group = create(:group, parent: nil) - parent = create(:group) - group.parent = parent - expect(group.full_path_was).to eq("#{group.path_was}") + group.update(parent: parent) + + expect(group.full_path_before_last_save).to eq("#{group.path_before_last_save}") end end @@ -766,9 +771,10 @@ describe Namespace do it 'returns the parent full path' do parent = create(:group) group = create(:group, parent: parent) - group.parent = nil - expect(group.full_path_was).to eq("#{parent.full_path}/#{group.path}") + group.update(parent: nil) + + expect(group.full_path_before_last_save).to eq("#{parent.full_path}/#{group.path}") end end @@ -777,8 +783,10 @@ describe Namespace do parent = create(:group) group = create(:group, parent: parent) new_parent = create(:group) - group.parent = new_parent - expect(group.full_path_was).to eq("#{parent.full_path}/#{group.path}") + + group.update(parent: new_parent) + + expect(group.full_path_before_last_save).to eq("#{parent.full_path}/#{group.path}") end end end diff --git a/spec/models/project_services/chat_message/deployment_message_spec.rb b/spec/models/project_services/chat_message/deployment_message_spec.rb index 86565ce8b01..42c1689db3d 100644 --- a/spec/models/project_services/chat_message/deployment_message_spec.rb +++ b/spec/models/project_services/chat_message/deployment_message_spec.rb @@ -89,8 +89,10 @@ describe ChatMessage::DeploymentMessage do name: "Jane Person", username: "jane" }, + user_url: "user_url", short_sha: "12345678", - commit_url: "commit_url" + commit_url: "commit_url", + commit_title: "commit title text" }.merge(params) end @@ -104,12 +106,13 @@ describe ChatMessage::DeploymentMessage do deployment = create(:deployment, :success, deployable: ci_build, environment: environment, project: project, user: user, sha: commit.sha) job_url = Gitlab::Routing.url_helpers.project_job_url(project, ci_build) commit_url = Gitlab::UrlBuilder.build(deployment.commit) + user_url = Gitlab::Routing.url_helpers.user_url(user) data = Gitlab::DataBuilder::Deployment.build(deployment) message = described_class.new(data) expect(message.attachments).to eq([{ - text: "[myspace/myproject](#{project.web_url})\n[Job ##{ci_build.id}](#{job_url}), SHA [#{deployment.short_sha}](#{commit_url}), by John Smith (smith)", + text: "[myspace/myproject](#{project.web_url}) with job [##{ci_build.id}](#{job_url}) by [John Smith (smith)](#{user_url})\n[#{deployment.short_sha}](#{commit_url}): #{commit.title}", color: "good" }]) end @@ -120,7 +123,7 @@ describe ChatMessage::DeploymentMessage do message = described_class.new(data) expect(message.attachments).to eq([{ - text: "[project_path_with_namespace](project_web_url)\n[Job #3](deployable_url), SHA [12345678](commit_url), by Jane Person (jane)", + text: "[project_path_with_namespace](project_web_url) with job [#3](deployable_url) by [Jane Person (jane)](user_url)\n[12345678](commit_url): commit title text", color: "danger" }]) end @@ -131,7 +134,7 @@ describe ChatMessage::DeploymentMessage do message = described_class.new(data) expect(message.attachments).to eq([{ - text: "[project_path_with_namespace](project_web_url)\n[Job #3](deployable_url), SHA [12345678](commit_url), by Jane Person (jane)", + text: "[project_path_with_namespace](project_web_url) with job [#3](deployable_url) by [Jane Person (jane)](user_url)\n[12345678](commit_url): commit title text", color: "warning" }]) end @@ -142,7 +145,7 @@ describe ChatMessage::DeploymentMessage do message = described_class.new(data) expect(message.attachments).to eq([{ - text: "[project_path_with_namespace](project_web_url)\n[Job #3](deployable_url), SHA [12345678](commit_url), by Jane Person (jane)", + text: "[project_path_with_namespace](project_web_url) with job [#3](deployable_url) by [Jane Person (jane)](user_url)\n[12345678](commit_url): commit title text", color: "#334455" }]) end diff --git a/spec/models/project_statistics_spec.rb b/spec/models/project_statistics_spec.rb index c670b6aac56..738398a06f9 100644 --- a/spec/models/project_statistics_spec.rb +++ b/spec/models/project_statistics_spec.rb @@ -124,16 +124,30 @@ describe ProjectStatistics do end describe '.increment_statistic' do - it 'increases the statistic by that amount' do - expect { described_class.increment_statistic(project.id, :build_artifacts_size, 13) } - .to change { statistics.reload.build_artifacts_size } - .by(13) + shared_examples 'a statistic that increases storage_size' do + it 'increases the statistic by that amount' do + expect { described_class.increment_statistic(project.id, stat, 13) } + .to change { statistics.reload.send(stat) || 0 } + .by(13) + end + + it 'increases also storage size by that amount' do + expect { described_class.increment_statistic(project.id, stat, 20) } + .to change { statistics.reload.storage_size } + .by(20) + end end - it 'increases also storage size by that amount' do - expect { described_class.increment_statistic(project.id, :build_artifacts_size, 20) } - .to change { statistics.reload.storage_size } - .by(20) + context 'when adjusting :build_artifacts_size' do + let(:stat) { :build_artifacts_size } + + it_behaves_like 'a statistic that increases storage_size' + end + + context 'when adjusting :packages_size' do + let(:stat) { :packages_size } + + it_behaves_like 'a statistic that increases storage_size' end context 'when the amount is 0' do diff --git a/spec/models/release_spec.rb b/spec/models/release_spec.rb index 0b19a4f8efc..7c106ce6b85 100644 --- a/spec/models/release_spec.rb +++ b/spec/models/release_spec.rb @@ -49,6 +49,11 @@ RSpec.describe Release do it 'counts the link as an asset' do is_expected.to eq(1 + Releases::Source::FORMATS.count) end + + it "excludes sources count when asked" do + assets_count = release.assets_count(except: [:sources]) + expect(assets_count).to eq(1) + end end end diff --git a/spec/models/repository_spec.rb b/spec/models/repository_spec.rb index a6c3d5756aa..9ff0f355fd4 100644 --- a/spec/models/repository_spec.rb +++ b/spec/models/repository_spec.rb @@ -1451,6 +1451,91 @@ describe Repository do end end + describe '#rebase' do + let(:merge_request) { create(:merge_request, source_branch: 'feature', target_branch: 'master', source_project: project) } + + shared_examples_for 'a method that can rebase successfully' do + it 'returns the rebase commit sha' do + rebase_commit_sha = repository.rebase(user, merge_request) + head_sha = merge_request.source_project.repository.commit(merge_request.source_branch).sha + + expect(rebase_commit_sha).to eq(head_sha) + end + + it 'sets the `rebase_commit_sha` for the given merge request' do + rebase_commit_sha = repository.rebase(user, merge_request) + + expect(rebase_commit_sha).not_to be_nil + expect(merge_request.rebase_commit_sha).to eq(rebase_commit_sha) + end + end + + context 'when two_step_rebase feature is enabled' do + before do + stub_feature_flags(two_step_rebase: true) + end + + it_behaves_like 'a method that can rebase successfully' + + it 'executes the new Gitaly RPC' do + expect_any_instance_of(Gitlab::GitalyClient::OperationService).to receive(:rebase) + expect_any_instance_of(Gitlab::GitalyClient::OperationService).not_to receive(:user_rebase) + + repository.rebase(user, merge_request) + end + + describe 'rolling back the `rebase_commit_sha`' do + let(:new_sha) { Digest::SHA1.hexdigest('foo') } + + it 'does not rollback when there are no errors' do + second_response = double(pre_receive_error: nil, git_error: nil) + mock_gitaly(second_response) + + repository.rebase(user, merge_request) + + expect(merge_request.reload.rebase_commit_sha).to eq(new_sha) + end + + it 'does rollback when an error is encountered in the second step' do + second_response = double(pre_receive_error: 'my_error', git_error: nil) + mock_gitaly(second_response) + + expect do + repository.rebase(user, merge_request) + end.to raise_error(Gitlab::Git::PreReceiveError) + + expect(merge_request.reload.rebase_commit_sha).to be_nil + end + + def mock_gitaly(second_response) + responses = [ + double(rebase_sha: new_sha).as_null_object, + second_response + ] + + expect_any_instance_of( + Gitaly::OperationService::Stub + ).to receive(:user_rebase_confirmable).and_return(responses.each) + end + end + end + + context 'when two_step_rebase feature is disabled' do + before do + stub_feature_flags(two_step_rebase: false) + end + + it_behaves_like 'a method that can rebase successfully' + + it 'executes the deprecated Gitaly RPC' do + expect_any_instance_of(Gitlab::GitalyClient::OperationService).to receive(:user_rebase) + expect_any_instance_of(Gitlab::GitalyClient::OperationService).not_to receive(:rebase) + + repository.rebase(user, merge_request) + end + end + end + describe '#revert' do let(:new_image_commit) { repository.commit('33f3729a45c02fc67d00adb1b8bca394b0e761d9') } let(:update_image_commit) { repository.commit('2f63565e7aac07bcdadb654e253078b727143ec4') } diff --git a/spec/policies/personal_snippet_policy_spec.rb b/spec/policies/personal_snippet_policy_spec.rb index a38e0dbd797..097000ceb6a 100644 --- a/spec/policies/personal_snippet_policy_spec.rb +++ b/spec/policies/personal_snippet_policy_spec.rb @@ -14,13 +14,6 @@ describe PersonalSnippetPolicy do ] end - let(:comment_permissions) do - [ - :comment_personal_snippet, - :create_note - ] - end - def permissions(user) described_class.new(user, snippet) end @@ -33,7 +26,7 @@ describe PersonalSnippetPolicy do it do is_expected.to be_allowed(:read_personal_snippet) - is_expected.to be_disallowed(*comment_permissions) + is_expected.to be_disallowed(:create_note) is_expected.to be_disallowed(:award_emoji) is_expected.to be_disallowed(*author_permissions) end @@ -44,7 +37,7 @@ describe PersonalSnippetPolicy do it do is_expected.to be_allowed(:read_personal_snippet) - is_expected.to be_allowed(*comment_permissions) + is_expected.to be_allowed(:create_note) is_expected.to be_allowed(:award_emoji) is_expected.to be_disallowed(*author_permissions) end @@ -55,7 +48,7 @@ describe PersonalSnippetPolicy do it do is_expected.to be_allowed(:read_personal_snippet) - is_expected.to be_allowed(*comment_permissions) + is_expected.to be_allowed(:create_note) is_expected.to be_allowed(:award_emoji) is_expected.to be_allowed(*author_permissions) end @@ -70,7 +63,7 @@ describe PersonalSnippetPolicy do it do is_expected.to be_disallowed(:read_personal_snippet) - is_expected.to be_disallowed(*comment_permissions) + is_expected.to be_disallowed(:create_note) is_expected.to be_disallowed(:award_emoji) is_expected.to be_disallowed(*author_permissions) end @@ -81,7 +74,7 @@ describe PersonalSnippetPolicy do it do is_expected.to be_allowed(:read_personal_snippet) - is_expected.to be_allowed(*comment_permissions) + is_expected.to be_allowed(:create_note) is_expected.to be_allowed(:award_emoji) is_expected.to be_disallowed(*author_permissions) end @@ -92,7 +85,7 @@ describe PersonalSnippetPolicy do it do is_expected.to be_disallowed(:read_personal_snippet) - is_expected.to be_disallowed(*comment_permissions) + is_expected.to be_disallowed(:create_note) is_expected.to be_disallowed(:award_emoji) is_expected.to be_disallowed(*author_permissions) end @@ -103,7 +96,7 @@ describe PersonalSnippetPolicy do it do is_expected.to be_allowed(:read_personal_snippet) - is_expected.to be_allowed(*comment_permissions) + is_expected.to be_allowed(:create_note) is_expected.to be_allowed(:award_emoji) is_expected.to be_allowed(*author_permissions) end @@ -118,7 +111,7 @@ describe PersonalSnippetPolicy do it do is_expected.to be_disallowed(:read_personal_snippet) - is_expected.to be_disallowed(*comment_permissions) + is_expected.to be_disallowed(:create_note) is_expected.to be_disallowed(:award_emoji) is_expected.to be_disallowed(*author_permissions) end @@ -129,7 +122,7 @@ describe PersonalSnippetPolicy do it do is_expected.to be_disallowed(:read_personal_snippet) - is_expected.to be_disallowed(*comment_permissions) + is_expected.to be_disallowed(:create_note) is_expected.to be_disallowed(:award_emoji) is_expected.to be_disallowed(*author_permissions) end @@ -140,7 +133,7 @@ describe PersonalSnippetPolicy do it do is_expected.to be_allowed(:read_personal_snippet) - is_expected.to be_disallowed(:comment_personal_snippet) + is_expected.to be_disallowed(:create_note) is_expected.to be_disallowed(:award_emoji) is_expected.to be_disallowed(*author_permissions) end @@ -151,7 +144,7 @@ describe PersonalSnippetPolicy do it do is_expected.to be_disallowed(:read_personal_snippet) - is_expected.to be_disallowed(*comment_permissions) + is_expected.to be_disallowed(:create_note) is_expected.to be_disallowed(:award_emoji) is_expected.to be_disallowed(*author_permissions) end @@ -162,7 +155,7 @@ describe PersonalSnippetPolicy do it do is_expected.to be_allowed(:read_personal_snippet) - is_expected.to be_allowed(*comment_permissions) + is_expected.to be_allowed(:create_note) is_expected.to be_allowed(:award_emoji) is_expected.to be_allowed(*author_permissions) end diff --git a/spec/policies/project_policy_spec.rb b/spec/policies/project_policy_spec.rb index 42f8bf3137b..8075fcade5f 100644 --- a/spec/policies/project_policy_spec.rb +++ b/spec/policies/project_policy_spec.rb @@ -17,7 +17,7 @@ describe ProjectPolicy do read_project_for_iids read_issue_iid read_label read_milestone read_project_snippet read_project_member read_note create_project create_issue create_note upload_file create_merge_request_in - award_emoji + award_emoji read_release ] end @@ -26,7 +26,7 @@ describe ProjectPolicy do download_code fork_project create_project_snippet update_issue admin_issue admin_label admin_list read_commit_status read_build read_container_image read_pipeline read_environment read_deployment - read_merge_request download_wiki_code read_sentry_issue read_release + read_merge_request download_wiki_code read_sentry_issue ] end diff --git a/spec/requests/api/internal_spec.rb b/spec/requests/api/internal_spec.rb index 88c19448373..bae0302f3ff 100644 --- a/spec/requests/api/internal_spec.rb +++ b/spec/requests/api/internal_spec.rb @@ -959,7 +959,9 @@ describe API::Internal do it 'creates a new merge request' do expect do - post api('/internal/post_receive'), params: valid_params + Sidekiq::Testing.fake! do + post api('/internal/post_receive'), params: valid_params + end end.to change { MergeRequest.count }.by(1) end diff --git a/spec/requests/api/project_clusters_spec.rb b/spec/requests/api/project_clusters_spec.rb index 94e6ca2c07c..5357be3cdee 100644 --- a/spec/requests/api/project_clusters_spec.rb +++ b/spec/requests/api/project_clusters_spec.rb @@ -189,6 +189,7 @@ describe API::ProjectClusters do { name: 'test-cluster', domain: 'domain.example.com', + managed: false, platform_kubernetes_attributes: platform_kubernetes_attributes } end @@ -220,6 +221,7 @@ describe API::ProjectClusters do expect(cluster_result.project).to eq(project) expect(cluster_result.name).to eq('test-cluster') expect(cluster_result.domain).to eq('domain.example.com') + expect(cluster_result.managed).to be_falsy expect(platform_kubernetes.rbac?).to be_truthy expect(platform_kubernetes.api_url).to eq(api_url) expect(platform_kubernetes.namespace).to eq(namespace) diff --git a/spec/requests/api/releases_spec.rb b/spec/requests/api/releases_spec.rb index 71ec091c42c..8603fa2a73d 100644 --- a/spec/requests/api/releases_spec.rb +++ b/spec/requests/api/releases_spec.rb @@ -52,7 +52,7 @@ describe API::Releases do it 'matches response schema' do get api("/projects/#{project.id}/releases", maintainer) - expect(response).to match_response_schema('releases') + expect(response).to match_response_schema('public_api/v4/releases') end end @@ -69,10 +69,25 @@ describe API::Releases do end context 'when user is a guest' do - it 'responds 403 Forbidden' do + let!(:release) do + create(:release, + project: project, + tag: 'v0.1', + author: maintainer, + created_at: 2.days.ago) + end + + it 'responds 200 OK' do get api("/projects/#{project.id}/releases", guest) - expect(response).to have_gitlab_http_status(:forbidden) + expect(response).to have_gitlab_http_status(:ok) + end + + it "does not expose tag, commit and source code" do + get api("/projects/#{project.id}/releases", guest) + + expect(response).to match_response_schema('public_api/v4/release/releases_for_guest') + expect(json_response[0]['assets']['count']).to eq(release.links.count) end context 'when project is public' do @@ -83,6 +98,13 @@ describe API::Releases do expect(response).to have_gitlab_http_status(:ok) end + + it "exposes tag, commit and source code" do + get api("/projects/#{project.id}/releases", guest) + + expect(response).to match_response_schema('public_api/v4/releases') + expect(json_response[0]['assets']['count']).to eq(release.links.count + release.sources.count) + end end end @@ -135,7 +157,7 @@ describe API::Releases do it 'matches response schema' do get api("/projects/#{project.id}/releases/v0.1", maintainer) - expect(response).to match_response_schema('release') + expect(response).to match_response_schema('public_api/v4/release') end it 'contains source information as assets' do @@ -225,6 +247,17 @@ describe API::Releases do expect(response).to have_gitlab_http_status(:ok) end + + it "exposes tag and commit" do + create(:release, + project: project, + tag: 'v0.1', + author: maintainer, + created_at: 2.days.ago) + get api("/projects/#{project.id}/releases/v0.1", guest) + + expect(response).to match_response_schema('public_api/v4/release') + end end end end @@ -306,7 +339,7 @@ describe API::Releases do it 'matches response schema' do post api("/projects/#{project.id}/releases", maintainer), params: params - expect(response).to match_response_schema('release') + expect(response).to match_response_schema('public_api/v4/release') end it 'does not create a new tag' do @@ -378,7 +411,7 @@ describe API::Releases do it 'matches response schema' do post api("/projects/#{project.id}/releases", maintainer), params: params - expect(response).to match_response_schema('release') + expect(response).to match_response_schema('public_api/v4/release') end end @@ -532,7 +565,7 @@ describe API::Releases do it 'matches response schema' do put api("/projects/#{project.id}/releases/v0.1", maintainer), params: params - expect(response).to match_response_schema('release') + expect(response).to match_response_schema('public_api/v4/release') end context 'when user tries to update sha' do @@ -624,7 +657,7 @@ describe API::Releases do it 'matches response schema' do delete api("/projects/#{project.id}/releases/v0.1", maintainer) - expect(response).to match_response_schema('release') + expect(response).to match_response_schema('public_api/v4/release') end context 'when there are no corresponding releases' do diff --git a/spec/requests/api/tags_spec.rb b/spec/requests/api/tags_spec.rb index fffe878ddbd..d898319e709 100644 --- a/spec/requests/api/tags_spec.rb +++ b/spec/requests/api/tags_spec.rb @@ -378,7 +378,7 @@ describe API::Tags do post api(route, user), params: { description: description } expect(response).to have_gitlab_http_status(201) - expect(response).to match_response_schema('public_api/v4/release') + expect(response).to match_response_schema('public_api/v4/release/tag_release') expect(json_response['tag_name']).to eq(tag_name) expect(json_response['description']).to eq(description) end diff --git a/spec/routing/uploads_routing_spec.rb b/spec/routing/uploads_routing_spec.rb new file mode 100644 index 00000000000..6a041ffdd6c --- /dev/null +++ b/spec/routing/uploads_routing_spec.rb @@ -0,0 +1,22 @@ +# frozen_string_literal: true + +require 'spec_helper' + +describe 'Uploads', 'routing' do + it 'allows creating uploads for personal snippets' do + expect(post('/uploads/personal_snippet?id=1')).to route_to( + controller: 'uploads', + action: 'create', + model: 'personal_snippet', + id: '1' + ) + end + + it 'does not allow creating uploads for other models' do + UploadsController::MODEL_CLASSES.keys.compact.each do |model| + next if model == 'personal_snippet' + + expect(post("/uploads/#{model}?id=1")).not_to be_routable + end + end +end diff --git a/spec/serializers/pipeline_serializer_spec.rb b/spec/serializers/pipeline_serializer_spec.rb index d9023036534..54e6abc2d3a 100644 --- a/spec/serializers/pipeline_serializer_spec.rb +++ b/spec/serializers/pipeline_serializer_spec.rb @@ -156,8 +156,8 @@ describe PipelineSerializer do it 'verifies number of queries', :request_store do recorded = ActiveRecord::QueryRecorder.new { subject } - expected_queries = Gitlab.ee? ? 38 : 31 + expect(recorded.count).to be_within(2).of(expected_queries) expect(recorded.cached_count).to eq(0) end diff --git a/spec/serializers/stage_entity_spec.rb b/spec/serializers/stage_entity_spec.rb index 2034c7891ef..6b1185d1283 100644 --- a/spec/serializers/stage_entity_spec.rb +++ b/spec/serializers/stage_entity_spec.rb @@ -48,6 +48,10 @@ describe StageEntity do expect(subject[:title]).to eq 'test: passed' end + it 'does not contain play_details info' do + expect(subject[:status][:action]).not_to be_present + end + context 'when the jobs should be grouped' do let(:entity) { described_class.new(stage, request: request, grouped: true) } @@ -66,5 +70,29 @@ describe StageEntity do end end end + + context 'with a skipped stage ' do + let(:stage) { create(:ci_stage_entity, status: 'skipped') } + + it 'contains play_all_manual' do + expect(subject[:status][:action]).to be_present + end + end + + context 'with a scheduled stage ' do + let(:stage) { create(:ci_stage_entity, status: 'scheduled') } + + it 'contains play_all_manual' do + expect(subject[:status][:action]).to be_present + end + end + + context 'with a manual stage ' do + let(:stage) { create(:ci_stage_entity, status: 'manual') } + + it 'contains play_all_manual' do + expect(subject[:status][:action]).to be_present + end + end end end diff --git a/spec/serializers/stage_serializer_spec.rb b/spec/serializers/stage_serializer_spec.rb new file mode 100644 index 00000000000..aae17cfbcb9 --- /dev/null +++ b/spec/serializers/stage_serializer_spec.rb @@ -0,0 +1,31 @@ +# frozen_string_literal: true + +require 'spec_helper' + +describe StageSerializer do + let(:project) { create(:project, :repository) } + let(:user) { create(:user) } + let(:resource) { create(:ci_stage_entity) } + + let(:serializer) do + described_class.new(current_user: user, project: project) + end + + subject { serializer.represent(resource) } + + describe '#represent' do + context 'with a single entity' do + it 'serializes the stage object' do + expect(subject[:name]).to eq(resource.name) + end + end + + context 'with an array of entities' do + let(:resource) { create_list(:ci_stage_entity, 2) } + + it 'serializes the array of pipelines' do + expect(subject).not_to be_empty + end + end + end +end diff --git a/spec/services/ci/play_manual_stage_service_spec.rb b/spec/services/ci/play_manual_stage_service_spec.rb new file mode 100644 index 00000000000..5d812745c7f --- /dev/null +++ b/spec/services/ci/play_manual_stage_service_spec.rb @@ -0,0 +1,79 @@ +# frozen_string_literal: true + +require 'spec_helper' + +describe Ci::PlayManualStageService, '#execute' do + let(:current_user) { create(:user) } + let(:pipeline) { create(:ci_pipeline, user: current_user) } + let(:project) { pipeline.project } + let(:service) { described_class.new(project, current_user, pipeline: pipeline) } + let(:stage_status) { 'manual' } + + let(:stage) do + create(:ci_stage_entity, + pipeline: pipeline, + project: project, + name: 'test') + end + + before do + project.add_maintainer(current_user) + create_builds_for_stage(status: stage_status) + end + + context 'when pipeline has manual builds' do + before do + service.execute(stage) + end + + it 'starts manual builds from pipeline' do + expect(pipeline.builds.manual.count).to eq(0) + end + + it 'updates manual builds' do + pipeline.builds.each do |build| + expect(build.user).to eq(current_user) + end + end + end + + context 'when pipeline has no manual builds' do + let(:stage_status) { 'failed' } + + before do + service.execute(stage) + end + + it 'does not update the builds' do + expect(pipeline.builds.failed.count).to eq(3) + end + end + + context 'when user does not have permission on a specific build' do + before do + allow_any_instance_of(Ci::Build).to receive(:play) + .and_raise(Gitlab::Access::AccessDeniedError) + + service.execute(stage) + end + + it 'logs the error' do + expect(Gitlab::AppLogger).to receive(:error) + .exactly(stage.builds.manual.count) + + service.execute(stage) + end + end + + def create_builds_for_stage(options) + options.merge!({ + when: 'manual', + pipeline: pipeline, + stage: stage.name, + stage_id: stage.id, + user: pipeline.user + }) + + create_list(:ci_build, 3, options) + end +end diff --git a/spec/services/clusters/refresh_service_spec.rb b/spec/services/clusters/refresh_service_spec.rb index 9e442ebf4e9..94c35228955 100644 --- a/spec/services/clusters/refresh_service_spec.rb +++ b/spec/services/clusters/refresh_service_spec.rb @@ -121,5 +121,11 @@ describe Clusters::RefreshService do end end end + + context 'cluster is not managed' do + let!(:cluster) { create(:cluster, :project, :not_managed, projects: [project]) } + + include_examples 'does not create a kubernetes namespace' + end end end diff --git a/spec/services/merge_requests/rebase_service_spec.rb b/spec/services/merge_requests/rebase_service_spec.rb index 0d9523a4c2c..a443e4588d9 100644 --- a/spec/services/merge_requests/rebase_service_spec.rb +++ b/spec/services/merge_requests/rebase_service_spec.rb @@ -73,7 +73,7 @@ describe MergeRequests::RebaseService do end context 'valid params' do - describe 'successful rebase' do + shared_examples_for 'a service that can execute a successful rebase' do before do service.execute(merge_request) end @@ -99,6 +99,22 @@ describe MergeRequests::RebaseService do end end + context 'when the two_step_rebase feature is enabled' do + before do + stub_feature_flags(two_step_rebase: true) + end + + it_behaves_like 'a service that can execute a successful rebase' + end + + context 'when the two_step_rebase feature is disabled' do + before do + stub_feature_flags(two_step_rebase: false) + end + + it_behaves_like 'a service that can execute a successful rebase' + end + context 'fork' do describe 'successful fork rebase' do let(:forked_project) do diff --git a/spec/services/projects/housekeeping_service_spec.rb b/spec/services/projects/housekeeping_service_spec.rb index 368cf123c3e..f651db70cbd 100644 --- a/spec/services/projects/housekeeping_service_spec.rb +++ b/spec/services/projects/housekeeping_service_spec.rb @@ -81,6 +81,9 @@ describe Projects::HousekeepingService do # At push 10, 20, ... (except those above) expect(GitGarbageCollectWorker).to receive(:perform_async).with(project.id, :incremental_repack, :the_lease_key, :the_uuid) .exactly(16).times + # At push 6, 12, 18, ... (except those above) + expect(GitGarbageCollectWorker).to receive(:perform_async).with(project.id, :pack_refs, :the_lease_key, :the_uuid) + .exactly(27).times 201.times do subject.increment! diff --git a/spec/services/system_hooks_service_spec.rb b/spec/services/system_hooks_service_spec.rb index 5dc7394b84f..f5c6e972953 100644 --- a/spec/services/system_hooks_service_spec.rb +++ b/spec/services/system_hooks_service_spec.rb @@ -86,20 +86,20 @@ describe SystemHooksService do context 'group_rename' do it 'contains old and new path' do - allow(group).to receive(:path_was).and_return('old-path') + allow(group).to receive(:path_before_last_save).and_return('old-path') data = event_data(group, :rename) expect(data).to include(:event_name, :name, :created_at, :updated_at, :full_path, :path, :group_id, :old_path, :old_full_path) expect(data[:path]).to eq(group.path) expect(data[:full_path]).to eq(group.path) - expect(data[:old_path]).to eq(group.path_was) - expect(data[:old_full_path]).to eq(group.path_was) + expect(data[:old_path]).to eq(group.path_before_last_save) + expect(data[:old_full_path]).to eq(group.path_before_last_save) end it 'contains old and new full_path for subgroup' do subgroup = create(:group, parent: group) - allow(subgroup).to receive(:path_was).and_return('old-path') + allow(subgroup).to receive(:path_before_last_save).and_return('old-path') data = event_data(subgroup, :rename) @@ -110,13 +110,13 @@ describe SystemHooksService do context 'user_rename' do it 'contains old and new username' do - allow(user).to receive(:username_was).and_return('old-username') + allow(user).to receive(:username_before_last_save).and_return('old-username') data = event_data(user, :rename) expect(data).to include(:event_name, :name, :created_at, :updated_at, :email, :user_id, :username, :old_username) expect(data[:username]).to eq(user.username) - expect(data[:old_username]).to eq(user.username_was) + expect(data[:old_username]).to eq(user.username_before_last_save) end end diff --git a/spec/spec_helper.rb b/spec/spec_helper.rb index fbc5fcea7b9..9266bee34d6 100644 --- a/spec/spec_helper.rb +++ b/spec/spec_helper.rb @@ -53,7 +53,7 @@ RSpec.configure do |config| config.display_try_failure_messages = true config.infer_spec_type_from_file_location! - config.full_backtrace = true + config.full_backtrace = !!ENV['CI'] config.define_derived_metadata(file_path: %r{/spec/}) do |metadata| location = metadata[:location] diff --git a/spec/support/shared_context/policies/project_policy_shared_context.rb b/spec/support/shared_context/policies/project_policy_shared_context.rb index ee5cfcd850d..54d9f5b15f2 100644 --- a/spec/support/shared_context/policies/project_policy_shared_context.rb +++ b/spec/support/shared_context/policies/project_policy_shared_context.rb @@ -24,8 +24,7 @@ RSpec.shared_context 'ProjectPolicy context' do download_code fork_project create_project_snippet update_issue admin_issue admin_label admin_list read_commit_status read_build read_container_image read_pipeline read_environment read_deployment - read_merge_request download_wiki_code read_sentry_issue read_release - read_prometheus + read_merge_request download_wiki_code read_sentry_issue read_prometheus ] end diff --git a/spec/support/shared_examples/ci/stage_shared_examples.rb b/spec/support/shared_examples/ci/stage_shared_examples.rb new file mode 100644 index 00000000000..925974ed11e --- /dev/null +++ b/spec/support/shared_examples/ci/stage_shared_examples.rb @@ -0,0 +1,27 @@ +# frozen_string_literal: true + +shared_examples 'manual playable stage' do |stage_type| + let(:stage) { build(stage_type, status: status) } + + describe '#manual_playable?' do + subject { stage.manual_playable? } + + context 'when is manual' do + let(:status) { 'manual' } + + it { is_expected.to be_truthy } + end + + context 'when is scheduled' do + let(:status) { 'scheduled' } + + it { is_expected.to be_truthy } + end + + context 'when is skipped' do + let(:status) { 'skipped' } + + it { is_expected.to be_truthy } + end + end +end diff --git a/spec/support/shared_examples/ci_trace_shared_examples.rb b/spec/support/shared_examples/ci_trace_shared_examples.rb index c603421d748..db935bcb388 100644 --- a/spec/support/shared_examples/ci_trace_shared_examples.rb +++ b/spec/support/shared_examples/ci_trace_shared_examples.rb @@ -329,14 +329,6 @@ shared_examples_for 'trace with disabled live trace feature' do it_behaves_like 'read successfully with IO' end - context 'when current_path (with project_ci_id) exists' do - before do - expect(trace).to receive(:deprecated_path) { expand_fixture_path('trace/sample_trace') } - end - - it_behaves_like 'read successfully with IO' - end - context 'when db trace exists' do before do build.send(:write_attribute, :trace, "data") @@ -396,37 +388,6 @@ shared_examples_for 'trace with disabled live trace feature' do end end - context 'deprecated path' do - let(:path) { trace.send(:deprecated_path) } - - context 'with valid ci_id' do - before do - build.project.update(ci_id: 1000) - - FileUtils.mkdir_p(File.dirname(path)) - - File.open(path, "w") do |file| - file.write("data") - end - end - - it "trace exist" do - expect(trace.exist?).to be(true) - end - - it "can be erased" do - trace.erase! - expect(trace.exist?).to be(false) - end - end - - context 'without valid ci_id' do - it "does not return deprecated path" do - expect(path).to be_nil - end - end - end - context 'stored in database' do before do build.send(:write_attribute, :trace, "data") diff --git a/spec/workers/cluster_configure_worker_spec.rb b/spec/workers/cluster_configure_worker_spec.rb index bdb8e0e9c84..daf014ac574 100644 --- a/spec/workers/cluster_configure_worker_spec.rb +++ b/spec/workers/cluster_configure_worker_spec.rb @@ -68,6 +68,16 @@ describe ClusterConfigureWorker, '#perform' do it_behaves_like 'configured cluster' end + context 'when cluster is not managed' do + let(:cluster) { create(:cluster, :not_managed) } + + it 'does not configure the cluster' do + expect(Clusters::RefreshService).not_to receive(:create_or_update_namespaces_for_cluster) + + described_class.new.perform(cluster.id) + end + end + context 'when cluster does not exist' do it 'does not provision a cluster' do expect_any_instance_of(Clusters::Gcp::Kubernetes::CreateOrUpdateNamespaceService).not_to receive(:execute) diff --git a/spec/workers/git_garbage_collect_worker_spec.rb b/spec/workers/git_garbage_collect_worker_spec.rb index 2459638c3e6..cc1c23bb9e7 100644 --- a/spec/workers/git_garbage_collect_worker_spec.rb +++ b/spec/workers/git_garbage_collect_worker_spec.rb @@ -115,6 +115,19 @@ describe GitGarbageCollectWorker do end end + context "pack_refs" do + before do + expect(subject).to receive(:get_lease_uuid).and_return(lease_uuid) + end + + it "calls Gitaly" do + expect_any_instance_of(Gitlab::GitalyClient::RefService).to receive(:pack_refs) + .and_return(nil) + + subject.perform(project.id, :pack_refs, lease_key, lease_uuid) + end + end + context "repack_incremental" do before do expect(subject).to receive(:get_lease_uuid).and_return(lease_uuid) |