diff options
345 files changed, 5331 insertions, 1430 deletions
diff --git a/.gitlab/merge_request_templates/Change documentation location.md b/.gitlab/merge_request_templates/Change documentation location.md index c80af95d5e5..7dc80a641c4 100644 --- a/.gitlab/merge_request_templates/Change documentation location.md +++ b/.gitlab/merge_request_templates/Change documentation location.md @@ -22,7 +22,7 @@ https://docs.gitlab.com/ce/development/documentation/index.html#changing-documen - [ ] Make sure internal links pointing to the document in question are not broken. - [ ] Search and replace any links referring to old docs in GitLab Rails app, specifically under the `app/views/` and `ee/app/views` (for GitLab EE) directories. -- [ ] Make sure to add [`redirect_from`](https://docs.gitlab.com/ce/development/writing_documentation.html#redirections-for-pages-with-disqus-comments) +- [ ] Make sure to add [`redirect_from`](https://docs.gitlab.com/ce/development/documentation/index.html#redirections-for-pages-with-disqus-comments) to the new document if there are any Disqus comments on the old document thread. - [ ] Update the link in `features.yml` (if applicable) - [ ] If working on CE and the `ee-compat-check` jobs fails, submit an MR to EE @@ -100,7 +100,7 @@ gem 'carrierwave', '~> 1.3' gem 'mini_magick' # for backups -gem 'fog-aws', '~> 3.3' +gem 'fog-aws', '~> 3.5' # Locked until fog-google resolves https://github.com/fog/fog-google/issues/421. # Also see config/initializers/fog_core_patch.rb. gem 'fog-core', '= 2.1.0' @@ -431,7 +431,7 @@ group :ed25519 do end # Gitaly GRPC client -gem 'gitaly-proto', '~> 1.36.0', require: 'gitaly' +gem 'gitaly-proto', '~> 1.37.0', require: 'gitaly' gem 'grpc', '~> 1.19.0' diff --git a/Gemfile.lock b/Gemfile.lock index bf469de3835..60939ae918c 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -253,7 +253,7 @@ GEM fog-json ipaddress (~> 0.8) xml-simple (~> 1.1) - fog-aws (3.3.0) + fog-aws (3.5.2) fog-core (~> 2.1) fog-json (~> 1.1) fog-xml (~> 0.1) @@ -310,7 +310,7 @@ GEM gettext_i18n_rails (>= 0.7.1) po_to_json (>= 1.0.0) rails (>= 3.2.0) - gitaly-proto (1.36.0) + gitaly-proto (1.37.0) grpc (~> 1.0) github-markup (1.7.0) gitlab-labkit (0.3.0) @@ -1105,7 +1105,7 @@ DEPENDENCIES flipper-active_support_cache_store (~> 0.13.0) flowdock (~> 0.7) fog-aliyun (~> 0.3) - fog-aws (~> 3.3) + fog-aws (~> 3.5) fog-core (= 2.1.0) fog-google (~> 1.8) fog-local (~> 0.6) @@ -1119,7 +1119,7 @@ DEPENDENCIES gettext (~> 3.2.2) gettext_i18n_rails (~> 1.8.0) gettext_i18n_rails_js (~> 1.3) - gitaly-proto (~> 1.36.0) + gitaly-proto (~> 1.37.0) github-markup (~> 1.7.0) gitlab-labkit (~> 0.3.0) gitlab-markup (~> 1.7.0) diff --git a/app/assets/javascripts/behaviors/markdown/render_mermaid.js b/app/assets/javascripts/behaviors/markdown/render_mermaid.js index b23de36f860..dbc28beffbe 100644 --- a/app/assets/javascripts/behaviors/markdown/render_mermaid.js +++ b/app/assets/javascripts/behaviors/markdown/render_mermaid.js @@ -36,7 +36,8 @@ export default function renderMermaid($els) { }); $els.each((i, el) => { - const source = el.textContent; + // Mermaid doesn't like `<br />` tags, so collapse all like tags into `<br>`, which is parsed correctly. + const source = el.textContent.replace(/<br\s*\/>/g, '<br>'); /** * Restrict the rendering to a certain amount of character to diff --git a/app/assets/javascripts/boards/components/board_form.vue b/app/assets/javascripts/boards/components/board_form.vue new file mode 100644 index 00000000000..6754abf4019 --- /dev/null +++ b/app/assets/javascripts/boards/components/board_form.vue @@ -0,0 +1,216 @@ +<script> +import Flash from '~/flash'; +import DeprecatedModal from '~/vue_shared/components/deprecated_modal.vue'; +import { visitUrl } from '~/lib/utils/url_utility'; +import boardsStore from '~/boards/stores/boards_store'; + +const boardDefaults = { + id: false, + name: '', + labels: [], + milestone_id: undefined, + assignee: {}, + assignee_id: undefined, + weight: null, +}; + +export default { + components: { + BoardScope: () => import('ee_component/boards/components/board_scope.vue'), + DeprecatedModal, + }, + props: { + canAdminBoard: { + type: Boolean, + required: true, + }, + milestonePath: { + type: String, + required: true, + }, + labelsPath: { + type: String, + required: true, + }, + scopedIssueBoardFeatureEnabled: { + type: Boolean, + required: false, + default: false, + }, + projectId: { + type: Number, + required: false, + default: 0, + }, + groupId: { + type: Number, + required: false, + default: 0, + }, + weights: { + type: Array, + required: false, + default: () => [], + }, + enableScopedLabels: { + type: Boolean, + required: false, + default: false, + }, + scopedLabelsDocumentationLink: { + type: String, + required: false, + default: '#', + }, + }, + data() { + return { + board: { ...boardDefaults, ...this.currentBoard }, + currentBoard: boardsStore.state.currentBoard, + currentPage: boardsStore.state.currentPage, + isLoading: false, + }; + }, + computed: { + isNewForm() { + return this.currentPage === 'new'; + }, + isDeleteForm() { + return this.currentPage === 'delete'; + }, + isEditForm() { + return this.currentPage === 'edit'; + }, + isVisible() { + return this.currentPage !== ''; + }, + buttonText() { + if (this.isNewForm) { + return 'Create board'; + } + if (this.isDeleteForm) { + return 'Delete'; + } + return 'Save changes'; + }, + buttonKind() { + if (this.isNewForm) { + return 'success'; + } + if (this.isDeleteForm) { + return 'danger'; + } + return 'info'; + }, + title() { + if (this.isNewForm) { + return 'Create new board'; + } + if (this.isDeleteForm) { + return 'Delete board'; + } + if (this.readonly) { + return 'Board scope'; + } + return 'Edit board'; + }, + readonly() { + return !this.canAdminBoard; + }, + submitDisabled() { + return this.isLoading || this.board.name.length === 0; + }, + }, + mounted() { + this.resetFormState(); + if (this.$refs.name) { + this.$refs.name.focus(); + } + }, + methods: { + submit() { + if (this.board.name.length === 0) return; + this.isLoading = true; + if (this.isDeleteForm) { + gl.boardService + .deleteBoard(this.currentBoard) + .then(() => { + visitUrl(boardsStore.rootPath); + }) + .catch(() => { + Flash('Failed to delete board. Please try again.'); + this.isLoading = false; + }); + } else { + gl.boardService + .createBoard(this.board) + .then(resp => resp.data) + .then(data => { + visitUrl(data.board_path); + }) + .catch(() => { + Flash('Unable to save your changes. Please try again.'); + this.isLoading = false; + }); + } + }, + cancel() { + boardsStore.showPage(''); + }, + resetFormState() { + if (this.isNewForm) { + // Clear the form when we open the "New board" modal + this.board = { ...boardDefaults }; + } else if (this.currentBoard && Object.keys(this.currentBoard).length) { + this.board = { ...boardDefaults, ...this.currentBoard }; + } + }, + }, +}; +</script> + +<template> + <deprecated-modal + v-show="isVisible" + :hide-footer="readonly" + :title="title" + :primary-button-label="buttonText" + :kind="buttonKind" + :submit-disabled="submitDisabled" + modal-dialog-class="board-config-modal" + @cancel="cancel" + @submit="submit" + > + <template slot="body"> + <p v-if="isDeleteForm">Are you sure you want to delete this board?</p> + <form v-else class="js-board-config-modal" @submit.prevent> + <div v-if="!readonly" class="append-bottom-20"> + <label class="form-section-title label-bold" for="board-new-name"> Board name </label> + <input + id="board-new-name" + ref="name" + v-model="board.name" + class="form-control" + type="text" + placeholder="Enter board name" + @keyup.enter="submit" + /> + </div> + + <board-scope + v-if="scopedIssueBoardFeatureEnabled" + :collapse-scope="isNewForm" + :board="board" + :can-admin-board="canAdminBoard" + :milestone-path="milestonePath" + :labels-path="labelsPath" + :scoped-labels-documentation-link="scopedLabelsDocumentationLink" + :enable-scoped-labels="enableScopedLabels" + :project-id="projectId" + :group-id="groupId" + :weights="weights" + /> + </form> + </template> + </deprecated-modal> +</template> diff --git a/app/assets/javascripts/boards/components/boards_selector.vue b/app/assets/javascripts/boards/components/boards_selector.vue new file mode 100644 index 00000000000..b05de4538f2 --- /dev/null +++ b/app/assets/javascripts/boards/components/boards_selector.vue @@ -0,0 +1,334 @@ +<script> +import { throttle } from 'underscore'; +import { + GlLoadingIcon, + GlSearchBoxByType, + GlDropdown, + GlDropdownDivider, + GlDropdownHeader, + GlDropdownItem, +} from '@gitlab/ui'; + +import Icon from '~/vue_shared/components/icon.vue'; +import httpStatusCodes from '~/lib/utils/http_status'; +import boardsStore from '../stores/boards_store'; +import BoardForm from './board_form.vue'; + +const MIN_BOARDS_TO_VIEW_RECENT = 10; + +export default { + name: 'BoardsSelector', + components: { + Icon, + BoardForm, + GlLoadingIcon, + GlSearchBoxByType, + GlDropdown, + GlDropdownDivider, + GlDropdownHeader, + GlDropdownItem, + }, + props: { + currentBoard: { + type: Object, + required: true, + }, + milestonePath: { + type: String, + required: true, + }, + throttleDuration: { + type: Number, + default: 200, + }, + boardBaseUrl: { + type: String, + required: true, + }, + hasMissingBoards: { + type: Boolean, + required: true, + }, + canAdminBoard: { + type: Boolean, + required: true, + }, + multipleIssueBoardsAvailable: { + type: Boolean, + required: true, + }, + labelsPath: { + type: String, + required: true, + }, + projectId: { + type: Number, + required: true, + }, + groupId: { + type: Number, + required: true, + }, + scopedIssueBoardFeatureEnabled: { + type: Boolean, + required: true, + }, + weights: { + type: Array, + required: true, + }, + enabledScopedLabels: { + type: Boolean, + required: false, + default: false, + }, + scopedLabelsDocumentationLink: { + type: String, + required: false, + default: '#', + }, + }, + data() { + return { + loading: true, + hasScrollFade: false, + scrollFadeInitialized: false, + boards: [], + recentBoards: [], + state: boardsStore.state, + throttledSetScrollFade: throttle(this.setScrollFade, this.throttleDuration), + contentClientHeight: 0, + maxPosition: 0, + store: boardsStore, + filterTerm: '', + }; + }, + computed: { + currentPage() { + return this.state.currentPage; + }, + filteredBoards() { + return this.boards.filter(board => + board.name.toLowerCase().includes(this.filterTerm.toLowerCase()), + ); + }, + reload: { + get() { + return this.state.reload; + }, + set(newValue) { + this.state.reload = newValue; + }, + }, + board() { + return this.state.currentBoard; + }, + showDelete() { + return this.boards.length > 1; + }, + scrollFadeClass() { + return { + 'fade-out': !this.hasScrollFade, + }; + }, + showRecentSection() { + return ( + this.recentBoards.length && + this.boards.length > MIN_BOARDS_TO_VIEW_RECENT && + !this.filterTerm.length + ); + }, + }, + watch: { + filteredBoards() { + this.scrollFadeInitialized = false; + this.$nextTick(this.setScrollFade); + }, + reload() { + if (this.reload) { + this.boards = []; + this.recentBoards = []; + this.loading = true; + this.reload = false; + + this.loadBoards(false); + } + }, + }, + created() { + boardsStore.setCurrentBoard(this.currentBoard); + }, + methods: { + showPage(page) { + boardsStore.showPage(page); + }, + loadBoards(toggleDropdown = true) { + if (toggleDropdown && this.boards.length > 0) { + return; + } + + const recentBoardsPromise = new Promise((resolve, reject) => + gl.boardService + .recentBoards() + .then(resolve) + .catch(err => { + /** + * If user is unauthorized we'd still want to resolve the + * request to display all boards. + */ + if (err.response.status === httpStatusCodes.UNAUTHORIZED) { + resolve({ data: [] }); // recent boards are empty + return; + } + reject(err); + }), + ); + + Promise.all([gl.boardService.allBoards(), recentBoardsPromise]) + .then(([allBoards, recentBoards]) => [allBoards.data, recentBoards.data]) + .then(([allBoardsJson, recentBoardsJson]) => { + this.loading = false; + this.boards = allBoardsJson; + this.recentBoards = recentBoardsJson; + }) + .then(() => this.$nextTick()) // Wait for boards list in DOM + .then(() => { + this.setScrollFade(); + }) + .catch(() => { + this.loading = false; + }); + }, + isScrolledUp() { + const { content } = this.$refs; + const currentPosition = this.contentClientHeight + content.scrollTop; + + return content && currentPosition < this.maxPosition; + }, + initScrollFade() { + this.scrollFadeInitialized = true; + + const { content } = this.$refs; + + this.contentClientHeight = content.clientHeight; + this.maxPosition = content.scrollHeight; + }, + setScrollFade() { + if (!this.scrollFadeInitialized) this.initScrollFade(); + + this.hasScrollFade = this.isScrolledUp(); + }, + }, +}; +</script> + +<template> + <div class="boards-switcher js-boards-selector append-right-10"> + <span class="boards-selector-wrapper js-boards-selector-wrapper"> + <gl-dropdown + toggle-class="dropdown-menu-toggle js-dropdown-toggle" + menu-class="flex-column dropdown-extended-height" + :text="board.name" + @show="loadBoards" + > + <div> + <div class="dropdown-title mb-0" @mousedown.prevent> + {{ s__('IssueBoards|Switch board') }} + </div> + </div> + + <gl-dropdown-header class="mt-0"> + <gl-search-box-by-type ref="searchBox" v-model="filterTerm" /> + </gl-dropdown-header> + + <div + v-if="!loading" + ref="content" + class="dropdown-content flex-fill" + @scroll.passive="throttledSetScrollFade" + > + <gl-dropdown-item + v-show="filteredBoards.length === 0" + class="no-pointer-events text-secondary" + > + {{ s__('IssueBoards|No matching boards found') }} + </gl-dropdown-item> + + <h6 v-if="showRecentSection" class="dropdown-bold-header my-0"> + {{ __('Recent') }} + </h6> + + <template v-if="showRecentSection"> + <gl-dropdown-item + v-for="recentBoard in recentBoards" + :key="`recent-${recentBoard.id}`" + class="js-dropdown-item" + :href="`${boardBaseUrl}/${recentBoard.id}`" + > + {{ recentBoard.name }} + </gl-dropdown-item> + </template> + + <hr v-if="showRecentSection" class="my-1" /> + + <h6 v-if="showRecentSection" class="dropdown-bold-header my-0"> + {{ __('All') }} + </h6> + + <gl-dropdown-item + v-for="otherBoard in filteredBoards" + :key="otherBoard.id" + class="js-dropdown-item" + :href="`${boardBaseUrl}/${otherBoard.id}`" + > + {{ otherBoard.name }} + </gl-dropdown-item> + <gl-dropdown-item v-if="hasMissingBoards" class="small unclickable"> + {{ + s__( + 'IssueBoards|Some of your boards are hidden, activate a license to see them again.', + ) + }} + </gl-dropdown-item> + </div> + + <div + v-show="filteredBoards.length > 0" + class="dropdown-content-faded-mask" + :class="scrollFadeClass" + ></div> + + <gl-loading-icon v-if="loading" /> + + <div v-if="canAdminBoard"> + <gl-dropdown-divider /> + + <gl-dropdown-item v-if="multipleIssueBoardsAvailable" @click.prevent="showPage('new')"> + {{ s__('IssueBoards|Create new board') }} + </gl-dropdown-item> + + <gl-dropdown-item + v-if="showDelete" + class="text-danger" + @click.prevent="showPage('delete')" + > + {{ s__('IssueBoards|Delete board') }} + </gl-dropdown-item> + </div> + </gl-dropdown> + + <board-form + v-if="currentPage" + :milestone-path="milestonePath" + :labels-path="labelsPath" + :project-id="projectId" + :group-id="groupId" + :can-admin-board="canAdminBoard" + :scoped-issue-board-feature-enabled="scopedIssueBoardFeatureEnabled" + :weights="weights" + :enable-scoped-labels="enabledScopedLabels" + :scoped-labels-documentation-link="scopedLabelsDocumentationLink" + /> + </span> + </div> +</template> diff --git a/app/assets/javascripts/boards/components/modal/footer.vue b/app/assets/javascripts/boards/components/modal/footer.vue index eea0fb71c1a..5f100c617a0 100644 --- a/app/assets/javascripts/boards/components/modal/footer.vue +++ b/app/assets/javascripts/boards/components/modal/footer.vue @@ -1,4 +1,5 @@ <script> +import footerEEMixin from 'ee_else_ce/boards/mixins/modal_footer'; import Flash from '../../../flash'; import { __, n__ } from '../../../locale'; import ListsDropdown from './lists_dropdown.vue'; @@ -10,7 +11,7 @@ export default { components: { ListsDropdown, }, - mixins: [modalMixin], + mixins: [modalMixin, footerEEMixin], data() { return { modal: ModalStore.store, diff --git a/app/assets/javascripts/boards/index.js b/app/assets/javascripts/boards/index.js index d6a5372b22d..f5a617a85ad 100644 --- a/app/assets/javascripts/boards/index.js +++ b/app/assets/javascripts/boards/index.js @@ -1,7 +1,6 @@ import $ from 'jquery'; import Vue from 'vue'; -import mountMultipleBoardsSwitcher from 'ee_else_ce/boards/mount_multiple_boards_switcher'; import Flash from '~/flash'; import { __ } from '~/locale'; import './models/label'; @@ -31,6 +30,7 @@ import { } from '~/lib/utils/common_utils'; import boardConfigToggle from 'ee_else_ce/boards/config_toggle'; import toggleFocusMode from 'ee_else_ce/boards/toggle_focus'; +import mountMultipleBoardsSwitcher from './mount_multiple_boards_switcher'; let issueBoardsApp; diff --git a/app/assets/javascripts/boards/mixins/modal_footer.js b/app/assets/javascripts/boards/mixins/modal_footer.js new file mode 100644 index 00000000000..ff8b4c56321 --- /dev/null +++ b/app/assets/javascripts/boards/mixins/modal_footer.js @@ -0,0 +1 @@ +export default {}; diff --git a/app/assets/javascripts/boards/mount_multiple_boards_switcher.js b/app/assets/javascripts/boards/mount_multiple_boards_switcher.js index bdb14a7f2f2..8d22f009784 100644 --- a/app/assets/javascripts/boards/mount_multiple_boards_switcher.js +++ b/app/assets/javascripts/boards/mount_multiple_boards_switcher.js @@ -1,2 +1,35 @@ -// this will be moved from EE to CE as part of https://gitlab.com/gitlab-org/gitlab-ce/issues/53811 -export default () => {}; +import Vue from 'vue'; +import { parseBoolean } from '~/lib/utils/common_utils'; +import BoardsSelector from '~/boards/components/boards_selector.vue'; + +export default () => { + const boardsSwitcherElement = document.getElementById('js-multiple-boards-switcher'); + return new Vue({ + el: boardsSwitcherElement, + components: { + BoardsSelector, + }, + data() { + const { dataset } = boardsSwitcherElement; + + const boardsSelectorProps = { + ...dataset, + currentBoard: JSON.parse(dataset.currentBoard), + hasMissingBoards: parseBoolean(dataset.hasMissingBoards), + canAdminBoard: parseBoolean(dataset.canAdminBoard), + multipleIssueBoardsAvailable: parseBoolean(dataset.multipleIssueBoardsAvailable), + projectId: Number(dataset.projectId), + groupId: Number(dataset.groupId), + scopedIssueBoardFeatureEnabled: parseBoolean(dataset.scopedIssueBoardFeatureEnabled), + weights: JSON.parse(dataset.weights), + }; + + return { boardsSelectorProps }; + }, + render(createElement) { + return createElement(BoardsSelector, { + props: this.boardsSelectorProps, + }); + }, + }); +}; diff --git a/app/assets/javascripts/create_merge_request_dropdown.js b/app/assets/javascripts/create_merge_request_dropdown.js index 052168bb21c..dce9c1a5410 100644 --- a/app/assets/javascripts/create_merge_request_dropdown.js +++ b/app/assets/javascripts/create_merge_request_dropdown.js @@ -182,7 +182,7 @@ export default class CreateMergeRequestDropdown { } enable() { - if (!canCreateConfidentialMergeRequest()) return; + if (isConfidentialIssue() && !canCreateConfidentialMergeRequest()) return; this.createMergeRequestButton.classList.remove('disabled'); this.createMergeRequestButton.removeAttribute('disabled'); diff --git a/app/assets/javascripts/diffs/components/diff_file.vue b/app/assets/javascripts/diffs/components/diff_file.vue index 63350fafefa..2514274224d 100644 --- a/app/assets/javascripts/diffs/components/diff_file.vue +++ b/app/assets/javascripts/diffs/components/diff_file.vue @@ -67,6 +67,18 @@ export default { errorMessage() { return this.file.viewer.error_message; }, + forkMessage() { + return sprintf( + __( + "You're not allowed to %{tag_start}edit%{tag_end} files in this project directly. Please fork this project, make your changes there, and submit a merge request.", + ), + { + tag_start: '<span class="js-file-fork-suggestion-section-action">', + tag_end: '</span>', + }, + false, + ); + }, }, watch: { isCollapsed: function fileCollapsedWatch(newVal, oldVal) { @@ -150,12 +162,7 @@ export default { /> <div v-if="forkMessageVisible" class="js-file-fork-suggestion-section file-fork-suggestion"> - <span class="file-fork-suggestion-note"> - {{ sprintf(__("You're not allowed to %{tag_start}edit%{tag_end} files in this project - directly. Please fork this project, make your changes there, and submit a merge request."), - { tag_start: '<span class="js-file-fork-suggestion-section-action">', tag_end: '</span>' }) - }} - </span> + <span class="file-fork-suggestion-note" v-html="forkMessage"></span> <a :href="file.fork_path" class="js-fork-suggestion-button btn btn-grouped btn-inverted btn-success" diff --git a/app/assets/javascripts/issue_show/components/app.vue b/app/assets/javascripts/issue_show/components/app.vue index de2a9664cde..9ca38d6bbfa 100644 --- a/app/assets/javascripts/issue_show/components/app.vue +++ b/app/assets/javascripts/issue_show/components/app.vue @@ -55,6 +55,11 @@ export default { required: false, default: true, }, + zoomMeetingUrl: { + type: String, + required: false, + default: null, + }, issuableRef: { type: String, required: true, @@ -342,7 +347,7 @@ export default { :title-text="state.titleText" :show-inline-edit-button="showInlineEditButton" /> - <pinned-links :description-html="state.descriptionHtml" /> + <pinned-links :zoom-meeting-url="zoomMeetingUrl" /> <description-component v-if="state.descriptionHtml" :can-update="canUpdate" diff --git a/app/assets/javascripts/issue_show/components/locked_warning.vue b/app/assets/javascripts/issue_show/components/locked_warning.vue index 2f3e611e089..19c7a11d87b 100644 --- a/app/assets/javascripts/issue_show/components/locked_warning.vue +++ b/app/assets/javascripts/issue_show/components/locked_warning.vue @@ -1,18 +1,27 @@ <script> +import { __, sprintf } from '~/locale'; + export default { computed: { currentPath() { return window.location.pathname; }, + alertMessage() { + return sprintf( + __( + 'Someone edited the issue at the same time you did. Please check out %{linkStart}the issue%{linkEnd} and make sure your changes will not unintentionally remove theirs.', + ), + { + linkStart: `<a href="${this.currentPath}" target="_blank" rel="nofollow">`, + linkEnd: `</a>`, + }, + false, + ); + }, }, }; </script> <template> - <div class="alert alert-danger"> - {{ sprintf(__("Someone edited the issue at the same time you did. Please check out - %{linkStart}%the issue%{linkEnd} and make sure your changes will not unintentionally remove - theirs."), { linkStart: `<a href="${currentPath}" target="_blank" rel="nofollow">` linkEnd: '</a - >', }) }} - </div> + <div class="alert alert-danger" v-html="alertMessage"></div> </template> diff --git a/app/assets/javascripts/issue_show/components/pinned_links.vue b/app/assets/javascripts/issue_show/components/pinned_links.vue index 7a54b26bc2b..965e8a3d751 100644 --- a/app/assets/javascripts/issue_show/components/pinned_links.vue +++ b/app/assets/javascripts/issue_show/components/pinned_links.vue @@ -8,40 +8,19 @@ export default { GlLink, }, props: { - descriptionHtml: { + zoomMeetingUrl: { type: String, - required: true, - }, - }, - computed: { - linksInDescription() { - const el = document.createElement('div'); - el.innerHTML = this.descriptionHtml; - return [...el.querySelectorAll('a')].map(a => a.href); - }, - // Detect links matching the following formats: - // Zoom Start links: https://zoom.us/s/<meeting-id> - // Zoom Join links: https://zoom.us/j/<meeting-id> - // Personal Zoom links: https://zoom.us/my/<meeting-id> - // Vanity Zoom links: https://gitlab.zoom.us/j/<meeting-id> (also /s and /my) - zoomHref() { - const zoomRegex = /^https:\/\/([\w\d-]+\.)?zoom\.us\/(s|j|my)\/.+/; - return this.linksInDescription.reduce((acc, currentLink) => { - let lastLink = acc; - if (zoomRegex.test(currentLink)) { - lastLink = currentLink; - } - return lastLink; - }, ''); + required: false, + default: null, }, }, }; </script> <template> - <div v-if="zoomHref" class="border-bottom mb-3 mt-n2"> + <div v-if="zoomMeetingUrl" class="border-bottom mb-3 mt-n2"> <gl-link - :href="zoomHref" + :href="zoomMeetingUrl" target="_blank" class="btn btn-inverted btn-secondary btn-sm text-dark mb-3" > diff --git a/app/assets/javascripts/labels_select.js b/app/assets/javascripts/labels_select.js index bea43430edc..f50a6e3b19d 100644 --- a/app/assets/javascripts/labels_select.js +++ b/app/assets/javascripts/labels_select.js @@ -311,7 +311,8 @@ export default class LabelsSelect { // We need to identify which items are actually labels if (label.id) { - selectedClass.push('label-item'); + const selectedLayoutClasses = ['d-flex', 'flex-row', 'text-break-word']; + selectedClass.push('label-item', ...selectedLayoutClasses); linkEl.dataset.labelId = label.id; } diff --git a/app/assets/javascripts/registry/components/app.vue b/app/assets/javascripts/registry/components/app.vue index 7752723baac..38519c220c5 100644 --- a/app/assets/javascripts/registry/components/app.vue +++ b/app/assets/javascripts/registry/components/app.vue @@ -2,6 +2,7 @@ import { mapGetters, mapActions } from 'vuex'; import { GlLoadingIcon } from '@gitlab/ui'; import store from '../stores'; +import clipboardButton from '../../vue_shared/components/clipboard_button.vue'; import CollapsibleContainer from './collapsible_container.vue'; import SvgMessage from './svg_message.vue'; import { s__, sprintf } from '../../locale'; @@ -9,6 +10,7 @@ import { s__, sprintf } from '../../locale'; export default { name: 'RegistryListApp', components: { + clipboardButton, CollapsibleContainer, GlLoadingIcon, SvgMessage, @@ -46,10 +48,10 @@ export default { dockerConnectionErrorText() { return sprintf( s__(`ContainerRegistry|We are having trouble connecting to Docker, which could be due to an - issue with your project name or path. For more information, please review the - %{docLinkStart}Container Registry documentation%{docLinkEnd}.`), + issue with your project name or path. + %{docLinkStart}More Information%{docLinkEnd}`), { - docLinkStart: `<a href="${this.helpPagePath}#docker-connection-error">`, + docLinkStart: `<a href="${this.helpPagePath}#docker-connection-error" target="_blank">`, docLinkEnd: '</a>', }, false, @@ -58,10 +60,10 @@ export default { introText() { return sprintf( s__(`ContainerRegistry|With the Docker Container Registry integrated into GitLab, every - project can have its own space to store its Docker images. Learn more about the - %{docLinkStart}Container Registry%{docLinkEnd}.`), + project can have its own space to store its Docker images. + %{docLinkStart}More Information%{docLinkEnd}`), { - docLinkStart: `<a href="${this.helpPagePath}">`, + docLinkStart: `<a href="${this.helpPagePath}" target="_blank">`, docLinkEnd: '</a>', }, false, @@ -70,14 +72,20 @@ export default { noContainerImagesText() { return sprintf( s__(`ContainerRegistry|With the Container Registry, every project can have its own space to - store its Docker images. Learn more about the %{docLinkStart}Container Registry%{docLinkEnd}.`), + store its Docker images. %{docLinkStart}More Information%{docLinkEnd}`), { - docLinkStart: `<a href="${this.helpPagePath}">`, + docLinkStart: `<a href="${this.helpPagePath}" target="_blank">`, docLinkEnd: '</a>', }, false, ); }, + dockerBuildCommand() { + return `docker build -t ${this.repositoryUrl} .`; + }, + dockerPushCommand() { + return `docker push ${this.repositoryUrl}`; + }, }, created() { this.setMainEndpoint(this.endpoint); @@ -99,7 +107,7 @@ export default { <p v-html="dockerConnectionErrorText"></p> </svg-message> - <gl-loading-icon v-else-if="isLoading" size="md" class="prepend-top-16" /> + <gl-loading-icon v-else-if="isLoading && !characterError" size="md" class="prepend-top-16" /> <div v-else-if="!isLoading && !characterError && repos.length"> <h4>{{ s__('ContainerRegistry|Container Registry') }}</h4> @@ -126,10 +134,27 @@ export default { }} </p> - <pre> - docker build -t {{ repositoryUrl }} . - docker push {{ repositoryUrl }} - </pre> + <div class="input-group append-bottom-10"> + <input :value="dockerBuildCommand" type="text" class="form-control monospace" readonly /> + <span class="input-group-append"> + <clipboard-button + :text="dockerBuildCommand" + :title="s__('ContainerRegistry|Copy build command to clipboard')" + class="input-group-text" + /> + </span> + </div> + + <div class="input-group"> + <input :value="dockerPushCommand" type="text" class="form-control monospace" readonly /> + <span class="input-group-append"> + <clipboard-button + :text="dockerPushCommand" + :title="s__('ContainerRegistry|Copy push command to clipboard')" + class="input-group-text" + /> + </span> + </div> </svg-message> </div> </template> diff --git a/app/assets/javascripts/registry/components/svg_message.vue b/app/assets/javascripts/registry/components/svg_message.vue index d0d44bf2d14..617093e054e 100644 --- a/app/assets/javascripts/registry/components/svg_message.vue +++ b/app/assets/javascripts/registry/components/svg_message.vue @@ -15,10 +15,12 @@ export default { </script> <template> - <div :id="id" class="empty-state container-message mw-70p"> + <div :id="id" class="empty-state container-message"> <div class="svg-content"> <img :src="svgPath" class="flex-align-self-center" /> </div> - <slot></slot> + <div class="text-content"> + <slot></slot> + </div> </div> </template> diff --git a/app/assets/javascripts/related_merge_requests/components/related_merge_requests.vue b/app/assets/javascripts/related_merge_requests/components/related_merge_requests.vue index 6d908524da9..f0112a5a623 100644 --- a/app/assets/javascripts/related_merge_requests/components/related_merge_requests.vue +++ b/app/assets/javascripts/related_merge_requests/components/related_merge_requests.vue @@ -65,7 +65,7 @@ export default { <template> <div v-if="isFetchingMergeRequests || (!isFetchingMergeRequests && totalCount)"> - <div id="merge-requests" class="card-slim mt-3"> + <div id="merge-requests" class="card card-slim mt-3"> <div class="card-header"> <div class="card-title mt-0 mb-0 h5 merge-requests-title"> <span class="mr-1"> diff --git a/app/assets/javascripts/reports/components/issue_status_icon.vue b/app/assets/javascripts/reports/components/issue_status_icon.vue index 04fba43b2f3..386653b9444 100644 --- a/app/assets/javascripts/reports/components/issue_status_icon.vue +++ b/app/assets/javascripts/reports/components/issue_status_icon.vue @@ -16,7 +16,7 @@ export default { statusIconSize: { type: Number, required: false, - default: 32, + default: 24, }, }, computed: { diff --git a/app/assets/javascripts/reports/components/report_item.vue b/app/assets/javascripts/reports/components/report_item.vue index 2be9c37b00a..d477fafd3f5 100644 --- a/app/assets/javascripts/reports/components/report_item.vue +++ b/app/assets/javascripts/reports/components/report_item.vue @@ -27,7 +27,7 @@ export default { statusIconSize: { type: Number, required: false, - default: 32, + default: 24, }, isNew: { type: Boolean, @@ -43,12 +43,15 @@ export default { }; </script> <template> - <li :class="{ 'is-dismissed': issue.isDismissed }" class="report-block-list-issue"> + <li + :class="{ 'is-dismissed': issue.isDismissed }" + class="report-block-list-issue justify-content-center align-items-center" + > <issue-status-icon v-if="showReportSectionStatusIcon" :status="status" :status-icon-size="statusIconSize" - class="append-right-5" + class="append-right-default" /> <component :is="component" v-if="component" :issue="issue" :status="status" :is-new="isNew" /> diff --git a/app/assets/javascripts/reports/components/report_section.vue b/app/assets/javascripts/reports/components/report_section.vue index 3d576caaf8f..9bc3e6388e3 100644 --- a/app/assets/javascripts/reports/components/report_section.vue +++ b/app/assets/javascripts/reports/components/report_section.vue @@ -165,8 +165,8 @@ export default { <template> <section class="media-section"> <div class="media"> - <status-icon :status="statusIconName" /> - <div class="media-body d-flex flex-align-self-center"> + <status-icon :status="statusIconName" :size="24" /> + <div class="media-body d-flex flex-align-self-center prepend-left-default"> <span class="js-code-text code-text"> {{ headerText }} <slot :name="slotName"></slot> diff --git a/app/assets/javascripts/reports/components/summary_row.vue b/app/assets/javascripts/reports/components/summary_row.vue index 97a68531d29..1caf52431e2 100644 --- a/app/assets/javascripts/reports/components/summary_row.vue +++ b/app/assets/javascripts/reports/components/summary_row.vue @@ -44,10 +44,16 @@ export default { }; </script> <template> - <div class="report-block-list-issue report-block-list-issue-parent"> - <div class="report-block-list-icon append-right-10 prepend-left-5"> - <gl-loading-icon v-if="statusIcon === 'loading'" css-class="report-block-list-loading-icon" /> - <ci-icon v-else :status="iconStatus" /> + <div + class="report-block-list-issue report-block-list-issue-parent justify-content-center align-items-center" + > + <div class="report-block-list-icon append-right-default"> + <gl-loading-icon + v-if="statusIcon === 'loading'" + css-class="report-block-list-loading-icon" + size="md" + /> + <ci-icon v-else :status="iconStatus" :size="24" /> </div> <div class="report-block-list-issue-description"> diff --git a/app/assets/javascripts/repository/components/breadcrumbs.vue b/app/assets/javascripts/repository/components/breadcrumbs.vue index 67963dc1923..afb58a60155 100644 --- a/app/assets/javascripts/repository/components/breadcrumbs.vue +++ b/app/assets/javascripts/repository/components/breadcrumbs.vue @@ -1,12 +1,41 @@ <script> +import { GlDropdown, GlDropdownDivider, GlDropdownHeader, GlDropdownItem } from '@gitlab/ui'; +import { __ } from '../../locale'; +import Icon from '../../vue_shared/components/icon.vue'; import getRefMixin from '../mixins/get_ref'; import getProjectShortPath from '../queries/getProjectShortPath.query.graphql'; +import getProjectPath from '../queries/getProjectPath.query.graphql'; +import getPermissions from '../queries/getPermissions.query.graphql'; + +const ROW_TYPES = { + header: 'header', + divider: 'divider', +}; export default { + components: { + GlDropdown, + GlDropdownDivider, + GlDropdownHeader, + GlDropdownItem, + Icon, + }, apollo: { projectShortPath: { query: getProjectShortPath, }, + projectPath: { + query: getProjectPath, + }, + userPermissions: { + query: getPermissions, + variables() { + return { + projectPath: this.projectPath, + }; + }, + update: data => data.project.userPermissions, + }, }, mixins: [getRefMixin], props: { @@ -15,10 +44,52 @@ export default { required: false, default: '/', }, + canCollaborate: { + type: Boolean, + required: false, + default: false, + }, + canEditTree: { + type: Boolean, + required: false, + default: false, + }, + newBranchPath: { + type: String, + required: false, + default: null, + }, + newTagPath: { + type: String, + required: false, + default: null, + }, + newBlobPath: { + type: String, + required: false, + default: null, + }, + forkNewBlobPath: { + type: String, + required: false, + default: null, + }, + forkNewDirectoryPath: { + type: String, + required: false, + default: null, + }, + forkUploadBlobPath: { + type: String, + required: false, + default: null, + }, }, data() { return { projectShortPath: '', + projectPath: '', + userPermissions: {}, }; }, computed: { @@ -39,11 +110,112 @@ export default { [{ name: this.projectShortPath, path: '/', to: `/tree/${this.ref}/` }], ); }, + canCreateMrFromFork() { + return this.userPermissions.forkProject && this.userPermissions.createMergeRequestIn; + }, + dropdownItems() { + const items = []; + + if (this.canEditTree) { + items.push( + { + type: ROW_TYPES.header, + text: __('This directory'), + }, + { + attrs: { + href: this.newBlobPath, + class: 'qa-new-file-option', + }, + text: __('New file'), + }, + { + attrs: { + href: '#modal-upload-blob', + 'data-target': '#modal-upload-blob', + 'data-toggle': 'modal', + }, + text: __('Upload file'), + }, + { + attrs: { + href: '#modal-create-new-dir', + 'data-target': '#modal-create-new-dir', + 'data-toggle': 'modal', + }, + text: __('New directory'), + }, + ); + } else if (this.canCreateMrFromFork) { + items.push( + { + attrs: { + href: this.forkNewBlobPath, + 'data-method': 'post', + }, + text: __('New file'), + }, + { + attrs: { + href: this.forkUploadBlobPath, + 'data-method': 'post', + }, + text: __('Upload file'), + }, + { + attrs: { + href: this.forkNewDirectoryPath, + 'data-method': 'post', + }, + text: __('New directory'), + }, + ); + } + + if (this.userPermissions.pushCode) { + items.push( + { + type: ROW_TYPES.divider, + }, + { + type: ROW_TYPES.header, + text: __('This repository'), + }, + { + attrs: { + href: this.newBranchPath, + }, + text: __('New branch'), + }, + { + attrs: { + href: this.newTagPath, + }, + text: __('New tag'), + }, + ); + } + + return items; + }, + renderAddToTreeDropdown() { + return this.canCollaborate || this.canCreateMrFromFork; + }, }, methods: { isLast(i) { return i === this.pathLinks.length - 1; }, + getComponent(type) { + switch (type) { + case ROW_TYPES.divider: + return 'gl-dropdown-divider'; + case ROW_TYPES.header: + return 'gl-dropdown-header'; + default: + return 'gl-dropdown-item'; + } + }, }, }; </script> @@ -56,6 +228,20 @@ export default { {{ link.name }} </router-link> </li> + <li v-if="renderAddToTreeDropdown" class="breadcrumb-item"> + <gl-dropdown toggle-class="add-to-tree qa-add-to-tree ml-1"> + <template slot="button-content"> + <span class="sr-only">{{ __('Add to tree') }}</span> + <icon name="plus" :size="16" class="float-left" /> + <icon name="arrow-down" :size="16" class="float-left" /> + </template> + <template v-for="(item, i) in dropdownItems"> + <component :is="getComponent(item.type)" :key="i" v-bind="item.attrs"> + {{ item.text }} + </component> + </template> + </gl-dropdown> + </li> </ol> </nav> </template> diff --git a/app/assets/javascripts/repository/components/table/index.vue b/app/assets/javascripts/repository/components/table/index.vue index 0d9e992e596..610c7e8d99e 100644 --- a/app/assets/javascripts/repository/components/table/index.vue +++ b/app/assets/javascripts/repository/components/table/index.vue @@ -137,6 +137,7 @@ export default { :path="entry.flatPath" :type="entry.type" :url="entry.webUrl" + :submodule-tree-url="entry.treeUrl" :lfs-oid="entry.lfsOid" /> </template> diff --git a/app/assets/javascripts/repository/components/table/row.vue b/app/assets/javascripts/repository/components/table/row.vue index 3e060e9ecb6..6029460d975 100644 --- a/app/assets/javascripts/repository/components/table/row.vue +++ b/app/assets/javascripts/repository/components/table/row.vue @@ -62,6 +62,11 @@ export default { required: false, default: null, }, + submoduleTreeUrl: { + type: String, + required: false, + default: null, + }, }, data() { return { @@ -112,7 +117,7 @@ export default { </component> <gl-badge v-if="lfsOid" variant="default" class="label-lfs ml-1">LFS</gl-badge> <template v-if="isSubmodule"> - @ <gl-link href="#" class="commit-sha">{{ shortSha }}</gl-link> + @ <gl-link :href="submoduleTreeUrl" class="commit-sha">{{ shortSha }}</gl-link> </template> </td> <td class="d-none d-sm-table-cell tree-commit"> diff --git a/app/assets/javascripts/repository/index.js b/app/assets/javascripts/repository/index.js index ea051eaa414..f9727960040 100644 --- a/app/assets/javascripts/repository/index.js +++ b/app/assets/javascripts/repository/index.js @@ -5,6 +5,7 @@ import Breadcrumbs from './components/breadcrumbs.vue'; import LastCommit from './components/last_commit.vue'; import apolloProvider from './graphql'; import { setTitle } from './utils/title'; +import { parseBoolean } from '../lib/utils/common_utils'; export default function setupVueRepositoryList() { const el = document.getElementById('js-tree-list'); @@ -36,19 +37,42 @@ export default function setupVueRepositoryList() { .forEach(elem => elem.classList.toggle('hidden', !isRoot)); }); - // eslint-disable-next-line no-new - new Vue({ - el: document.getElementById('js-repo-breadcrumb'), - router, - apolloProvider, - render(h) { - return h(Breadcrumbs, { - props: { - currentPath: this.$route.params.pathMatch, - }, - }); - }, - }); + const breadcrumbEl = document.getElementById('js-repo-breadcrumb'); + + if (breadcrumbEl) { + const { + canCollaborate, + canEditTree, + newBranchPath, + newTagPath, + newBlobPath, + forkNewBlobPath, + forkNewDirectoryPath, + forkUploadBlobPath, + } = breadcrumbEl.dataset; + + // eslint-disable-next-line no-new + new Vue({ + el: breadcrumbEl, + router, + apolloProvider, + render(h) { + return h(Breadcrumbs, { + props: { + currentPath: this.$route.params.pathMatch, + canCollaborate: parseBoolean(canCollaborate), + canEditTree: parseBoolean(canEditTree), + newBranchPath, + newTagPath, + newBlobPath, + forkNewBlobPath, + forkNewDirectoryPath, + forkUploadBlobPath, + }, + }); + }, + }); + } // eslint-disable-next-line no-new new Vue({ diff --git a/app/assets/javascripts/repository/queries/getFiles.query.graphql b/app/assets/javascripts/repository/queries/getFiles.query.graphql index 4c24fc4087f..b3cc0878cad 100644 --- a/app/assets/javascripts/repository/queries/getFiles.query.graphql +++ b/app/assets/javascripts/repository/queries/getFiles.query.graphql @@ -35,6 +35,8 @@ query getFiles( edges { node { ...TreeEntry + webUrl + treeUrl } } pageInfo { diff --git a/app/assets/javascripts/repository/queries/getPermissions.query.graphql b/app/assets/javascripts/repository/queries/getPermissions.query.graphql new file mode 100644 index 00000000000..092fa44e2d0 --- /dev/null +++ b/app/assets/javascripts/repository/queries/getPermissions.query.graphql @@ -0,0 +1,9 @@ +query getPermissions($projectPath: ID!) { + project(fullPath: $projectPath) { + userPermissions { + pushCode + forkProject + createMergeRequestIn + } + } +} diff --git a/app/assets/javascripts/vue_merge_request_widget/components/mr_widget_icon.vue b/app/assets/javascripts/vue_merge_request_widget/components/mr_widget_icon.vue index 4b57693e8f1..57d4d8b7ae6 100644 --- a/app/assets/javascripts/vue_merge_request_widget/components/mr_widget_icon.vue +++ b/app/assets/javascripts/vue_merge_request_widget/components/mr_widget_icon.vue @@ -14,6 +14,6 @@ export default { <template> <div class="circle-icon-container append-right-default align-self-start align-self-lg-center"> - <icon :name="name" /> + <icon :name="name" :size="24" /> </div> </template> diff --git a/app/assets/javascripts/vue_merge_request_widget/components/mr_widget_pipeline.vue b/app/assets/javascripts/vue_merge_request_widget/components/mr_widget_pipeline.vue index f5fa68308bc..40c095aa954 100644 --- a/app/assets/javascripts/vue_merge_request_widget/components/mr_widget_pipeline.vue +++ b/app/assets/javascripts/vue_merge_request_widget/components/mr_widget_pipeline.vue @@ -96,16 +96,14 @@ export default { <template> <div class="ci-widget media js-ci-widget"> <template v-if="!hasPipeline || hasCIError"> - <div - class="add-border ci-status-icon ci-status-icon-failed ci-error js-ci-error append-right-default" - > - <icon :size="32" name="status_failed_borderless" /> + <div class="add-border ci-status-icon ci-status-icon-failed ci-error js-ci-error"> + <icon :size="24" name="status_failed_borderless" /> </div> - <div class="media-body" v-html="errorText"></div> + <div class="media-body prepend-left-default" v-html="errorText"></div> </template> <template v-else-if="hasPipeline"> <a :href="status.details_path" class="align-self-start append-right-default"> - <ci-icon :status="status" :size="32" :borderless="true" class="add-border" /> + <ci-icon :status="status" :size="24" :borderless="true" class="add-border" /> </a> <div class="ci-widget-container d-flex"> <div class="ci-widget-content"> diff --git a/app/assets/javascripts/vue_merge_request_widget/components/mr_widget_status_icon.vue b/app/assets/javascripts/vue_merge_request_widget/components/mr_widget_status_icon.vue index 392eb6fb425..8dbd9e52cfe 100644 --- a/app/assets/javascripts/vue_merge_request_widget/components/mr_widget_status_icon.vue +++ b/app/assets/javascripts/vue_merge_request_widget/components/mr_widget_status_icon.vue @@ -32,8 +32,8 @@ export default { }; </script> <template> - <div class="space-children d-flex append-right-10 widget-status-icon"> - <div v-if="isLoading" class="mr-widget-icon"><gl-loading-icon size="md" /></div> + <div class="d-flex widget-status-icon"> + <div v-if="isLoading" class="mr-widget-icon"><gl-loading-icon size="sm" /></div> <ci-icon v-else :status="statusObj" :size="24" /> diff --git a/app/assets/javascripts/vue_merge_request_widget/components/states/commits_header.vue b/app/assets/javascripts/vue_merge_request_widget/components/states/commits_header.vue index 0312b147b62..01524f4b650 100644 --- a/app/assets/javascripts/vue_merge_request_widget/components/states/commits_header.vue +++ b/app/assets/javascripts/vue_merge_request_widget/components/states/commits_header.vue @@ -83,7 +83,7 @@ export default { <gl-button :aria-label="ariaLabel" variant="blank" - class="commit-edit-toggle square s24 mr-2" + class="commit-edit-toggle square s24 append-right-default" @click.stop="toggle()" > <icon :name="collapseIcon" :size="16" /> diff --git a/app/assets/javascripts/vue_merge_request_widget/components/states/nothing_to_merge.vue b/app/assets/javascripts/vue_merge_request_widget/components/states/nothing_to_merge.vue index 7312b31c01c..4d7d49398eb 100644 --- a/app/assets/javascripts/vue_merge_request_widget/components/states/nothing_to_merge.vue +++ b/app/assets/javascripts/vue_merge_request_widget/components/states/nothing_to_merge.vue @@ -18,7 +18,9 @@ export default { <template> <div class="mr-widget-body mr-widget-empty-state"> <div class="row"> - <div class="artwork col-md-5 order-md-last col-12 text-center"> + <div + class="artwork col-md-5 order-md-last col-12 text-center d-flex justify-content-center align-items-center" + > <span v-html="emptyStateSVG"></span> </div> <div class="text col-md-7 order-md-first col-12"> diff --git a/app/assets/stylesheets/framework/animations.scss b/app/assets/stylesheets/framework/animations.scss index 6bc5632365f..6f5a2e561af 100644 --- a/app/assets/stylesheets/framework/animations.scss +++ b/app/assets/stylesheets/framework/animations.scss @@ -104,7 +104,7 @@ } .btn { - @include transition(border-color); + @include transition(background-color, border-color, color, box-shadow); } .dropdown-menu-toggle, diff --git a/app/assets/stylesheets/framework/buttons.scss b/app/assets/stylesheets/framework/buttons.scss index e0b6da31261..767832e242c 100644 --- a/app/assets/stylesheets/framework/buttons.scss +++ b/app/assets/stylesheets/framework/buttons.scss @@ -24,11 +24,12 @@ border-radius: $border-radius-default; font-size: $gl-font-size; font-weight: $gl-font-weight-normal; - padding: $gl-bordered-btn-vert-padding $gl-bordered-btn-horz-padding; + padding: $gl-vert-padding $gl-btn-padding; &:focus, &:active { background-color: $btn-active-gray; + box-shadow: $gl-btn-active-background; } } @@ -49,89 +50,77 @@ color: $text; } - &:not(:disabled):not(.disabled) { - &:hover { - box-shadow: inset 0 0 0 1px $hover-border, 0 2px 2px 0 $gl-btn-hover-shadow-light; - } + &:hover, + &:focus { + background-color: $hover-background; + border-color: $hover-border; + color: $hover-text; - &:focus { - box-shadow: inset 0 0 0 1px $hover-border, 0 0 4px 1px $blue-300; + > .icon { + color: $hover-text; } + } - &:hover, - &:focus { - background-color: $hover-background; - border-color: $hover-border; - color: $hover-text; + &:focus { + box-shadow: 0 0 4px 1px $blue-300; + } - > .icon { - color: $hover-text; - } - } + &:active { + background-color: $active-background; + border-color: $active-border; + box-shadow: inset 0 2px 4px 0 rgba($black, 0.2); + color: $active-text; - &:active, - &:active:focus { - background-color: $active-background; - border-color: $active-border; - box-shadow: inset 0 0 0 1px $hover-border, inset 0 2px 4px 0 rgba($black, 0.2); + > .icon { color: $active-text; + } - > .icon { - color: $active-text; - } + &:focus { + box-shadow: inset 0 2px 4px 0 rgba($black, 0.2); } } } -@mixin btn-color($light, $border-light, $normal, $border-normal, $dark, $border-dark, $color, $hover-shadow-color: $gl-btn-hover-shadow-dark) { +@mixin btn-color($light, $border-light, $normal, $border-normal, $dark, $border-dark, $color) { background-color: $light; border-color: $border-light; color: $color; - &:not(:disabled):not(.disabled) { - &:hover { - box-shadow: inset 0 0 0 1px $border-normal, 0 2px 2px 0 $hover-shadow-color; - } - - &:focus { - box-shadow: inset 0 0 0 1px $border-normal, 0 0 4px 1px $blue-300; - } + &:hover, + &:focus { + background-color: $normal; + border-color: $border-normal; + color: $color; + } - &:hover, - &:focus { - background-color: $normal; - border-color: $border-normal; - color: $color; - } + &:active, + &.active { + box-shadow: $gl-btn-active-background; - &:active, - &.active { - box-shadow: inset 0 2px 4px 0 $gl-btn-hover-shadow-dark; - background-color: $dark; - border-color: $border-dark; - color: $color; - } + background-color: $dark; + border-color: $border-dark; + color: $color; } } @mixin btn-green { - @include btn-color($green-500, $green-600, $green-500, $green-700, $green-600, $green-800, $white-light); + @include btn-color($green-500, $green-600, $green-600, $green-700, $green-700, $green-800, $white-light); } @mixin btn-blue { - @include btn-color($blue-500, $blue-600, $blue-500, $blue-700, $blue-600, $blue-800, $white-light); + @include btn-color($blue-500, $blue-600, $blue-600, $blue-700, $blue-700, $blue-800, $white-light); } @mixin btn-orange { - @include btn-color($orange-500, $orange-600, $orange-500, $orange-700, $orange-600, $orange-800, $white-light); + @include btn-color($orange-500, $orange-600, $orange-600, $orange-700, $orange-700, $orange-800, $white-light); } @mixin btn-red { - @include btn-color($red-500, $red-600, $red-500, $red-700, $red-600, $red-800, $white-light); + @include btn-color($red-500, $red-600, $red-600, $red-700, $red-700, $red-800, $white-light); } @mixin btn-white { - @include btn-color($white-light, $gray-400, $gray-200, $gray-400, $gl-gray-200, $gray-500, $gl-text-color, $gl-btn-hover-shadow-light); + @include btn-color($white-light, $border-color, $white-normal, $border-white-normal, $white-dark, $border-gray-dark, $gl-text-color); } @mixin btn-with-margin { @@ -160,20 +149,21 @@ color: $gl-text-color; white-space: nowrap; - line-height: $gl-btn-line-height; &:focus:active { outline: 0; } - &.btn-xs { - font-size: $gl-btn-xs-font-size; - line-height: $gl-btn-xs-line-height; + &.btn-sm { + padding: 4px 10px; + font-size: $gl-btn-small-font-size; + line-height: $gl-btn-small-line-height; } - &.btn-sm, &.btn-xs { - padding: 3px $gl-bordered-btn-vert-padding; + padding: 2px $gl-btn-padding; + font-size: $gl-btn-xs-font-size; + line-height: $gl-btn-xs-line-height; } &.btn-success, @@ -249,7 +239,7 @@ &.dropdown-toggle { .fa-caret-down { - margin: 0; + margin-left: 3px; } } @@ -282,7 +272,10 @@ } svg { - @include btn-svg; + height: 15px; + width: 15px; + position: relative; + top: 2px; } svg, @@ -337,12 +330,6 @@ &.btn-grouped { @include btn-with-margin; } - - .btn { - border-radius: $border-radius-default; - font-size: $gl-font-size; - line-height: $gl-btn-line-height; - } } .btn-clipboard { @@ -500,25 +487,18 @@ &:active, &:focus { color: $gl-text-color-secondary; - border: 1px solid $border-gray-normal-dashed; background-color: $white-normal; } } -.btn-svg { - padding: $gl-bordered-btn-vert-padding; - - svg { - @include btn-svg; - display: block; - } +.btn-svg svg { + @include btn-svg; } // All disabled buttons, regardless of color, type, etc %disabled { background-color: $gray-light !important; border-color: $gray-200 !important; - box-shadow: none; color: $gl-text-color-disabled !important; opacity: 1 !important; cursor: default !important; diff --git a/app/assets/stylesheets/framework/dropdowns.scss b/app/assets/stylesheets/framework/dropdowns.scss index 05afcecca05..29f63e9578d 100644 --- a/app/assets/stylesheets/framework/dropdowns.scss +++ b/app/assets/stylesheets/framework/dropdowns.scss @@ -8,6 +8,12 @@ } } +@mixin chevron-active { + .fa-chevron-down { + color: $gray-darkest; + } +} + @mixin set-visible { transform: translateY(0); display: block; @@ -43,6 +49,7 @@ .dropdown-toggle, .dropdown-menu-toggle { + @include chevron-active; border-color: $gray-darkest; } @@ -58,12 +65,12 @@ .dropdown-toggle, .confidential-merge-request-fork-group .dropdown-toggle { - padding: $gl-bordered-btn-vert-padding $gl-bordered-btn-horz-padding; + padding: 6px 8px 6px 10px; background-color: $white-light; color: $gl-text-color; font-size: 14px; - line-height: $gl-btn-line-height; text-align: left; + border: 1px solid $border-color; border-radius: $border-radius-base; white-space: nowrap; @@ -96,6 +103,10 @@ padding-right: 25px; } + .fa { + color: $gray-darkest; + } + .fa-chevron-down { font-size: $dropdown-chevron-size; position: relative; @@ -104,10 +115,12 @@ } &:hover { + @include chevron-active; border-color: $gray-darkest; } &:focus:active { + @include chevron-active; border-color: $dropdown-toggle-active-border-color; outline: 0; } diff --git a/app/assets/stylesheets/framework/icons.scss b/app/assets/stylesheets/framework/icons.scss index 1be5ef276fd..7332c4981d2 100644 --- a/app/assets/stylesheets/framework/icons.scss +++ b/app/assets/stylesheets/framework/icons.scss @@ -88,8 +88,5 @@ display: flex; align-items: center; justify-content: center; - border: $border-size solid $gray-400; - border-radius: 50%; - padding: $gl-padding-8 - $border-size; color: $gray-700; } diff --git a/app/assets/stylesheets/framework/panels.scss b/app/assets/stylesheets/framework/panels.scss index cd3d6f8297e..d9c93fed1c4 100644 --- a/app/assets/stylesheets/framework/panels.scss +++ b/app/assets/stylesheets/framework/panels.scss @@ -3,7 +3,6 @@ } .card-slim { - @extend .card; margin-bottom: $gl-vert-padding; } diff --git a/app/assets/stylesheets/framework/variables.scss b/app/assets/stylesheets/framework/variables.scss index 047a9799c3f..c108f45622f 100644 --- a/app/assets/stylesheets/framework/variables.scss +++ b/app/assets/stylesheets/framework/variables.scss @@ -405,8 +405,6 @@ $tanuki-yellow: #fca326; */ $green-500-focus: rgba($green-500, 0.4); $gl-btn-active-background: rgba(0, 0, 0, 0.16); -$gl-btn-hover-shadow-dark: rgba($black, 0.2); -$gl-btn-hover-shadow-light: rgba($black, 0.1); $gl-btn-active-gradient: inset 0 2px 3px $gl-btn-active-background; /* @@ -483,8 +481,6 @@ $gl-btn-padding: 10px; $gl-btn-line-height: 16px; $gl-btn-vert-padding: 8px; $gl-btn-horz-padding: 12px; -$gl-bordered-btn-vert-padding: $gl-btn-vert-padding - 1px; -$gl-bordered-btn-horz-padding: $gl-btn-horz-padding - 1px; $gl-btn-small-font-size: 13px; $gl-btn-small-line-height: 18px; $gl-btn-xs-font-size: 13px; diff --git a/app/assets/stylesheets/pages/commits.scss b/app/assets/stylesheets/pages/commits.scss index ffc6e433988..0b0a4e50146 100644 --- a/app/assets/stylesheets/pages/commits.scss +++ b/app/assets/stylesheets/pages/commits.scss @@ -167,7 +167,7 @@ min-width: 0; .project-namespace { - color: $gl-text-color-secondary; + color: $gl-text-color-tertiary; } } @@ -214,10 +214,10 @@ .label, .btn { - padding: $gl-bordered-btn-vert-padding $gl-bordered-btn-horz-padding; + padding: $gl-vert-padding $gl-btn-padding; border: 1px $border-color solid; font-size: $gl-font-size; - line-height: $gl-btn-line-height; + line-height: $line-height-base; border-radius: 0; display: flex; align-items: center; diff --git a/app/assets/stylesheets/pages/container_registry.scss b/app/assets/stylesheets/pages/container_registry.scss index cca5214a508..a21fa29f34a 100644 --- a/app/assets/stylesheets/pages/container_registry.scss +++ b/app/assets/stylesheets/pages/container_registry.scss @@ -6,6 +6,10 @@ pre { white-space: pre-line; } + + span .btn { + margin: 0; + } } .container-image { diff --git a/app/assets/stylesheets/pages/issuable.scss b/app/assets/stylesheets/pages/issuable.scss index 66ea70e79da..6a0127eb51c 100644 --- a/app/assets/stylesheets/pages/issuable.scss +++ b/app/assets/stylesheets/pages/issuable.scss @@ -929,6 +929,10 @@ margin: 0; } } + + .dropdown-toggle > .icon { + margin: 0 3px; + } } .right-sidebar-collapsed { diff --git a/app/assets/stylesheets/pages/issues.scss b/app/assets/stylesheets/pages/issues.scss index e51ca44476c..8359a60ec9f 100644 --- a/app/assets/stylesheets/pages/issues.scss +++ b/app/assets/stylesheets/pages/issues.scss @@ -267,6 +267,7 @@ ul.related-merge-requests > li { .fa-caret-down { pointer-events: none; color: inherit; + margin-left: 0; } } } diff --git a/app/assets/stylesheets/pages/labels.scss b/app/assets/stylesheets/pages/labels.scss index 11e8a32389f..7d5e185834b 100644 --- a/app/assets/stylesheets/pages/labels.scss +++ b/app/assets/stylesheets/pages/labels.scss @@ -30,6 +30,10 @@ .dropdown-content { max-height: 135px; } + + .dropdown-label-box { + flex: 0 0 auto; + } } .dropdown-new-label { diff --git a/app/assets/stylesheets/pages/merge_requests.scss b/app/assets/stylesheets/pages/merge_requests.scss index 3917937f4af..2780afa11fa 100644 --- a/app/assets/stylesheets/pages/merge_requests.scss +++ b/app/assets/stylesheets/pages/merge_requests.scss @@ -10,8 +10,8 @@ float: left; } - > *:not(:last-child) { - margin-right: 10px; + > *:not(:first-child) { + margin-left: 10px; } } @@ -69,7 +69,7 @@ content: ''; border-left: 1px solid $gray-200; position: absolute; - left: 32px; + left: 28px; top: -17px; height: 16px; } @@ -114,7 +114,7 @@ padding: $gl-padding; @include media-breakpoint-up(md) { - padding-left: $gl-padding-50; + padding-left: $gl-padding-8 * 7; } } } @@ -264,6 +264,10 @@ .widget-status-icon { align-self: flex-start; + + button { + margin-left: $gl-padding; + } } .mr-widget-body { @@ -271,8 +275,8 @@ @include clearfix; - &.media > *:first-child { - margin-right: 10px; + button { + margin-left: $gl-padding; } .approve-btn { @@ -312,6 +316,7 @@ .bold { font-weight: $gl-font-weight-bold; color: $gl-gray-light; + margin-left: 10px; } .state-label { @@ -377,9 +382,13 @@ &.mr-widget-empty-state { line-height: 20px; + padding: $gl-padding; .artwork { - margin-bottom: $gl-padding; + + @include media-breakpoint-down(md) { + margin-bottom: $gl-padding; + } } .text { @@ -395,7 +404,7 @@ } .mr-widget-help { - padding: 10px 16px 10px $gl-padding-50; + padding: 10px 16px 10px ($gl-padding-8 * 7); font-style: italic; } @@ -913,7 +922,7 @@ .media-body { min-width: 0; font-size: 12px; - margin-left: 48px; + margin-left: 40px; } &:not(:last-child) { diff --git a/app/assets/stylesheets/pages/note_form.scss b/app/assets/stylesheets/pages/note_form.scss index 1d57a4a4784..c6bac33e888 100644 --- a/app/assets/stylesheets/pages/note_form.scss +++ b/app/assets/stylesheets/pages/note_form.scss @@ -417,6 +417,7 @@ table { i { color: $white-light; + padding-right: 2px; margin-top: 2px; } diff --git a/app/assets/stylesheets/pages/pipelines.scss b/app/assets/stylesheets/pages/pipelines.scss index aa6bbc8e473..ff4fa8aacdc 100644 --- a/app/assets/stylesheets/pages/pipelines.scss +++ b/app/assets/stylesheets/pages/pipelines.scss @@ -588,8 +588,8 @@ } .ci-status-icon svg { - height: 20px; - width: 20px; + height: 24px; + width: 24px; } .dropdown-menu-toggle { diff --git a/app/assets/stylesheets/pages/projects.scss b/app/assets/stylesheets/pages/projects.scss index 09a576c11cb..c80beceae52 100644 --- a/app/assets/stylesheets/pages/projects.scss +++ b/app/assets/stylesheets/pages/projects.scss @@ -303,7 +303,7 @@ .count-badge-count, .count-badge-button { - border: 1px solid $gray-400; + border: 1px solid $border-color; line-height: 1; } @@ -429,7 +429,7 @@ padding: 0; background: transparent; border: 0; - line-height: 2; + line-height: 34px; margin: 0; > li + li::before { @@ -792,6 +792,7 @@ .btn { margin-top: $gl-padding; + padding: $gl-btn-vert-padding $gl-btn-padding; line-height: $gl-btn-line-height; .icon { diff --git a/app/assets/stylesheets/pages/reports.scss b/app/assets/stylesheets/pages/reports.scss index 94da72622af..85e9f303dde 100644 --- a/app/assets/stylesheets/pages/reports.scss +++ b/app/assets/stylesheets/pages/reports.scss @@ -57,7 +57,7 @@ .report-block-container { border-top: 1px solid $border-color; - padding: $gl-padding-top; + padding: $gl-padding - 2; background-color: $gray-light; // Clean MR widget CSS @@ -96,17 +96,14 @@ .ci-status-icon { svg { - width: 16px; - height: 16px; - left: -2px; + width: 24px; + height: 24px; } } } .report-block-list-issue { display: flex; - align-items: flex-start; - align-content: flex-start; } .is-dismissed .report-block-list-issue-description, diff --git a/app/assets/stylesheets/pages/tree.scss b/app/assets/stylesheets/pages/tree.scss index 5c732ab0d1f..5664f46484e 100644 --- a/app/assets/stylesheets/pages/tree.scss +++ b/app/assets/stylesheets/pages/tree.scss @@ -90,7 +90,7 @@ .add-to-tree { vertical-align: top; - padding: $gl-bordered-btn-vert-padding; + padding: 8px; svg { top: 0; diff --git a/app/controllers/boards/issues_controller.rb b/app/controllers/boards/issues_controller.rb index 353a9806fd1..90528f75ffd 100644 --- a/app/controllers/boards/issues_controller.rb +++ b/app/controllers/boards/issues_controller.rb @@ -58,11 +58,8 @@ module Boards service = Boards::Issues::MoveService.new(board_parent, current_user, move_params(true)) issues = Issue.find(params[:ids]) - if service.execute_multiple(issues) - head :ok - else - head :unprocessable_entity - end + + render json: service.execute_multiple(issues) end def update diff --git a/app/controllers/projects/environments_controller.rb b/app/controllers/projects/environments_controller.rb index 1bca52106fa..ccd54b369fa 100644 --- a/app/controllers/projects/environments_controller.rb +++ b/app/controllers/projects/environments_controller.rb @@ -160,20 +160,22 @@ class Projects::EnvironmentsController < Projects::ApplicationController end def metrics_dashboard - return render_403 unless Feature.enabled?(:environment_metrics_use_prometheus_endpoint, project) - - if Feature.enabled?(:environment_metrics_show_multiple_dashboards, project) + if Feature.enabled?(:gfm_embedded_metrics, project) && params[:embedded] result = dashboard_finder.find( project, current_user, environment, - dashboard_path: params[:dashboard], embedded: params[:embedded] ) + elsif Feature.enabled?(:environment_metrics_show_multiple_dashboards, project) + result = dashboard_finder.find( + project, + current_user, + environment, + dashboard_path: params[:dashboard] + ) - unless params[:embedded] - result[:all_dashboards] = dashboard_finder.find_all_paths(project) - end + result[:all_dashboards] = dashboard_finder.find_all_paths(project) else result = dashboard_finder.find(project, current_user, environment) end diff --git a/app/graphql/types/tree/submodule_type.rb b/app/graphql/types/tree/submodule_type.rb index 8cb1e04f5ba..2b47e5c0161 100644 --- a/app/graphql/types/tree/submodule_type.rb +++ b/app/graphql/types/tree/submodule_type.rb @@ -7,6 +7,9 @@ module Types implements Types::Tree::EntryType graphql_name 'Submodule' + + field :web_url, type: GraphQL::STRING_TYPE, null: true + field :tree_url, type: GraphQL::STRING_TYPE, null: true end # rubocop: enable Graphql/AuthorizeTypes end diff --git a/app/graphql/types/tree/tree_type.rb b/app/graphql/types/tree/tree_type.rb index fbdc1597461..99f2a6c0235 100644 --- a/app/graphql/types/tree/tree_type.rb +++ b/app/graphql/types/tree/tree_type.rb @@ -15,7 +15,9 @@ module Types Gitlab::Graphql::Representation::TreeEntry.decorate(obj.trees, obj.repository) end - field :submodules, Types::Tree::SubmoduleType.connection_type, null: false, calls_gitaly: true + field :submodules, Types::Tree::SubmoduleType.connection_type, null: false, calls_gitaly: true, resolve: -> (obj, args, ctx) do + Gitlab::Graphql::Representation::SubmoduleTreeEntry.decorate(obj.submodules, obj) + end field :blobs, Types::Tree::BlobType.connection_type, null: false, calls_gitaly: true, resolve: -> (obj, args, ctx) do Gitlab::Graphql::Representation::TreeEntry.decorate(obj.blobs, obj.repository) diff --git a/app/helpers/dropdowns_helper.rb b/app/helpers/dropdowns_helper.rb index fd94f07cc2c..64c5fae7d96 100644 --- a/app/helpers/dropdowns_helper.rb +++ b/app/helpers/dropdowns_helper.rb @@ -46,7 +46,7 @@ module DropdownsHelper def dropdown_toggle(toggle_text, data_attr, options = {}) default_label = data_attr[:default_label] - content_tag(:button, disabled: options[:disabled], class: "dropdown-menu-toggle btn #{options[:toggle_class] if options.key?(:toggle_class)}", id: (options[:id] if options.key?(:id)), type: "button", data: data_attr) do + content_tag(:button, disabled: options[:disabled], class: "dropdown-menu-toggle #{options[:toggle_class] if options.key?(:toggle_class)}", id: (options[:id] if options.key?(:id)), type: "button", data: data_attr) do output = content_tag(:span, toggle_text, class: "dropdown-toggle-text #{'is-default' if toggle_text == default_label}") output << icon('chevron-down') output.html_safe diff --git a/app/helpers/issuables_helper.rb b/app/helpers/issuables_helper.rb index 67685ba4e1d..e2e007eee50 100644 --- a/app/helpers/issuables_helper.rb +++ b/app/helpers/issuables_helper.rb @@ -282,6 +282,10 @@ module IssuablesHelper data[:hasClosingMergeRequest] = issuable.merge_requests_count(current_user) != 0 if issuable.is_a?(Issue) + zoom_links = Gitlab::ZoomLinkExtractor.new(issuable.description).links + + data[:zoomMeetingUrl] = zoom_links.last if zoom_links.any? + if parent.is_a?(Group) data[:groupPath] = parent.path else diff --git a/app/helpers/submodule_helper.rb b/app/helpers/submodule_helper.rb index 164c69ca50b..35e04b0ced3 100644 --- a/app/helpers/submodule_helper.rb +++ b/app/helpers/submodule_helper.rb @@ -9,6 +9,10 @@ module SubmoduleHelper def submodule_links(submodule_item, ref = nil, repository = @repository) url = repository.submodule_url_for(ref, submodule_item.path) + submodule_links_for_url(submodule_item.id, url, repository) + end + + def submodule_links_for_url(submodule_item_id, url, repository) if url == '.' || url == './' url = File.join(Gitlab.config.gitlab.url, repository.project.full_path) end @@ -31,13 +35,13 @@ module SubmoduleHelper if self_url?(url, namespace, project) [namespace_project_path(namespace, project), - namespace_project_tree_path(namespace, project, submodule_item.id)] + namespace_project_tree_path(namespace, project, submodule_item_id)] elsif relative_self_url?(url) - relative_self_links(url, submodule_item.id, repository.project) + relative_self_links(url, submodule_item_id, repository.project) elsif github_dot_com_url?(url) - standard_links('github.com', namespace, project, submodule_item.id) + standard_links('github.com', namespace, project, submodule_item_id) elsif gitlab_dot_com_url?(url) - standard_links('gitlab.com', namespace, project, submodule_item.id) + standard_links('gitlab.com', namespace, project, submodule_item_id) else [sanitize_submodule_url(url), nil] end diff --git a/app/helpers/tree_helper.rb b/app/helpers/tree_helper.rb index a3575462de0..bb1cdcb1b31 100644 --- a/app/helpers/tree_helper.rb +++ b/app/helpers/tree_helper.rb @@ -154,4 +154,36 @@ module TreeHelper "logs-path" => logs_path } end + + def breadcrumb_data_attributes + attrs = { + can_collaborate: can_collaborate_with_project?(@project).to_s, + new_blob_path: project_new_blob_path(@project, @id), + new_branch_path: new_project_branch_path(@project), + new_tag_path: new_project_tag_path(@project), + can_edit_tree: can_edit_tree?.to_s + } + + if can?(current_user, :fork_project, @project) && can?(current_user, :create_merge_request_in, @project) + continue_param = { + to: project_new_blob_path(@project, @id), + notice: edit_in_new_fork_notice, + notice_now: edit_in_new_fork_notice_now + } + + attrs.merge!( + fork_new_blob_path: project_forks_path(@project, namespace_key: current_user.namespace.id, continue: continue_param), + fork_new_directory_path: project_forks_path(@project, namespace_key: current_user.namespace.id, continue: continue_param.merge({ + to: request.fullpath, + notice: _("%{edit_in_new_fork_notice} Try to create a new directory again.") % { edit_in_new_fork_notice: edit_in_new_fork_notice } + })), + fork_upload_blob_path: project_forks_path(@project, namespace_key: current_user.namespace.id, continue: continue_param.merge({ + to: request.fullpath, + notice: _("%{edit_in_new_fork_notice} Try to upload a file again.") % { edit_in_new_fork_notice: edit_in_new_fork_notice } + })) + ) + end + + attrs + end end diff --git a/app/models/active_session.rb b/app/models/active_session.rb index f355b02c428..345767179eb 100644 --- a/app/models/active_session.rb +++ b/app/models/active_session.rb @@ -3,6 +3,8 @@ class ActiveSession include ActiveModel::Model + SESSION_BATCH_SIZE = 200 + attr_accessor :created_at, :updated_at, :session_id, :ip_address, :browser, :os, :device_name, :device_type, @@ -106,10 +108,12 @@ class ActiveSession Gitlab::Redis::SharedState.with do |redis| session_keys = session_ids.map { |session_id| "#{Gitlab::Redis::SharedState::SESSION_NAMESPACE}:#{session_id}" } - redis.mget(session_keys).compact.map do |raw_session| - # rubocop:disable Security/MarshalLoad - Marshal.load(raw_session) - # rubocop:enable Security/MarshalLoad + session_keys.each_slice(SESSION_BATCH_SIZE).flat_map do |session_keys_batch| + redis.mget(session_keys_batch).compact.map do |raw_session| + # rubocop:disable Security/MarshalLoad + Marshal.load(raw_session) + # rubocop:enable Security/MarshalLoad + end end end end diff --git a/app/models/ci/pipeline_schedule.rb b/app/models/ci/pipeline_schedule.rb index ba8cea0cea9..42d4e86fe8d 100644 --- a/app/models/ci/pipeline_schedule.rb +++ b/app/models/ci/pipeline_schedule.rb @@ -4,11 +4,8 @@ module Ci class PipelineSchedule < ApplicationRecord extend Gitlab::Ci::Model include Importable - include IgnorableColumn include StripAttribute - ignore_column :deleted_at - belongs_to :project belongs_to :owner, class_name: 'User' has_one :last_pipeline, -> { order(id: :desc) }, class_name: 'Ci::Pipeline' diff --git a/app/models/ci/trigger.rb b/app/models/ci/trigger.rb index 8927bb9bc18..8792c5cf98b 100644 --- a/app/models/ci/trigger.rb +++ b/app/models/ci/trigger.rb @@ -3,17 +3,15 @@ module Ci class Trigger < ApplicationRecord extend Gitlab::Ci::Model - include IgnorableColumn include Presentable - ignore_column :deleted_at - belongs_to :project belongs_to :owner, class_name: "User" has_many :trigger_requests validates :token, presence: true, uniqueness: true + validates :owner, presence: true, unless: :supports_legacy_tokens? before_validation :set_default_values @@ -37,8 +35,13 @@ module Ci self.owner_id.blank? end + def supports_legacy_tokens? + Feature.enabled?(:use_legacy_pipeline_triggers, project) + end + def can_access_project? - self.owner_id.blank? || Ability.allowed?(self.owner, :create_build, project) + supports_legacy_tokens? && legacy? || + Ability.allowed?(self.owner, :create_build, project) end end end diff --git a/app/models/clusters/applications/runner.rb b/app/models/clusters/applications/runner.rb index f0256ff4d41..6ae8c3bd7f3 100644 --- a/app/models/clusters/applications/runner.rb +++ b/app/models/clusters/applications/runner.rb @@ -29,13 +29,6 @@ module Clusters content_values.to_yaml end - # Need to investigate if pipelines run by this runner will stop upon the - # executor pod stopping - # I.e.run a pipeline, and uninstall runner while pipeline is running - def allowed_to_uninstall? - false - end - def install_command Gitlab::Kubernetes::Helm::InstallCommand.new( name: name, @@ -47,6 +40,14 @@ module Clusters ) end + def prepare_uninstall + runner&.update!(active: false) + end + + def post_uninstall + runner.destroy! + end + private def ensure_runner diff --git a/app/models/clusters/concerns/application_core.rb b/app/models/clusters/concerns/application_core.rb index 4514498b84b..803a65726d3 100644 --- a/app/models/clusters/concerns/application_core.rb +++ b/app/models/clusters/concerns/application_core.rb @@ -46,6 +46,16 @@ module Clusters command.version = version end end + + def prepare_uninstall + # Override if your application needs any action before + # being uninstalled by Helm + end + + def post_uninstall + # Override if your application needs any action after + # being uninstalled by Helm + end end end end diff --git a/app/models/clusters/concerns/application_status.rb b/app/models/clusters/concerns/application_status.rb index 54a3dda6d75..342d766f723 100644 --- a/app/models/clusters/concerns/application_status.rb +++ b/app/models/clusters/concerns/application_status.rb @@ -59,29 +59,33 @@ module Clusters transition [:scheduled] => :uninstalling end - before_transition any => [:scheduled] do |app_status, _| - app_status.status_reason = nil + before_transition any => [:scheduled] do |application, _| + application.status_reason = nil end - before_transition any => [:errored] do |app_status, transition| + before_transition any => [:errored] do |application, transition| status_reason = transition.args.first - app_status.status_reason = status_reason if status_reason + application.status_reason = status_reason if status_reason end - before_transition any => [:updating] do |app_status, _| - app_status.status_reason = nil + before_transition any => [:updating] do |application, _| + application.status_reason = nil end - before_transition any => [:update_errored, :uninstall_errored] do |app_status, transition| + before_transition any => [:update_errored, :uninstall_errored] do |application, transition| status_reason = transition.args.first - app_status.status_reason = status_reason if status_reason + application.status_reason = status_reason if status_reason end - before_transition any => [:installed, :updated] do |app_status, _| + before_transition any => [:installed, :updated] do |application, _| # When installing any application we are also performing an update # of tiller (see Gitlab::Kubernetes::Helm::ClientCommand) so # therefore we need to reflect that in the database. - app_status.cluster.application_helm.update!(version: Gitlab::Kubernetes::Helm::HELM_VERSION) + application.cluster.application_helm.update!(version: Gitlab::Kubernetes::Helm::HELM_VERSION) + end + + after_transition any => [:uninstalling], :use_transactions => false do |application, _| + application.prepare_uninstall end end end diff --git a/app/models/concerns/deployment_platform.rb b/app/models/concerns/deployment_platform.rb index 8f28c897eb6..e1a8725e728 100644 --- a/app/models/concerns/deployment_platform.rb +++ b/app/models/concerns/deployment_platform.rb @@ -12,19 +12,10 @@ module DeploymentPlatform private def find_deployment_platform(environment) - find_platform_kubernetes(environment) || + find_platform_kubernetes_with_cte(environment) || find_instance_cluster_platform_kubernetes(environment: environment) end - def find_platform_kubernetes(environment) - if Feature.enabled?(:clusters_cte) - find_platform_kubernetes_with_cte(environment) - else - find_cluster_platform_kubernetes(environment: environment) || - find_group_cluster_platform_kubernetes(environment: environment) - end - end - # EE would override this and utilize environment argument def find_platform_kubernetes_with_cte(_environment) Clusters::ClustersHierarchy.new(self).base_and_ancestors @@ -33,18 +24,6 @@ module DeploymentPlatform end # EE would override this and utilize environment argument - def find_cluster_platform_kubernetes(environment: nil) - clusters.enabled.default_environment - .last&.platform_kubernetes - end - - # EE would override this and utilize environment argument - def find_group_cluster_platform_kubernetes(environment: nil) - Clusters::Cluster.enabled.default_environment.ancestor_clusters_for_clusterable(self) - .first&.platform_kubernetes - end - - # EE would override this and utilize environment argument def find_instance_cluster_platform_kubernetes(environment: nil) Clusters::Instance.new.clusters.enabled.default_environment .first&.platform_kubernetes diff --git a/app/models/concerns/stepable.rb b/app/models/concerns/stepable.rb new file mode 100644 index 00000000000..d00a049a004 --- /dev/null +++ b/app/models/concerns/stepable.rb @@ -0,0 +1,35 @@ +# frozen_string_literal: true + +module Stepable + extend ActiveSupport::Concern + + def steps + self.class._all_steps + end + + def execute_steps + initial_result = {} + + steps.inject(initial_result) do |previous_result, callback| + result = method(callback).call + + if result[:status] == :error + result[:failed_step] = callback + + break result + end + + previous_result.merge(result) + end + end + + class_methods do + def _all_steps + @_all_steps ||= [] + end + + def steps(*methods) + _all_steps.concat methods + end + end +end diff --git a/app/models/issue.rb b/app/models/issue.rb index 982a94315bd..12d30389910 100644 --- a/app/models/issue.rb +++ b/app/models/issue.rb @@ -13,11 +13,8 @@ class Issue < ApplicationRecord include RelativePositioning include TimeTrackable include ThrottledTouch - include IgnorableColumn include LabelEventable - ignore_column :assignee_id, :branch_name, :deleted_at - DueDateStruct = Struct.new(:title, :name).freeze NoDueDate = DueDateStruct.new('No Due Date', '0').freeze AnyDueDate = DueDateStruct.new('Any Due Date', '').freeze diff --git a/app/models/merge_request.rb b/app/models/merge_request.rb index ba57fefd8f1..68e6e48fb7d 100644 --- a/app/models/merge_request.rb +++ b/app/models/merge_request.rb @@ -7,7 +7,6 @@ class MergeRequest < ApplicationRecord include Noteable include Referable include Presentable - include IgnorableColumn include TimeTrackable include ManualInverseAssociation include EachBatch @@ -24,10 +23,6 @@ class MergeRequest < ApplicationRecord SORTING_PREFERENCE_FIELD = :merge_requests_sort - ignore_column :locked_at, - :ref_fetched, - :deleted_at - belongs_to :target_project, class_name: "Project" belongs_to :source_project, class_name: "Project" belongs_to :merge_user, class_name: "User" diff --git a/app/models/namespace.rb b/app/models/namespace.rb index b3021fab7f1..b8d7348268a 100644 --- a/app/models/namespace.rb +++ b/app/models/namespace.rb @@ -8,13 +8,10 @@ class Namespace < ApplicationRecord include AfterCommitQueue include Storage::LegacyNamespace include Gitlab::SQL::Pattern - include IgnorableColumn include FeatureGate include FromUnion include Gitlab::Utils::StrongMemoize - ignore_column :deleted_at - # Prevent users from creating unreasonably deep level of nesting. # The number 20 was taken based on maximum nesting level of # Android repo (15) + some extra backup. diff --git a/app/models/project.rb b/app/models/project.rb index f6f7d373f91..2906aca75fc 100644 --- a/app/models/project.rb +++ b/app/models/project.rb @@ -31,7 +31,6 @@ class Project < ApplicationRecord include FeatureGate include OptionallySearch include FromUnion - include IgnorableColumn extend Gitlab::Cache::RequestCache extend Gitlab::ConfigHelper @@ -56,8 +55,6 @@ class Project < ApplicationRecord VALID_MIRROR_PORTS = [22, 80, 443].freeze VALID_MIRROR_PROTOCOLS = %w(http https ssh git).freeze - ignore_column :import_status, :import_jid, :import_error - cache_markdown_field :description, pipeline: :description delegate :feature_available?, :builds_enabled?, :wiki_enabled?, diff --git a/app/models/project_feature.rb b/app/models/project_feature.rb index 7ff06655de0..78e82955342 100644 --- a/app/models/project_feature.rb +++ b/app/models/project_feature.rb @@ -86,6 +86,8 @@ class ProjectFeature < ApplicationRecord default_value_for :wiki_access_level, value: ENABLED, allows_nil: false default_value_for :repository_access_level, value: ENABLED, allows_nil: false + default_value_for(:pages_access_level, allows_nil: false) { |feature| feature.project&.public? ? ENABLED : PRIVATE } + def feature_available?(feature, user) # This feature might not be behind a feature flag at all, so default to true return false unless ::Feature.enabled?(feature, user, default_enabled: true) diff --git a/app/models/project_services/jira_service.rb b/app/models/project_services/jira_service.rb index a3b89b2543a..e571700fd02 100644 --- a/app/models/project_services/jira_service.rb +++ b/app/models/project_services/jira_service.rb @@ -250,7 +250,7 @@ class JiraService < IssueTrackerService end log_info("Successfully posted", client_url: client_url) - "SUCCESS: Successfully posted to http://jira.example.net." + "SUCCESS: Successfully posted to #{client_url}." end end diff --git a/app/policies/ci/trigger_policy.rb b/app/policies/ci/trigger_policy.rb index 209db44539c..578301d7f7e 100644 --- a/app/policies/ci/trigger_policy.rb +++ b/app/policies/ci/trigger_policy.rb @@ -5,7 +5,7 @@ module Ci delegate { @subject.project } with_options scope: :subject, score: 0 - condition(:legacy) { @subject.legacy? } + condition(:legacy) { @subject.supports_legacy_tokens? && @subject.legacy? } with_score 0 condition(:is_owner) { @user && @subject.owner_id == @user.id } diff --git a/app/serializers/diff_file_base_entity.rb b/app/serializers/diff_file_base_entity.rb index d8630165e49..ee68b4b98e0 100644 --- a/app/serializers/diff_file_base_entity.rb +++ b/app/serializers/diff_file_base_entity.rb @@ -3,7 +3,6 @@ class DiffFileBaseEntity < Grape::Entity include RequestAwareEntity include BlobHelper - include SubmoduleHelper include DiffHelper include TreeHelper include ChecksCollaboration @@ -12,12 +11,12 @@ class DiffFileBaseEntity < Grape::Entity expose :content_sha expose :submodule?, as: :submodule - expose :submodule_link do |diff_file| - memoized_submodule_links(diff_file).first + expose :submodule_link do |diff_file, options| + memoized_submodule_links(diff_file, options).first end expose :submodule_tree_url do |diff_file| - memoized_submodule_links(diff_file).last + memoized_submodule_links(diff_file, options).last end expose :edit_path, if: -> (_, options) { options[:merge_request] } do |diff_file| @@ -92,10 +91,10 @@ class DiffFileBaseEntity < Grape::Entity private - def memoized_submodule_links(diff_file) + def memoized_submodule_links(diff_file, options) strong_memoize(:submodule_links) do if diff_file.submodule? - submodule_links(diff_file.blob, diff_file.content_sha, diff_file.repository) + options[:submodule_links].for(diff_file.blob, diff_file.content_sha) else [] end diff --git a/app/serializers/diffs_entity.rb b/app/serializers/diffs_entity.rb index b51e4a7e6d0..1763fe5b6ab 100644 --- a/app/serializers/diffs_entity.rb +++ b/app/serializers/diffs_entity.rb @@ -64,7 +64,10 @@ class DiffsEntity < Grape::Entity merge_request_path(merge_request, format: :diff) end - expose :diff_files, using: DiffFileEntity + expose :diff_files do |diffs, options| + submodule_links = Gitlab::SubmoduleLinks.new(merge_request.project.repository) + DiffFileEntity.represent(diffs.diff_files, options.merge(submodule_links: submodule_links)) + end expose :merge_request_diffs, using: MergeRequestDiffEntity, if: -> (_, options) { options[:merge_request_diffs]&.any? } do |diffs| options[:merge_request_diffs] diff --git a/app/serializers/discussion_serializer.rb b/app/serializers/discussion_serializer.rb index 5be40e74175..8bb7e93c033 100644 --- a/app/serializers/discussion_serializer.rb +++ b/app/serializers/discussion_serializer.rb @@ -2,4 +2,18 @@ class DiscussionSerializer < BaseSerializer entity DiscussionEntity + + def represent(resource, opts = {}, entity_class = nil) + super(resource, with_additional_opts(opts), entity_class) + end + + private + + def with_additional_opts(opts) + additional_opts = { + submodule_links: Gitlab::SubmoduleLinks.new(@request.project.repository) + } + + opts.merge(additional_opts) + end end diff --git a/app/serializers/submodule_entity.rb b/app/serializers/submodule_entity.rb deleted file mode 100644 index e475a4f301f..00000000000 --- a/app/serializers/submodule_entity.rb +++ /dev/null @@ -1,25 +0,0 @@ -# frozen_string_literal: true - -class SubmoduleEntity < Grape::Entity - include RequestAwareEntity - - expose :id, :path, :name, :mode - - expose :icon do |blob| - 'archive' - end - - expose :url do |blob| - submodule_links(blob, request).first - end - - expose :tree_url do |blob| - submodule_links(blob, request).last - end - - private - - def submodule_links(blob, request) - @submodule_links ||= SubmoduleHelper.submodule_links(blob, request.ref, request.repository) - end -end diff --git a/app/services/boards/issues/move_service.rb b/app/services/boards/issues/move_service.rb index 755d723b9a0..00ce27db7c8 100644 --- a/app/services/boards/issues/move_service.rb +++ b/app/services/boards/issues/move_service.rb @@ -11,26 +11,51 @@ module Boards end def execute_multiple(issues) - return false if issues.empty? + return execute_multiple_empty_result if issues.empty? + handled_issues = [] last_inserted_issue_id = nil - issues.map do |issue| + count = issues.each.inject(0) do |moved_count, issue| issue_modification_params = issue_params(issue) - next if issue_modification_params.empty? + next moved_count if issue_modification_params.empty? if last_inserted_issue_id - issue_modification_params[:move_between_ids] = move_between_ids({ move_after_id: nil, move_before_id: last_inserted_issue_id }) + issue_modification_params[:move_between_ids] = move_below(last_inserted_issue_id) end last_inserted_issue_id = issue.id - move_single_issue(issue, issue_modification_params) - end.all? + handled_issue = move_single_issue(issue, issue_modification_params) + handled_issues << present_issue_entity(handled_issue) if handled_issue + handled_issue && handled_issue.valid? ? moved_count + 1 : moved_count + end + + { + count: count, + success: count == issues.size, + issues: handled_issues + } end private + def present_issue_entity(issue) + ::API::Entities::Issue.represent(issue) + end + + def execute_multiple_empty_result + @execute_multiple_empty_result ||= { + count: 0, + success: false, + issues: [] + } + end + + def move_below(id) + move_between_ids({ move_after_id: nil, move_before_id: id }) + end + def move_single_issue(issue, issue_modification_params) - return false unless can?(current_user, :update_issue, issue) + return unless can?(current_user, :update_issue, issue) update(issue, issue_modification_params) end diff --git a/app/services/clusters/applications/check_uninstall_progress_service.rb b/app/services/clusters/applications/check_uninstall_progress_service.rb index 8786d295d6a..e51d84ef052 100644 --- a/app/services/clusters/applications/check_uninstall_progress_service.rb +++ b/app/services/clusters/applications/check_uninstall_progress_service.rb @@ -23,6 +23,7 @@ module Clusters private def on_success + app.post_uninstall app.destroy! rescue StandardError => e app.make_errored!(_('Application uninstalled but failed to destroy: %{error_message}') % { error_message: e.message }) diff --git a/app/services/self_monitoring/project/create_service.rb b/app/services/self_monitoring/project/create_service.rb new file mode 100644 index 00000000000..e5ef8c15456 --- /dev/null +++ b/app/services/self_monitoring/project/create_service.rb @@ -0,0 +1,132 @@ +# frozen_string_literal: true + +module SelfMonitoring + module Project + class CreateService < ::BaseService + include Stepable + + DEFAULT_VISIBILITY_LEVEL = Gitlab::VisibilityLevel::INTERNAL + DEFAULT_NAME = 'GitLab Instance Administration' + DEFAULT_DESCRIPTION = <<~HEREDOC + This project is automatically generated and will be used to help monitor this GitLab instance. + HEREDOC + + steps :validate_admins, + :create_project, + :add_project_members, + :add_prometheus_manual_configuration + + def initialize + super(nil) + end + + def execute + execute_steps + end + + private + + def validate_admins + unless instance_admins.any? + log_error('No active admin user found') + return error('No active admin user found') + end + + success + end + + def create_project + admin_user = project_owner + @project = ::Projects::CreateService.new(admin_user, create_project_params).execute + + if project.persisted? + success(project: project) + else + log_error("Could not create self-monitoring project. Errors: #{project.errors.full_messages}") + error('Could not create project') + end + end + + def add_project_members + members = project.add_users(project_maintainers, Gitlab::Access::MAINTAINER) + errors = members.flat_map { |member| member.errors.full_messages } + + if errors.any? + log_error("Could not add admins as members to self-monitoring project. Errors: #{errors}") + error('Could not add admins as members') + else + success + end + end + + def add_prometheus_manual_configuration + return success unless prometheus_enabled? + return success unless prometheus_listen_address.present? + + # TODO: Currently, adding the internal prometheus server as a manual configuration + # is only possible if the setting to allow webhooks and services to connect + # to local network is on. + # https://gitlab.com/gitlab-org/gitlab-ce/issues/44496 will add + # a whitelist that will allow connections to certain ips on the local network. + + service = project.find_or_initialize_service('prometheus') + + unless service.update(prometheus_service_attributes) + log_error("Could not save prometheus manual configuration for self-monitoring project. Errors: #{service.errors.full_messages}") + return error('Could not save prometheus manual configuration') + end + + success + end + + def prometheus_enabled? + Gitlab.config.prometheus.enable + rescue Settingslogic::MissingSetting + false + end + + def prometheus_listen_address + Gitlab.config.prometheus.listen_address + rescue Settingslogic::MissingSetting + end + + def instance_admins + @instance_admins ||= User.admins.active + end + + def project_owner + instance_admins.first + end + + def project_maintainers + # Exclude the first so that the project_owner is not added again as a member. + instance_admins - [project_owner] + end + + def create_project_params + { + initialize_with_readme: true, + visibility_level: DEFAULT_VISIBILITY_LEVEL, + name: DEFAULT_NAME, + description: DEFAULT_DESCRIPTION + } + end + + def internal_prometheus_listen_address_uri + if prometheus_listen_address.starts_with?('http') + prometheus_listen_address + else + 'http://' + prometheus_listen_address + end + end + + def prometheus_service_attributes + { + api_url: internal_prometheus_listen_address_uri, + manual_configuration: true, + active: true + } + end + end + end +end diff --git a/app/views/devise/shared/_signup_box.html.haml b/app/views/devise/shared/_signup_box.html.haml index 034273558bb..074edf645ba 100644 --- a/app/views/devise/shared/_signup_box.html.haml +++ b/app/views/devise/shared/_signup_box.html.haml @@ -7,26 +7,26 @@ = render "devise/shared/error_messages", resource: resource .name.form-group = f.label :name, _('Full name'), class: 'label-bold' - = f.text_field :name, class: "form-control top qa-new-user-name js-block-emoji js-validate-length", :data => { :max_length => max_name_length, :max_length_message => s_("SignUp|Name is too long (maximum is %{max_length} characters).") % { max_length: max_name_length } }, required: true, title: _("This field is required.") + = f.text_field :name, class: "form-control top js-block-emoji js-validate-length", :data => { :max_length => max_name_length, :max_length_message => s_("SignUp|Name is too long (maximum is %{max_length} characters).") % { max_length: max_name_length }, :qa_selector => 'new_user_name_field' }, required: true, title: _("This field is required.") .username.form-group = f.label :username, class: 'label-bold' - = f.text_field :username, class: "form-control middle qa-new-user-username js-block-emoji js-validate-length js-validate-username", :data => { :max_length => max_username_length, :max_length_message => s_("SignUp|Username is too long (maximum is %{max_length} characters).") % { max_length: max_username_length } }, pattern: Gitlab::PathRegex::NAMESPACE_FORMAT_REGEX_JS, required: true, title: _("Please create a username with only alphanumeric characters.") + = f.text_field :username, class: "form-control middle js-block-emoji js-validate-length js-validate-username", :data => { :max_length => max_username_length, :max_length_message => s_("SignUp|Username is too long (maximum is %{max_length} characters).") % { max_length: max_username_length }, :qa_selector => 'new_user_username_field' }, pattern: Gitlab::PathRegex::NAMESPACE_FORMAT_REGEX_JS, required: true, title: _("Please create a username with only alphanumeric characters.") %p.validation-error.gl-field-error-ignore.field-validation.hide= _('Username is already taken.') %p.validation-success.gl-field-error-ignore.field-validation.hide= _('Username is available.') %p.validation-pending.gl-field-error-ignore.field-validation.hide= _('Checking username availability...') .form-group = f.label :email, class: 'label-bold' - = f.email_field :email, class: "form-control middle qa-new-user-email", required: true, title: _("Please provide a valid email address.") + = f.email_field :email, class: "form-control middle", data: { qa_selector: 'new_user_email_field' }, required: true, title: _("Please provide a valid email address.") .form-group = f.label :email_confirmation, class: 'label-bold' - = f.email_field :email_confirmation, class: "form-control middle qa-new-user-email-confirmation", required: true, title: _("Please retype the email address.") + = f.email_field :email_confirmation, class: "form-control middle", data: { qa_selector: 'new_user_email_confirmation_field' }, required: true, title: _("Please retype the email address.") .form-group.append-bottom-20#password-strength = f.label :password, class: 'label-bold' - = f.password_field :password, class: "form-control bottom qa-new-user-password", required: true, pattern: ".{#{@minimum_password_length},}", title: _("Minimum length is %{minimum_password_length} characters.") % { minimum_password_length: @minimum_password_length } + = f.password_field :password, class: "form-control bottom", data: { qa_selector: 'new_user_password_field' }, required: true, pattern: ".{#{@minimum_password_length},}", title: _("Minimum length is %{minimum_password_length} characters.") % { minimum_password_length: @minimum_password_length } %p.gl-field-hint.text-secondary= _('Minimum length is %{minimum_password_length} characters') % { minimum_password_length: @minimum_password_length } - if Gitlab::CurrentSettings.current_application_settings.enforce_terms? .form-group - = check_box_tag :terms_opt_in, '1', false, required: true, class: 'qa-new-user-accept-terms' + = check_box_tag :terms_opt_in, '1', false, required: true, data: { qa_selector: 'new_user_accept_terms_checkbox' } = label_tag :terms_opt_in do - terms_link = link_to s_("I accept the|Terms of Service and Privacy Policy"), terms_path, target: "_blank" - accept_terms_label = _("I accept the %{terms_link}") % { terms_link: terms_link } @@ -36,4 +36,4 @@ - if show_recaptcha_sign_up? = recaptcha_tags .submit-container - = f.submit _("Register"), class: "btn-register btn qa-new-user-register-button" + = f.submit _("Register"), class: "btn-register btn", data: { qa_selector: 'new_user_register_button' } diff --git a/app/views/devise/shared/_tabs_normal.html.haml b/app/views/devise/shared/_tabs_normal.html.haml index 4cd03be572f..ab8c22532fd 100644 --- a/app/views/devise/shared/_tabs_normal.html.haml +++ b/app/views/devise/shared/_tabs_normal.html.haml @@ -3,4 +3,4 @@ %a.nav-link.qa-sign-in-tab.active{ href: '#login-pane', data: { toggle: 'tab' }, role: 'tab' } Sign in - if allow_signup? %li.nav-item{ role: 'presentation' } - %a.nav-link.qa-register-tab{ href: '#register-pane', data: { track_label: 'sign_in_register', track_property: 'sign_in', track_event: 'click_button', track_value: 'register', toggle: 'tab' }, role: 'tab' } Register + %a.nav-link.qa-register-tab{ href: '#register-pane', data: { track_label: 'sign_in_register', track_property: '', track_event: 'click_button', track_value: '', toggle: 'tab' }, role: 'tab' } Register diff --git a/app/views/doorkeeper/authorizations/new.html.haml b/app/views/doorkeeper/authorizations/new.html.haml index dae9a7acf6b..5d57337a568 100644 --- a/app/views/doorkeeper/authorizations/new.html.haml +++ b/app/views/doorkeeper/authorizations/new.html.haml @@ -46,4 +46,4 @@ = hidden_field_tag :response_type, @pre_auth.response_type = hidden_field_tag :scope, @pre_auth.scope = hidden_field_tag :nonce, @pre_auth.nonce - = submit_tag _("Authorize"), class: "btn btn-success prepend-left-10" + = submit_tag _("Authorize"), class: "btn btn-success prepend-left-10", data: { qa_selector: 'authorization_button' } diff --git a/app/views/layouts/_search.html.haml b/app/views/layouts/_search.html.haml index a5f57f5893c..c62dce880c0 100644 --- a/app/views/layouts/_search.html.haml +++ b/app/views/layouts/_search.html.haml @@ -2,7 +2,7 @@ - group_data_attrs = { group_path: j(@group.path), name: j(@group.name), issues_path: issues_group_path(@group), mr_path: merge_requests_group_path(@group) } - if @project && @project.persisted? - project_data_attrs = { project_path: j(@project.path), name: j(@project.name), issues_path: project_issues_path(@project), mr_path: project_merge_requests_path(@project), issues_disabled: !@project.issues_enabled? } -.search.search-form{ data: { track_label: "navbar_search", track_event: "activate_form_input" } } +.search.search-form{ data: { track_label: "navbar_search", track_event: "activate_form_input", track_value: "" } } = form_tag search_path, method: :get, class: 'form-inline' do |f| .search-input-container .search-input-wrap diff --git a/app/views/layouts/header/_default.html.haml b/app/views/layouts/header/_default.html.haml index fc2dea25c77..89f99472270 100644 --- a/app/views/layouts/header/_default.html.haml +++ b/app/views/layouts/header/_default.html.haml @@ -64,7 +64,7 @@ .dropdown-menu.dropdown-menu-right = render 'layouts/header/help_dropdown' - if header_link?(:user_dropdown) - %li.nav-item.header-user.dropdown{ data: { track_label: "profile_dropdown", track_event: "click_dropdown", qa_selector: 'user_menu' } } + %li.nav-item.header-user.dropdown{ data: { track_label: "profile_dropdown", track_event: "click_dropdown", track_value: "", qa_selector: 'user_menu' } } = link_to current_user, class: user_dropdown_class, data: { toggle: "dropdown" } do = image_tag avatar_icon_for_user(current_user, 23), width: 23, height: 23, class: "header-user-avatar qa-user-avatar" = sprite_icon('angle-down', css_class: 'caret-down') diff --git a/app/views/layouts/header/_new_dropdown.haml b/app/views/layouts/header/_new_dropdown.haml index 1d7a501e5c2..e28efb09be5 100644 --- a/app/views/layouts/header/_new_dropdown.haml +++ b/app/views/layouts/header/_new_dropdown.haml @@ -1,4 +1,4 @@ -%li.header-new.dropdown{ data: { track_label: "new_dropdown", track_event: "click_dropdown" } } +%li.header-new.dropdown{ data: { track_label: "new_dropdown", track_event: "click_dropdown", track_value: "" } } = link_to new_project_path, class: "header-new-dropdown-toggle has-tooltip qa-new-menu-toggle", id: "js-onboarding-new-project-link", title: _("New..."), ref: 'tooltip', aria: { label: _("New...") }, data: { toggle: 'dropdown', placement: 'bottom', container: 'body', display: 'static' } do = sprite_icon('plus-square', size: 16) = sprite_icon('angle-down', css_class: 'caret-down') diff --git a/app/views/layouts/nav/_dashboard.html.haml b/app/views/layouts/nav/_dashboard.html.haml index 54028dc8554..cbe713b7468 100644 --- a/app/views/layouts/nav/_dashboard.html.haml +++ b/app/views/layouts/nav/_dashboard.html.haml @@ -2,7 +2,7 @@ -# https://gitlab.com/gitlab-org/gitlab-ce/issues/49713 for more information. %ul.list-unstyled.navbar-sub-nav - if dashboard_nav_link?(:projects) - = nav_link(path: ['root#index', 'projects#trending', 'projects#starred', 'dashboard/projects#index'], html_options: { id: 'nav-projects-dropdown', class: "home dropdown header-projects qa-projects-dropdown", data: { track_label: "projects_dropdown", track_event: "click_dropdown" } }) do + = nav_link(path: ['root#index', 'projects#trending', 'projects#starred', 'dashboard/projects#index'], html_options: { id: 'nav-projects-dropdown', class: "home dropdown header-projects qa-projects-dropdown", data: { track_label: "projects_dropdown", track_event: "click_dropdown", track_value: "" } }) do %button.btn{ type: 'button', data: { toggle: "dropdown" } } = _('Projects') = sprite_icon('angle-down', css_class: 'caret-down') @@ -10,7 +10,7 @@ = render "layouts/nav/projects_dropdown/show" - if dashboard_nav_link?(:groups) - = nav_link(controller: ['dashboard/groups', 'explore/groups'], html_options: { id: 'nav-groups-dropdown', class: "home dropdown header-groups qa-groups-dropdown", data: { track_label: "groups_dropdown", track_event: "click_dropdown" } }) do + = nav_link(controller: ['dashboard/groups', 'explore/groups'], html_options: { id: 'nav-groups-dropdown', class: "home dropdown header-groups qa-groups-dropdown", data: { track_label: "groups_dropdown", track_event: "click_dropdown", track_value: "" } }) do %button.btn{ type: 'button', data: { toggle: "dropdown" } } = _('Groups') = sprite_icon('angle-down', css_class: 'caret-down') diff --git a/app/views/projects/_files.html.haml b/app/views/projects/_files.html.haml index 6763513f9ae..95fdad125a7 100644 --- a/app/views/projects/_files.html.haml +++ b/app/views/projects/_files.html.haml @@ -20,6 +20,9 @@ - if vue_file_list_enabled? #js-tree-list{ data: { project_path: @project.full_path, project_short_path: @project.path, ref: ref, full_name: @project.name_with_namespace } } + - if can_edit_tree? + = render 'projects/blob/upload', title: _('Upload New File'), placeholder: _('Upload New File'), button_title: _('Upload file'), form_path: project_create_blob_path(@project, @id), method: :post + = render 'projects/blob/new_dir' - if @tree.readme = render "projects/tree/readme", readme: @tree.readme - else diff --git a/app/views/projects/_new_project_fields.html.haml b/app/views/projects/_new_project_fields.html.haml index 7541737f79c..5d88be0925e 100644 --- a/app/views/projects/_new_project_fields.html.haml +++ b/app/views/projects/_new_project_fields.html.haml @@ -54,7 +54,7 @@ .form-group.row.initialize-with-readme-setting %div{ :class => "col-sm-12" } .form-check - = check_box_tag 'project[initialize_with_readme]', '1', false, class: 'form-check-input qa-initialize-with-readme-checkbox', data: { track_label: "#{track_label}", track_event: "activate_form_input", track_property: "init_with_readme" } + = check_box_tag 'project[initialize_with_readme]', '1', false, class: 'form-check-input qa-initialize-with-readme-checkbox', data: { track_label: "#{track_label}", track_event: "activate_form_input", track_property: "init_with_readme", track_value: "" } = label_tag 'project[initialize_with_readme]', class: 'form-check-label' do .option-title %strong= s_('ProjectsNew|Initialize repository with a README') @@ -62,4 +62,4 @@ = s_('ProjectsNew|Allows you to immediately clone this project’s repository. Skip this if you plan to push up an existing repository.') = f.submit _('Create project'), class: "btn btn-success project-submit", data: { track_label: "#{track_label}", track_event: "click_button", track_property: "create_project", track_value: "" } -= link_to _('Cancel'), dashboard_projects_path, class: 'btn btn-cancel', data: { track_label: "#{track_label}", track_event: "click_button", track_property: "cancel" } += link_to _('Cancel'), dashboard_projects_path, class: 'btn btn-cancel', data: { track_label: "#{track_label}", track_event: "click_button", track_property: "cancel", track_value: "" } diff --git a/app/views/projects/issues/import_csv/_button.html.haml b/app/views/projects/issues/import_csv/_button.html.haml index 8442a53ed61..acc2c50294f 100644 --- a/app/views/projects/issues/import_csv/_button.html.haml +++ b/app/views/projects/issues/import_csv/_button.html.haml @@ -1,6 +1,6 @@ - type = local_assigns.fetch(:type, :icon) -%button.csv-import-button.btn.btn-svg{ title: _('Import CSV'), class: ('has-tooltip' if type == :icon), +%button.csv-import-button.btn{ title: _('Import CSV'), class: ('has-tooltip' if type == :icon), data: { toggle: 'modal', target: '.issues-import-modal' } } - if type == :icon = sprite_icon('upload') diff --git a/app/views/projects/issues/import_csv/_modal.html.haml b/app/views/projects/issues/import_csv/_modal.html.haml index 86bc54786ad..fe4a4236896 100644 --- a/app/views/projects/issues/import_csv/_modal.html.haml +++ b/app/views/projects/issues/import_csv/_modal.html.haml @@ -20,5 +20,5 @@ = _('It must have a header row and at least two columns: the first column is the issue title and the second column is the issue description. The separator is automatically detected.') = _('The maximum file size allowed is %{size}.') % { size: number_to_human_size(Gitlab::CurrentSettings.max_attachment_size.megabytes) } .modal-footer - %button{ type: 'submit', class: 'btn btn-success', title: _('Import issues'), data: { track_label: "export_issues_csv", track_event: "click_button"} } + %button{ type: 'submit', class: 'btn btn-success', title: _('Import issues'), data: { track_label: "export_issues_csv", track_event: "click_button", track_value: ""} } = _('Import issues') diff --git a/app/views/projects/new.html.haml b/app/views/projects/new.html.haml index 33de0aa153b..fabe636b05c 100644 --- a/app/views/projects/new.html.haml +++ b/app/views/projects/new.html.haml @@ -32,15 +32,15 @@ .col-lg-9.js-toggle-container %ul.nav.nav-tabs.nav-links.gitlab-tabs{ role: 'tablist' } %li.nav-item{ role: 'presentation' } - %a.nav-link.active{ href: '#blank-project-pane', id: 'blank-project-tab', data: { toggle: 'tab', track_label: 'blank_project', track_event: "click_tab" }, role: 'tab' } + %a.nav-link.active{ href: '#blank-project-pane', id: 'blank-project-tab', data: { toggle: 'tab', track_label: 'blank_project', track_event: "click_tab", track_value: "" }, role: 'tab' } %span.d-none.d-sm-block= s_('ProjectsNew|Blank project') %span.d-block.d-sm-none= s_('ProjectsNew|Blank') %li.nav-item{ role: 'presentation' } - %a.nav-link{ href: '#create-from-template-pane', id: 'create-from-template-tab', data: { toggle: 'tab', track_label: 'create_from_template', track_event: "click_tab" }, role: 'tab' } + %a.nav-link{ href: '#create-from-template-pane', id: 'create-from-template-tab', data: { toggle: 'tab', track_label: 'create_from_template', track_event: "click_tab", track_value: "" }, role: 'tab' } %span.d-none.d-sm-block.qa-project-create-from-template-tab= s_('ProjectsNew|Create from template') %span.d-block.d-sm-none= s_('ProjectsNew|Template') %li.nav-item{ role: 'presentation' } - %a.nav-link{ href: '#import-project-pane', id: 'import-project-tab', data: { toggle: 'tab', track_label: 'import_project', track_event: "click_tab" }, role: 'tab' } + %a.nav-link{ href: '#import-project-pane', id: 'import-project-tab', data: { toggle: 'tab', track_label: 'import_project', track_event: "click_tab", track_value: "" }, role: 'tab' } %span.d-none.d-sm-block= s_('ProjectsNew|Import project') %span.d-block.d-sm-none= s_('ProjectsNew|Import') = render_if_exists 'projects/new_ci_cd_only_project_tab', active_tab: active_tab @@ -51,7 +51,7 @@ = render 'new_project_fields', f: f, project_name_id: "blank-project-name" #create-from-template-pane.tab-pane.js-toggle-container.px-0.pb-0{ class: active_when(active_tab == 'template'), role: 'tabpanel' } - .card-slim.m-4.p-4 + .card.card-slim.m-4.p-4 %div - contributing_templates_url = 'https://gitlab.com/gitlab-org/project-templates/contributing' - link_start = '<a href="%{url}" target="_blank" rel="noopener noreferrer">'.html_safe % { url: contributing_templates_url } diff --git a/app/views/projects/pages_domains/_form.html.haml b/app/views/projects/pages_domains/_form.html.haml index 0e5c65a2f72..4aa1e574d93 100644 --- a/app/views/projects/pages_domains/_form.html.haml +++ b/app/views/projects/pages_domains/_form.html.haml @@ -33,7 +33,7 @@ = sprite_icon("status_success_borderless", size: 16, css_class: "toggle-icon-svg toggle-status-checked") = sprite_icon("status_failed_borderless", size: 16, css_class: "toggle-icon-svg toggle-status-unchecked") %p.text-secondary.mt-3 - - docs_link_url = help_page_path("user/project/pages/lets_encrypt_for_gitlab_pages.md", anchor: "lets-encrypt-for-gitlab-pages") + - docs_link_url = help_page_path("user/project/pages/custom_domains_ssl_tls_certification/lets_encrypt_integration.md") - docs_link_start = "<a href=\"%{docs_link_url}\" target=\"_blank\" rel=\"noopener noreferrer\" class=\"text-nowrap\">".html_safe % { docs_link_url: docs_link_url } - docs_link_end = "</a>".html_safe = _("Let's Encrypt is a free, automated, and open certificate authority (CA) that gives digital certificates in order to enable HTTPS (SSL/TLS) for websites. Learn more about Let's Encrypt configuration by following the %{docs_link_start}documentation on GitLab Pages%{docs_link_end}.").html_safe % { docs_link_url: docs_link_url, docs_link_start: docs_link_start, docs_link_end: docs_link_end } diff --git a/app/views/projects/project_templates/_built_in_templates.html.haml b/app/views/projects/project_templates/_built_in_templates.html.haml index 6159f1c3542..d1c09e83fd3 100644 --- a/app/views/projects/project_templates/_built_in_templates.html.haml +++ b/app/views/projects/project_templates/_built_in_templates.html.haml @@ -9,9 +9,9 @@ .text-muted = template.description .controls.d-flex.align-items-center - %a.btn.btn-default.append-right-10{ href: template.preview, rel: 'noopener noreferrer', target: '_blank', data: { track_label: "create_from_template", track_property: "template_preview", track_event: "click_button", track_value: template.name } } + %a.btn.btn-default.append-right-10{ href: template.preview, rel: 'noopener noreferrer', target: '_blank', data: { track_label: "template_preview", track_property: template.name, track_event: "click_button", track_value: "" } } = _("Preview") %label.btn.btn-success.template-button.choose-template.append-bottom-0{ for: template.name } - %input{ type: "radio", autocomplete: "off", name: "project[template_name]", id: template.name, value: template.name, data: { track_label: "create_from_template", track_property: "template_use", track_event: "click_button" } } + %input{ type: "radio", autocomplete: "off", name: "project[template_name]", id: template.name, value: template.name, data: { track_label: "template_use", track_property: template.name, track_event: "click_button", track_value: "" } } %span = _("Use template") diff --git a/app/views/projects/tree/_tree_header.html.haml b/app/views/projects/tree/_tree_header.html.haml index 1d0bc588c9c..41cd044a5b0 100644 --- a/app/views/projects/tree/_tree_header.html.haml +++ b/app/views/projects/tree/_tree_header.html.haml @@ -11,7 +11,7 @@ - addtotree_toggle_attributes = { title: _("You can only add files when you are on a branch"), data: { container: 'body' }, class: 'disabled has-tooltip' } - if vue_file_list_enabled? - #js-repo-breadcrumb + #js-repo-breadcrumb{ data: breadcrumb_data_attributes } - else %ul.breadcrumb.repo-breadcrumb %li.breadcrumb-item diff --git a/app/views/projects/triggers/_content.html.haml b/app/views/projects/triggers/_content.html.haml index 96a41aa066c..e686068657c 100644 --- a/app/views/projects/triggers/_content.html.haml +++ b/app/views/projects/triggers/_content.html.haml @@ -1,8 +1,9 @@ -%p.append-bottom-default - Triggers with the - %span.badge.badge-primary legacy - label do not have an associated user and only have access to the current project. - %br - = succeed '.' do - Learn more in the - = link_to 'triggers documentation', help_page_path('ci/triggers/README'), target: '_blank' +- if Feature.enabled?(:use_legacy_pipeline_triggers, @project) + %p.append-bottom-default + Triggers with the + %span.badge.badge-primary legacy + label do not have an associated user and only have access to the current project. + %br + = succeed '.' do + Learn more in the + = link_to 'triggers documentation', help_page_path('ci/triggers/README'), target: '_blank' diff --git a/app/views/projects/triggers/_trigger.html.haml b/app/views/projects/triggers/_trigger.html.haml index 6f6f1e5e0c5..31a598ccd5e 100644 --- a/app/views/projects/triggers/_trigger.html.haml +++ b/app/views/projects/triggers/_trigger.html.haml @@ -8,8 +8,11 @@ .label-container - if trigger.legacy? - %span.badge.badge-primary.has-tooltip{ title: "Trigger makes use of deprecated functionality" } legacy - - if !trigger.can_access_project? + - if trigger.supports_legacy_tokens? + %span.badge.badge-primary.has-tooltip{ title: "Trigger makes use of deprecated functionality" } legacy + - else + %span.badge.badge-danger.has-tooltip{ title: "Trigger is invalid due to being a legacy trigger. We recommend replacing it with a new trigger" } invalid + - elsif !trigger.can_access_project? %span.badge.badge-danger.has-tooltip{ title: "Trigger user has insufficient permissions to project" } invalid %td diff --git a/app/views/shared/_visibility_radios.html.haml b/app/views/shared/_visibility_radios.html.haml index 342fdb20d41..82ffdc9cd13 100644 --- a/app/views/shared/_visibility_radios.html.haml +++ b/app/views/shared/_visibility_radios.html.haml @@ -4,7 +4,7 @@ - next if disallowed || restricted .form-check - = form.radio_button model_method, level, checked: (selected_level == level), class: 'form-check-input', data: { track_label: "blank_project", track_event: "activate_form_input", track_property: "#{model_method}", track_value: "#{level}" } + = form.radio_button model_method, level, checked: (selected_level == level), class: 'form-check-input', data: { track_label: "blank_project", track_event: "activate_form_input", track_property: "#{model_method}_#{level}", track_value: "" } = form.label "#{model_method}_#{level}", class: 'form-check-label' do = visibility_level_icon(level) .option-title diff --git a/app/views/shared/boards/_switcher.html.haml b/app/views/shared/boards/_switcher.html.haml new file mode 100644 index 00000000000..79118630762 --- /dev/null +++ b/app/views/shared/boards/_switcher.html.haml @@ -0,0 +1,16 @@ +- parent = board.parent +- milestone_filter_opts = { format: :json } +- milestone_filter_opts = milestone_filter_opts.merge(only_group_milestones: true) if board.group_board? +- weights = Gitlab.ee? ? ([Issue::WEIGHT_ANY] + Issue.weight_options) : [] + +#js-multiple-boards-switcher.inline.boards-switcher{ data: { current_board: current_board_json.to_json, + milestone_path: milestones_filter_path(milestone_filter_opts), + board_base_url: board_base_url, + has_missing_boards: (!multiple_boards_available? && current_board_parent.boards.size > 1).to_s, + can_admin_board: can?(current_user, :admin_board, parent).to_s, + multiple_issue_boards_available: parent.multiple_issue_boards_available?.to_s, + labels_path: labels_filter_path_with_defaults(only_group_labels: true, include_descendant_groups: true), + project_id: @project&.id, + group_id: @group&.id, + scoped_issue_board_feature_enabled: Gitlab.ee? && parent.feature_available?(:scoped_issue_board) ? 'true' : 'false', + weights: weights.to_json } } diff --git a/app/views/shared/issuable/_feed_buttons.html.haml b/app/views/shared/issuable/_feed_buttons.html.haml index c9506a3295c..83f60fa6fe2 100644 --- a/app/views/shared/issuable/_feed_buttons.html.haml +++ b/app/views/shared/issuable/_feed_buttons.html.haml @@ -1,4 +1,4 @@ -= link_to safe_params.merge(rss_url_options), class: 'btn btn-svg has-tooltip js-rss-button', data: { container: 'body' }, title: _('Subscribe to RSS feed') do - = sprite_icon('rss') -= link_to safe_params.merge(calendar_url_options), class: 'btn btn-svg has-tooltip', data: { container: 'body' }, title: _('Subscribe to calendar') do - = sprite_icon('calendar') += link_to safe_params.merge(rss_url_options), class: 'btn has-tooltip', data: { container: 'body' }, title: _('Subscribe to RSS feed') do + = icon('rss') += link_to safe_params.merge(calendar_url_options), class: 'btn has-tooltip', data: { container: 'body' }, title: _('Subscribe to calendar') do + = custom_icon('icon_calendar') diff --git a/app/views/shared/issuable/_search_bar.html.haml b/app/views/shared/issuable/_search_bar.html.haml index a97ac5e2a2d..e253413929a 100644 --- a/app/views/shared/issuable/_search_bar.html.haml +++ b/app/views/shared/issuable/_search_bar.html.haml @@ -6,7 +6,7 @@ .issues-filters{ class: ("w-100" if type == :boards_modal) } .issues-details-filters.filtered-search-block.d-flex.flex-column.flex-md-row{ class: block_css_class, "v-pre" => type == :boards_modal } - if type == :boards - = render_if_exists "shared/boards/switcher", board: board + = render "shared/boards/switcher", board: board = form_tag page_filter_path, method: :get, class: 'filter-form js-filter-form w-100' do - if params[:search].present? = hidden_field_tag :search, params[:search] diff --git a/changelogs/unreleased/48771-label-picker-line-break-on-long-label-titles.yml b/changelogs/unreleased/48771-label-picker-line-break-on-long-label-titles.yml new file mode 100644 index 00000000000..e598247b5d8 --- /dev/null +++ b/changelogs/unreleased/48771-label-picker-line-break-on-long-label-titles.yml @@ -0,0 +1,5 @@ +--- +title: 'Resolve Label picker: Line break on long label titles' +merge_request: 30610 +author: +type: fixed diff --git a/changelogs/unreleased/60666-kubernetes-applications-uninstall-runner.yml b/changelogs/unreleased/60666-kubernetes-applications-uninstall-runner.yml new file mode 100644 index 00000000000..3632c8eec20 --- /dev/null +++ b/changelogs/unreleased/60666-kubernetes-applications-uninstall-runner.yml @@ -0,0 +1,5 @@ +--- +title: Allow GitLab Runner to be uninstalled from the UI +merge_request: 30176 +author: +type: added diff --git a/changelogs/unreleased/61145-fix-button-dimensions.yml b/changelogs/unreleased/61145-fix-button-dimensions.yml deleted file mode 100644 index 8f209ceaa8e..00000000000 --- a/changelogs/unreleased/61145-fix-button-dimensions.yml +++ /dev/null @@ -1,5 +0,0 @@ ---- -title: Updating button dimensions according to design spec -merge_request: 28545 -author: -type: fixed diff --git a/changelogs/unreleased/61342-commit-search-result-doesn-t-pass-wcag-color-audit.yml b/changelogs/unreleased/61342-commit-search-result-doesn-t-pass-wcag-color-audit.yml new file mode 100644 index 00000000000..f4ed4551aab --- /dev/null +++ b/changelogs/unreleased/61342-commit-search-result-doesn-t-pass-wcag-color-audit.yml @@ -0,0 +1,5 @@ +--- +title: Change color for namespace in commit search +merge_request: 30312 +author: +type: other diff --git a/changelogs/unreleased/61613-spacing-mr-widgets.yml b/changelogs/unreleased/61613-spacing-mr-widgets.yml new file mode 100644 index 00000000000..7d37ef8da2e --- /dev/null +++ b/changelogs/unreleased/61613-spacing-mr-widgets.yml @@ -0,0 +1,5 @@ +--- +title: Left align mr widget icons and text +merge_request: 28561 +author: +type: fixed diff --git a/changelogs/unreleased/64070-asciidoctor-enable-section-anchors.yml b/changelogs/unreleased/64070-asciidoctor-enable-section-anchors.yml new file mode 100644 index 00000000000..51c1537a159 --- /dev/null +++ b/changelogs/unreleased/64070-asciidoctor-enable-section-anchors.yml @@ -0,0 +1,5 @@ +--- +title: "Enable section anchors in Asciidoctor" +merge_request: 30666 +author: Guillaume Grossetie +type: added
\ No newline at end of file diff --git a/changelogs/unreleased/64249-align-container-registry-empty-state-with-design-guidelines.yml b/changelogs/unreleased/64249-align-container-registry-empty-state-with-design-guidelines.yml new file mode 100644 index 00000000000..ecdb4b6bed1 --- /dev/null +++ b/changelogs/unreleased/64249-align-container-registry-empty-state-with-design-guidelines.yml @@ -0,0 +1,5 @@ +--- +title: Alignign empty container registry message with design guidelines +merge_request: 30502 +author: +type: other diff --git a/changelogs/unreleased/64315-mget_sessions_in_chunks.yml b/changelogs/unreleased/64315-mget_sessions_in_chunks.yml new file mode 100644 index 00000000000..d50d86726e2 --- /dev/null +++ b/changelogs/unreleased/64315-mget_sessions_in_chunks.yml @@ -0,0 +1,5 @@ +--- +title: Do Redis lookup in batches in ActiveSession.sessions_from_ids +merge_request: 30561 +author: +type: performance diff --git a/changelogs/unreleased/64645-asciidoctor-preserve-footnote-link-ids.yml b/changelogs/unreleased/64645-asciidoctor-preserve-footnote-link-ids.yml new file mode 100644 index 00000000000..5427a035478 --- /dev/null +++ b/changelogs/unreleased/64645-asciidoctor-preserve-footnote-link-ids.yml @@ -0,0 +1,5 @@ +--- +title: "Preserve footnote link ids in Asciidoctor" +merge_request: 30790 +author: Guillaume Grossetie +type: added
\ No newline at end of file diff --git a/changelogs/unreleased/9928ee-add-rule_type-to-approval-project-rules.yml b/changelogs/unreleased/9928ee-add-rule_type-to-approval-project-rules.yml new file mode 100644 index 00000000000..698ecebb971 --- /dev/null +++ b/changelogs/unreleased/9928ee-add-rule_type-to-approval-project-rules.yml @@ -0,0 +1,5 @@ +--- +title: Add migration for adding rule_type to approval_project_rules +merge_request: 30575 +author: +type: added diff --git a/changelogs/unreleased/bjk-fix_prom_example.yml b/changelogs/unreleased/bjk-fix_prom_example.yml new file mode 100644 index 00000000000..2f81bc6196b --- /dev/null +++ b/changelogs/unreleased/bjk-fix_prom_example.yml @@ -0,0 +1,5 @@ +--- +title: Update example Prometheus scrape config +merge_request: 30739 +author: +type: other diff --git a/changelogs/unreleased/button-bug-fixes.yml b/changelogs/unreleased/button-bug-fixes.yml deleted file mode 100644 index b63bfdf24ad..00000000000 --- a/changelogs/unreleased/button-bug-fixes.yml +++ /dev/null @@ -1,5 +0,0 @@ ---- -title: Fix Project Badge Button Styles -merge_request: 30678 -author: -type: fixed diff --git a/changelogs/unreleased/fix-broken-vue-i18n-strings.yml b/changelogs/unreleased/fix-broken-vue-i18n-strings.yml new file mode 100644 index 00000000000..69cec8a6b1b --- /dev/null +++ b/changelogs/unreleased/fix-broken-vue-i18n-strings.yml @@ -0,0 +1,5 @@ +--- +title: Fix broken warnings while Editing Issues and Edit File on MR +merge_request: +author: +type: fixed diff --git a/changelogs/unreleased/issue-zoom-url.yml b/changelogs/unreleased/issue-zoom-url.yml new file mode 100644 index 00000000000..e0bd5478192 --- /dev/null +++ b/changelogs/unreleased/issue-zoom-url.yml @@ -0,0 +1,5 @@ +--- +title: Extract zoom link from issue and pass to frontend +merge_request: 29910 +author: raju249 +type: added diff --git a/changelogs/unreleased/jc-remove-catfile-flag.yml b/changelogs/unreleased/jc-remove-catfile-flag.yml new file mode 100644 index 00000000000..6b72de7bc45 --- /dev/null +++ b/changelogs/unreleased/jc-remove-catfile-flag.yml @@ -0,0 +1,5 @@ +--- +title: Remove catfile cache feature flag +merge_request: 30750 +author: +type: performance diff --git a/changelogs/unreleased/mh-mermaid-linebreaks.yml b/changelogs/unreleased/mh-mermaid-linebreaks.yml new file mode 100644 index 00000000000..e38820d8ce3 --- /dev/null +++ b/changelogs/unreleased/mh-mermaid-linebreaks.yml @@ -0,0 +1,5 @@ +--- +title: Fix linebreak rendering in Mermaid flowcharts +merge_request: 30730 +author: +type: fixed diff --git a/changelogs/unreleased/remove-support-for-legacy-pipeline-triggers.yml b/changelogs/unreleased/remove-support-for-legacy-pipeline-triggers.yml new file mode 100644 index 00000000000..3f4d4bbd432 --- /dev/null +++ b/changelogs/unreleased/remove-support-for-legacy-pipeline-triggers.yml @@ -0,0 +1,5 @@ +--- +title: Remove support for legacy pipeline triggers +merge_request: 30133 +author: +type: removed diff --git a/changelogs/unreleased/sh-bump-fog-aws.yml b/changelogs/unreleased/sh-bump-fog-aws.yml new file mode 100644 index 00000000000..a936b81ff02 --- /dev/null +++ b/changelogs/unreleased/sh-bump-fog-aws.yml @@ -0,0 +1,5 @@ +--- +title: Bump fog-aws to v3.5.2 +merge_request: 30803 +author: +type: fixed diff --git a/changelogs/unreleased/winh-multiple-issueboards-core.yml b/changelogs/unreleased/winh-multiple-issueboards-core.yml new file mode 100644 index 00000000000..c45e420c133 --- /dev/null +++ b/changelogs/unreleased/winh-multiple-issueboards-core.yml @@ -0,0 +1,5 @@ +--- +title: Move multiple issue boards to core +merge_request: 30503 +author: +type: changed diff --git a/config/gitlab.yml.example b/config/gitlab.yml.example index 334c241bcaa..0e78980350f 100644 --- a/config/gitlab.yml.example +++ b/config/gitlab.yml.example @@ -952,6 +952,16 @@ production: &base # address: localhost # port: 3807 + ## Prometheus settings + # Do not modify these settings here. They should be modified in /etc/gitlab/gitlab.rb + # if you installed GitLab via Omnibus. + # If you installed from source, you need to install and configure Prometheus + # yourself, and then update the values here. + # https://docs.gitlab.com/ee/administration/monitoring/prometheus/ + prometheus: + # enable: true + # listen_address: 'localhost:9090' + # # 5. Extra customization # ========================== @@ -1158,6 +1168,9 @@ test: user_filter: '' group_base: 'ou=groups,dc=example,dc=com' admin_group: '' + prometheus: + enable: true + listen_address: 'localhost:9090' staging: <<: *base diff --git a/config/initializers/zz_metrics.rb b/config/initializers/zz_metrics.rb index 5aa6f73c5c5..b005fdf159b 100644 --- a/config/initializers/zz_metrics.rb +++ b/config/initializers/zz_metrics.rb @@ -6,6 +6,7 @@ # that we can stub it for testing, as it is only called when metrics are # enabled. # +# rubocop:disable Metrics/AbcSize def instrument_classes(instrumentation) instrumentation.instrument_instance_methods(Gitlab::Shell) @@ -53,7 +54,7 @@ def instrument_classes(instrumentation) instrumentation.instrument_methods(Banzai::Querying) instrumentation.instrument_instance_methods(Banzai::ObjectRenderer) - instrumentation.instrument_instance_methods(Banzai::Redactor) + instrumentation.instrument_instance_methods(Banzai::ReferenceRedactor) [Issuable, Mentionable, Participable].each do |klass| instrumentation.instrument_instance_methods(klass) @@ -86,12 +87,42 @@ def instrument_classes(instrumentation) instrumentation.instrument_methods(Gitlab::Highlight) instrumentation.instrument_instance_methods(Gitlab::Highlight) + Gitlab.ee do + instrumentation.instrument_methods(Elasticsearch::Git::Repository) + instrumentation.instrument_instance_methods(Elasticsearch::Git::Repository) + + instrumentation.instrument_instance_methods(Search::GlobalService) + instrumentation.instrument_instance_methods(Search::ProjectService) + + instrumentation.instrument_instance_methods(Gitlab::Elastic::SearchResults) + instrumentation.instrument_instance_methods(Gitlab::Elastic::ProjectSearchResults) + instrumentation.instrument_instance_methods(Gitlab::Elastic::Indexer) + instrumentation.instrument_instance_methods(Gitlab::Elastic::SnippetSearchResults) + instrumentation.instrument_methods(Gitlab::Elastic::Helper) + + instrumentation.instrument_instance_methods(Elastic::ApplicationSearch) + instrumentation.instrument_instance_methods(Elastic::IssuesSearch) + instrumentation.instrument_instance_methods(Elastic::MergeRequestsSearch) + instrumentation.instrument_instance_methods(Elastic::MilestonesSearch) + instrumentation.instrument_instance_methods(Elastic::NotesSearch) + instrumentation.instrument_instance_methods(Elastic::ProjectsSearch) + instrumentation.instrument_instance_methods(Elastic::RepositoriesSearch) + instrumentation.instrument_instance_methods(Elastic::SnippetsSearch) + instrumentation.instrument_instance_methods(Elastic::WikiRepositoriesSearch) + + instrumentation.instrument_instance_methods(Gitlab::BitbucketImport::Importer) + instrumentation.instrument_instance_methods(Bitbucket::Connection) + + instrumentation.instrument_instance_methods(Geo::RepositorySyncWorker) + end + # This is a Rails scope so we have to instrument it manually. instrumentation.instrument_method(Project, :visible_to_user) # Needed for https://gitlab.com/gitlab-org/gitlab-ce/issues/30224#note_32306159 instrumentation.instrument_instance_method(MergeRequestDiff, :load_commits) end +# rubocop:enable Metrics/AbcSize # With prometheus enabled by default this breaks all specs # that stubs methods using `any_instance_of` for the models reloaded here. diff --git a/danger/commit_messages/Dangerfile b/danger/commit_messages/Dangerfile index ec494635f02..0c675cc4c9c 100644 --- a/danger/commit_messages/Dangerfile +++ b/danger/commit_messages/Dangerfile @@ -88,6 +88,19 @@ def lint_commit(commit) # rubocop:disable Metrics/AbcSize # We ignore revert commits as they are well structured by Git already return false if commit.message.start_with?('Revert "') + # Fail if a suggestion commit is used and squash is not enabled + if commit.message.start_with?('Apply suggestion to') + if gitlab.mr_json['squash'] + return false + else + fail_commit( + commit, + 'If you are applying suggestions, enable squash in the merge request and re-run the failed job' + ) + return true + end + end + failures = false subject, separator, details = commit.message.split("\n", 3) @@ -114,16 +127,6 @@ def lint_commit(commit) # rubocop:disable Metrics/AbcSize ) end - # Fail if a suggestion commit is used and squash is not enabled - if commit.message.start_with?('Apply suggestion to') && !gitlab.mr_json['squash'] - fail_commit( - commit, - 'If you are applying suggestions, squash needs to be enabled in the merge request' - ) - - failures = true - end - unless subject_starts_with_capital?(subject) fail_commit(commit, 'The commit subject must start with a capital letter') failures = true diff --git a/danger/metadata/Dangerfile b/danger/metadata/Dangerfile index 1adca152736..f2d68e64eb6 100644 --- a/danger/metadata/Dangerfile +++ b/danger/metadata/Dangerfile @@ -1,5 +1,13 @@ # rubocop:disable Style/SignalException +THROUGHPUT_LABELS = [ + 'Community contribution', + 'security', + 'bug', + 'feature', + 'backstage' +].freeze + if gitlab.mr_body.size < 5 fail "Please provide a proper merge request description." end @@ -8,6 +16,10 @@ if gitlab.mr_labels.empty? fail "Please add labels to this merge request." end +if (THROUGHPUT_LABELS & gitlab.mr_labels).empty? + warn 'Please add a [throughput label](https://about.gitlab.com/handbook/engineering/management/throughput/#implementation) to this merge request.' +end + unless gitlab.mr_json["assignee"] warn "This merge request does not have any assignee yet. Setting an assignee clarifies who needs to take action on the merge request at any given time." end diff --git a/db/migrate/20190709204413_add_rule_type_to_approval_project_rules.rb b/db/migrate/20190709204413_add_rule_type_to_approval_project_rules.rb new file mode 100644 index 00000000000..87a228c9bf9 --- /dev/null +++ b/db/migrate/20190709204413_add_rule_type_to_approval_project_rules.rb @@ -0,0 +1,17 @@ +# frozen_string_literal: true + +class AddRuleTypeToApprovalProjectRules < ActiveRecord::Migration[5.1] + include Gitlab::Database::MigrationHelpers + + DOWNTIME = false + + disable_ddl_transaction! + + def up + add_column_with_default :approval_project_rules, :rule_type, :integer, limit: 2, default: 0, allow_null: false + end + + def down + remove_column :approval_project_rules, :rule_type + end +end diff --git a/db/migrate/20190710151229_add_index_to_approval_project_rules_rule_type.rb b/db/migrate/20190710151229_add_index_to_approval_project_rules_rule_type.rb new file mode 100644 index 00000000000..64123c53c4a --- /dev/null +++ b/db/migrate/20190710151229_add_index_to_approval_project_rules_rule_type.rb @@ -0,0 +1,17 @@ +# frozen_string_literal: true + +class AddIndexToApprovalProjectRulesRuleType < ActiveRecord::Migration[5.1] + include Gitlab::Database::MigrationHelpers + + DOWNTIME = false + + disable_ddl_transaction! + + def up + add_concurrent_index :approval_project_rules, :rule_type + end + + def down + remove_concurrent_index :approval_project_rules, :rule_type + end +end diff --git a/db/post_migrate/20190703185326_fix_wrong_pages_access_level.rb b/db/post_migrate/20190703185326_fix_wrong_pages_access_level.rb new file mode 100644 index 00000000000..e5981956cf5 --- /dev/null +++ b/db/post_migrate/20190703185326_fix_wrong_pages_access_level.rb @@ -0,0 +1,28 @@ +# frozen_string_literal: true + +class FixWrongPagesAccessLevel < ActiveRecord::Migration[5.1] + include Gitlab::Database::MigrationHelpers + + DOWNTIME = false + + MIGRATION = 'FixPagesAccessLevel' + BATCH_SIZE = 20_000 + BATCH_TIME = 2.minutes + + disable_ddl_transaction! + + class ProjectFeature < ActiveRecord::Base + include ::EachBatch + + self.table_name = 'project_features' + self.inheritance_column = :_type_disabled + end + + def up + queue_background_migration_jobs_by_range_at_intervals( + ProjectFeature, + MIGRATION, + BATCH_TIME, + batch_size: BATCH_SIZE) + end +end diff --git a/db/post_migrate/20190715114644_drop_project_features_pages_access_level_default.rb b/db/post_migrate/20190715114644_drop_project_features_pages_access_level_default.rb new file mode 100644 index 00000000000..2fb0aa0f460 --- /dev/null +++ b/db/post_migrate/20190715114644_drop_project_features_pages_access_level_default.rb @@ -0,0 +1,12 @@ +# frozen_string_literal: true + +class DropProjectFeaturesPagesAccessLevelDefault < ActiveRecord::Migration[5.1] + include Gitlab::Database::MigrationHelpers + + DOWNTIME = false + ENABLED_VALUE = 20 + + def change + change_column_default :project_features, :pages_access_level, from: ENABLED_VALUE, to: nil + end +end diff --git a/db/schema.rb b/db/schema.rb index 644ca1fe970..a5079d3a5bc 100644 --- a/db/schema.rb +++ b/db/schema.rb @@ -10,7 +10,7 @@ # # It's strongly recommended that you check this file into your version control system. -ActiveRecord::Schema.define(version: 2019_07_03_130053) do +ActiveRecord::Schema.define(version: 2019_07_15_114644) do # These are extensions that must be enabled in order to support this database enable_extension "pg_trgm" @@ -282,7 +282,9 @@ ActiveRecord::Schema.define(version: 2019_07_03_130053) do t.integer "project_id", null: false t.integer "approvals_required", limit: 2, default: 0, null: false t.string "name", null: false + t.integer "rule_type", limit: 2, default: 0, null: false t.index ["project_id"], name: "index_approval_project_rules_on_project_id", using: :btree + t.index ["rule_type"], name: "index_approval_project_rules_on_rule_type", using: :btree end create_table "approval_project_rules_groups", force: :cascade do |t| @@ -2507,7 +2509,7 @@ ActiveRecord::Schema.define(version: 2019_07_03_130053) do t.datetime "created_at" t.datetime "updated_at" t.integer "repository_access_level", default: 20, null: false - t.integer "pages_access_level", default: 20, null: false + t.integer "pages_access_level", null: false t.index ["project_id"], name: "index_project_features_on_project_id", unique: true, using: :btree end diff --git a/doc/administration/auth/README.md b/doc/administration/auth/README.md index d8094587d14..2fc9db0632e 100644 --- a/doc/administration/auth/README.md +++ b/doc/administration/auth/README.md @@ -1,19 +1,34 @@ --- comments: false +type: index --- -# Authentication and Authorization +# GitLab authentication and authorization GitLab integrates with the following external authentication and authorization -providers. +providers: -- [LDAP](ldap.md) Includes Active Directory, Apple Open Directory, Open LDAP, - and 389 Server +- [Auth0](../../integration/auth0.md) +- [Authentiq](authentiq.md) +- [Azure](../../integration/azure.md) +- [Bitbucket Cloud](../../integration/bitbucket.md) +- [CAS](../../integration/cas.md) +- [Crowd](../../integration/crowd.md) +- [Facebook](../../integration/facebook.md) +- [GitHub](../../integration/github.md) +- [GitLab.com](../../integration/gitlab.md) +- [Google](../../integration/google.md) +- [JWT](jwt.md) +- [Kerberos](../../integration/kerberos.md) +- [LDAP](ldap.md): Includes Active Directory, Apple Open Directory, Open LDAP, + and 389 Server. - [LDAP for GitLab EE](ldap-ee.md): LDAP additions to GitLab Enterprise Editions **(STARTER ONLY)** -- [OmniAuth](../../integration/omniauth.md) Sign in via Twitter, GitHub, GitLab.com, Google, - Bitbucket, Facebook, Shibboleth, Crowd, Azure, Authentiq ID, and JWT -- [CAS](../../integration/cas.md) Configure GitLab to sign in using CAS -- [SAML](../../integration/saml.md) Configure GitLab as a SAML 2.0 Service Provider -- [Okta](okta.md) Configure GitLab to sign in using Okta -- [Authentiq](authentiq.md): Enable the Authentiq OmniAuth provider for passwordless authentication -- [Smartcard](smartcard.md) Smartcard authentication **(PREMIUM ONLY)** + - [Google Secure LDAP](google_secure_ldap.md) +- [Okta](okta.md) +- [Salesforce](../../integration/salesforce.md) +- [SAML](../../integration/saml.md) +- [SAML for GitLab.com groups](../../user/group/saml_sso/index.md) **(SILVER ONLY)** +- [Shibboleth](../../integration/shibboleth.md) +- [Smartcard](smartcard.md) **(PREMIUM ONLY)** +- [Twitter](../../integration/twitter.md) +- [UltraAuth](../../integration/ultra_auth.md) diff --git a/doc/administration/auth/authentiq.md b/doc/administration/auth/authentiq.md index 835c97c0288..b84eca4ef0d 100644 --- a/doc/administration/auth/authentiq.md +++ b/doc/administration/auth/authentiq.md @@ -1,3 +1,7 @@ +--- +type: reference +--- + # Authentiq OmniAuth Provider To enable the Authentiq OmniAuth provider for passwordless authentication you must register an application with Authentiq. @@ -66,3 +70,15 @@ On the sign in page there should now be an Authentiq icon below the regular sign - If not they will be prompted to download the app and then follow the procedure above. If everything goes right, the user will be returned to GitLab and will be signed in. + +<!-- ## Troubleshooting + +Include any troubleshooting steps that you can foresee. If you know beforehand what issues +one might have when setting this up, or when something is changed, or on upgrading, it's +important to describe those, too. Think of things that may go wrong and include them here. +This is important to minimize requests for support, and to avoid doc comments with +questions that you know someone might ask. + +Each scenario can be a third-level heading, e.g. `### Getting error message X`. +If you have none to add when creating a doc, leave this section in place +but commented out to help encourage others to add to it in the future. --> diff --git a/doc/administration/auth/crowd.md b/doc/administration/auth/crowd.md index 86c7bad2ebf..ac63b4f2b97 100644 --- a/doc/administration/auth/crowd.md +++ b/doc/administration/auth/crowd.md @@ -1,5 +1,11 @@ +--- +type: reference +--- + # Atlassian Crowd OmniAuth Provider +Authenticate to GitLab using the Atlassian Crowd OmniAuth provider. + ## Configure a new Crowd application 1. Choose 'Applications' in the top menu, then 'Add application'. diff --git a/doc/administration/auth/google_secure_ldap.md b/doc/administration/auth/google_secure_ldap.md index 0e6d7ff1df1..55e6f53622c 100644 --- a/doc/administration/auth/google_secure_ldap.md +++ b/doc/administration/auth/google_secure_ldap.md @@ -1,3 +1,7 @@ +--- +type: reference +--- + # Google Secure LDAP **(CORE ONLY)** > [Introduced](https://gitlab.com/gitlab-org/gitlab-ce/issues/46391) in GitLab 11.9. @@ -204,3 +208,15 @@ values obtained during the LDAP client configuration earlier: [reconfigure]: ../restart_gitlab.md#omnibus-gitlab-reconfigure [restart]: ../restart_gitlab.md#installations-from-source + +<!-- ## Troubleshooting + +Include any troubleshooting steps that you can foresee. If you know beforehand what issues +one might have when setting this up, or when something is changed, or on upgrading, it's +important to describe those, too. Think of things that may go wrong and include them here. +This is important to minimize requests for support, and to avoid doc comments with +questions that you know someone might ask. + +Each scenario can be a third-level heading, e.g. `### Getting error message X`. +If you have none to add when creating a doc, leave this section in place +but commented out to help encourage others to add to it in the future. --> diff --git a/doc/administration/auth/how_to_configure_ldap_gitlab_ce/index.md b/doc/administration/auth/how_to_configure_ldap_gitlab_ce/index.md index 320a65b665d..86dd398343b 100644 --- a/doc/administration/auth/how_to_configure_ldap_gitlab_ce/index.md +++ b/doc/administration/auth/how_to_configure_ldap_gitlab_ce/index.md @@ -1,15 +1,9 @@ --- -author: Chris Wilson -author_gitlab: MrChrisW -level: intermediary -article_type: admin guide -date: 2017-05-03 +type: howto --- # How to configure LDAP with GitLab CE -## Introduction - Managing a large number of users in GitLab can become a burden for system administrators. As an organization grows so do user accounts. Keeping these user accounts in sync across multiple enterprise applications often becomes a time consuming task. In this guide we will focus on configuring GitLab with Active Directory. [Active Directory](https://en.wikipedia.org/wiki/Active_Directory) is a popular LDAP compatible directory service provided by Microsoft, included in all modern Windows Server operating systems. @@ -268,3 +262,15 @@ have extended functionalities with LDAP, such as: - Multiple LDAP servers Read through the article on [LDAP for GitLab EE](../how_to_configure_ldap_gitlab_ee/index.md) **(STARTER ONLY)** for an overview. + +<!-- ## Troubleshooting + +Include any troubleshooting steps that you can foresee. If you know beforehand what issues +one might have when setting this up, or when something is changed, or on upgrading, it's +important to describe those, too. Think of things that may go wrong and include them here. +This is important to minimize requests for support, and to avoid doc comments with +questions that you know someone might ask. + +Each scenario can be a third-level heading, e.g. `### Getting error message X`. +If you have none to add when creating a doc, leave this section in place +but commented out to help encourage others to add to it in the future. --> diff --git a/doc/administration/auth/how_to_configure_ldap_gitlab_ee/index.md b/doc/administration/auth/how_to_configure_ldap_gitlab_ee/index.md index 2683950f143..366acb9ed3e 100644 --- a/doc/administration/auth/how_to_configure_ldap_gitlab_ee/index.md +++ b/doc/administration/auth/how_to_configure_ldap_gitlab_ee/index.md @@ -1,16 +1,10 @@ --- -author: Chris Wilson -author_gitlab: MrChrisW -level: intermediary -article_type: admin guide -date: 2017-05-03 +type: howto --- # How to configure LDAP with GitLab EE **(STARTER ONLY)** -## Introduction - -The present article follows [How to Configure LDAP with GitLab CE](../how_to_configure_ldap_gitlab_ce/index.md). Make sure to read through it before moving forward. +This article expands on [How to Configure LDAP with GitLab CE](../how_to_configure_ldap_gitlab_ce/index.md). Make sure to read through it before moving forward. ## GitLab Enterprise Edition - LDAP features @@ -117,3 +111,15 @@ Integration of GitLab with Active Directory (LDAP) reduces the complexity of use It has the advantage of improving user permission controls, whilst easing the deployment of GitLab into an existing [IT environment](https://www.techopedia.com/definition/29199/it-infrastructure). GitLab EE offers advanced group management and multiple LDAP servers. With the assistance of the [GitLab Support](https://about.gitlab.com/support) team, setting up GitLab with an existing AD/LDAP solution will be a smooth and painless process. + +<!-- ## Troubleshooting + +Include any troubleshooting steps that you can foresee. If you know beforehand what issues +one might have when setting this up, or when something is changed, or on upgrading, it's +important to describe those, too. Think of things that may go wrong and include them here. +This is important to minimize requests for support, and to avoid doc comments with +questions that you know someone might ask. + +Each scenario can be a third-level heading, e.g. `### Getting error message X`. +If you have none to add when creating a doc, leave this section in place +but commented out to help encourage others to add to it in the future. --> diff --git a/doc/administration/auth/jwt.md b/doc/administration/auth/jwt.md index 7db22bdd5df..e6b3287ce60 100644 --- a/doc/administration/auth/jwt.md +++ b/doc/administration/auth/jwt.md @@ -1,3 +1,7 @@ +--- +type: reference +--- + # JWT OmniAuth provider To enable the JWT OmniAuth provider, you must register your application with JWT. @@ -70,3 +74,15 @@ will be redirected to GitLab and will be signed in. [reconfigure]: ../restart_gitlab.md#omnibus-gitlab-reconfigure [restart GitLab]: ../restart_gitlab.md#installations-from-source + +<!-- ## Troubleshooting + +Include any troubleshooting steps that you can foresee. If you know beforehand what issues +one might have when setting this up, or when something is changed, or on upgrading, it's +important to describe those, too. Think of things that may go wrong and include them here. +This is important to minimize requests for support, and to avoid doc comments with +questions that you know someone might ask. + +Each scenario can be a third-level heading, e.g. `### Getting error message X`. +If you have none to add when creating a doc, leave this section in place +but commented out to help encourage others to add to it in the future. --> diff --git a/doc/administration/auth/ldap-ee.md b/doc/administration/auth/ldap-ee.md index 2afac23c20c..2f2ee8a27d3 100644 --- a/doc/administration/auth/ldap-ee.md +++ b/doc/administration/auth/ldap-ee.md @@ -1,3 +1,7 @@ +--- +type: reference +--- + # LDAP Additions in GitLab EE **(STARTER ONLY)** This is a continuation of the main [LDAP documentation](ldap.md), detailing LDAP diff --git a/doc/administration/auth/ldap.md b/doc/administration/auth/ldap.md index 86e6be5f4fa..be05a4d63a7 100644 --- a/doc/administration/auth/ldap.md +++ b/doc/administration/auth/ldap.md @@ -1,3 +1,7 @@ +--- +type: reference +--- + <!-- If the change is EE-specific, put it in `ldap-ee.md`, NOT here. --> # LDAP @@ -494,6 +498,13 @@ be mandatory and clients cannot be authenticated with the TLS protocol. ## Troubleshooting +If a user account is blocked or unblocked due to the LDAP configuration, a +message will be logged to `application.log`. + +If there is an unexpected error during an LDAP lookup (configuration error, +timeout), the login is rejected and a message will be logged to +`production.log`. + ### Debug LDAP user filter with ldapsearch This example uses ldapsearch and assumes you are using ActiveDirectory. The @@ -527,18 +538,9 @@ ldapsearch -H ldaps://$host:$port -D "$bind_dn" -y bind_dn_password.txt -b "$ba sudo -u git -H bundle exec rake gitlab:ldap:check RAILS_ENV=production ``` -### Connection Refused +### Connection refused If you are getting 'Connection Refused' errors when trying to connect to the LDAP server please double-check the LDAP `port` and `encryption` settings used by GitLab. Common combinations are `encryption: 'plain'` and `port: 389`, OR `encryption: 'simple_tls'` and `port: 636`. - -### Troubleshooting - -If a user account is blocked or unblocked due to the LDAP configuration, a -message will be logged to `application.log`. - -If there is an unexpected error during an LDAP lookup (configuration error, -timeout), the login is rejected and a message will be logged to -`production.log`. diff --git a/doc/administration/auth/oidc.md b/doc/administration/auth/oidc.md index 758501629af..78d040cda99 100644 --- a/doc/administration/auth/oidc.md +++ b/doc/administration/auth/oidc.md @@ -1,3 +1,7 @@ +--- +type: reference +--- + # OpenID Connect OmniAuth provider GitLab can use [OpenID Connect](https://openid.net/specs/openid-connect-core-1_0.html) as an OmniAuth provider. @@ -146,7 +150,7 @@ for more details: } ``` -### Troubleshooting +## Troubleshooting If you're having trouble, here are some tips: diff --git a/doc/administration/auth/okta.md b/doc/administration/auth/okta.md index 566003ba708..5524c3ba092 100644 --- a/doc/administration/auth/okta.md +++ b/doc/administration/auth/okta.md @@ -1,3 +1,7 @@ +--- +type: reference +--- + # Okta SSO provider Okta is a [Single Sign-on provider](https://www.okta.com/products/single-sign-on/) that can be used to authenticate @@ -157,3 +161,15 @@ Make sure the groups exist and are assigned to the Okta app. You can take a look of the [SAML documentation](../../integration/saml.md#marking-users-as-external-based-on-saml-groups) on external groups since it works the same. + +<!-- ## Troubleshooting + +Include any troubleshooting steps that you can foresee. If you know beforehand what issues +one might have when setting this up, or when something is changed, or on upgrading, it's +important to describe those, too. Think of things that may go wrong and include them here. +This is important to minimize requests for support, and to avoid doc comments with +questions that you know someone might ask. + +Each scenario can be a third-level heading, e.g. `### Getting error message X`. +If you have none to add when creating a doc, leave this section in place +but commented out to help encourage others to add to it in the future. --> diff --git a/doc/administration/auth/smartcard.md b/doc/administration/auth/smartcard.md index e47751e0cc5..4f236d1afb8 100644 --- a/doc/administration/auth/smartcard.md +++ b/doc/administration/auth/smartcard.md @@ -1,3 +1,7 @@ +--- +type: reference +--- + # Smartcard authentication **(PREMIUM ONLY)** GitLab supports authentication using smartcards. @@ -22,7 +26,7 @@ To use a smartcard with an X.509 certificate to authenticate against a local database with GitLab, `CN` and `emailAddress` must be defined in the certificate. For example: -``` +```text Certificate: Data: Version: 1 (0x0) @@ -212,3 +216,15 @@ attribute. As a prerequisite, you must use an LDAP server that: 1. Save the file and [restart](../restart_gitlab.md#installations-from-source) GitLab for the changes to take effect. + +<!-- ## Troubleshooting + +Include any troubleshooting steps that you can foresee. If you know beforehand what issues +one might have when setting this up, or when something is changed, or on upgrading, it's +important to describe those, too. Think of things that may go wrong and include them here. +This is important to minimize requests for support, and to avoid doc comments with +questions that you know someone might ask. + +Each scenario can be a third-level heading, e.g. `### Getting error message X`. +If you have none to add when creating a doc, leave this section in place +but commented out to help encourage others to add to it in the future. --> diff --git a/doc/administration/geo/disaster_recovery/background_verification.md b/doc/administration/geo/disaster_recovery/background_verification.md index 8eee9427b56..27866b7536e 100644 --- a/doc/administration/geo/disaster_recovery/background_verification.md +++ b/doc/administration/geo/disaster_recovery/background_verification.md @@ -171,14 +171,21 @@ If the **primary** and **secondary** nodes have a checksum verification mismatch ## Current limitations -Until [issue #5064][ee-5064] is completed, background verification doesn't cover -CI job artifacts and traces, LFS objects, or user uploads in file storage. -Verify their integrity manually by following [these instructions][foreground-verification] -on both nodes, and comparing the output between them. +Automatic background verification doesn't cover attachments, LFS objects, +job artifacts, and user uploads in file storage. You can keep track of the +progress to include them in [ee-1430]. For now, you can verify their integrity +manually by following [these instructions][foreground-verification] on both +nodes, and comparing the output between them. + +In GitLab EE 12.1, Geo calculates checksums for attachments, LFS objects and +archived traces on secondary nodes after the transfer, compares it with the +stored checksums, and rejects transfers if mismatched. Please note that Geo +currently does not support an automatic way to verify these data if they have +been synced before GitLab EE 12.1. Data in object storage is **not verified**, as the object store is responsible for ensuring the integrity of the data. [reset-verification]: background_verification.md#reset-verification-for-projects-where-verification-has-failed [foreground-verification]: ../../raketasks/check.md -[ee-5064]: https://gitlab.com/gitlab-org/gitlab-ee/issues/5064 +[ee-1430]: https://gitlab.com/groups/gitlab-org/-/epics/1430 diff --git a/doc/administration/high_availability/README.md b/doc/administration/high_availability/README.md index 4317d14ba68..41ef68f5b57 100644 --- a/doc/administration/high_availability/README.md +++ b/doc/administration/high_availability/README.md @@ -1,3 +1,7 @@ +--- +type: reference, concepts +--- + # Scaling and High Availability GitLab supports several different types of clustering and high-availability. diff --git a/doc/administration/high_availability/alpha_database.md b/doc/administration/high_availability/alpha_database.md index 7bf20be60e6..7afd739f44c 100644 --- a/doc/administration/high_availability/alpha_database.md +++ b/doc/administration/high_availability/alpha_database.md @@ -2,5 +2,4 @@ redirect_to: 'database.md' --- -This documentation has been moved to the main -[database documentation](database.md#configure_using_omnibus_for_high_availability). +This document was moved to [another location](database.md). diff --git a/doc/administration/high_availability/consul.md b/doc/administration/high_availability/consul.md index 1f93c8130d3..b02a61b9256 100644 --- a/doc/administration/high_availability/consul.md +++ b/doc/administration/high_availability/consul.md @@ -1,6 +1,8 @@ -# Working with the bundled Consul service **(PREMIUM ONLY)** +--- +type: reference +--- -## Overview +# Working with the bundled Consul service **(PREMIUM ONLY)** As part of its High Availability stack, GitLab Premium includes a bundled version of [Consul](https://www.consul.io/) that can be managed through `/etc/gitlab/gitlab.rb`. diff --git a/doc/administration/high_availability/database.md b/doc/administration/high_availability/database.md index 1702a731647..f7a1f425b40 100644 --- a/doc/administration/high_availability/database.md +++ b/doc/administration/high_availability/database.md @@ -1,5 +1,12 @@ +--- +type: reference +--- + # Configuring PostgreSQL for Scaling and High Availability +In this section, you'll be guided through configuring a PostgreSQL database +to be used with GitLab in a highly available environment. + ## Provide your own PostgreSQL instance **(CORE ONLY)** If you're hosting GitLab on a cloud provider, you can optionally use a diff --git a/doc/administration/high_availability/gitaly.md b/doc/administration/high_availability/gitaly.md index b7eaa4ce105..739d1ae35fb 100644 --- a/doc/administration/high_availability/gitaly.md +++ b/doc/administration/high_availability/gitaly.md @@ -1,3 +1,7 @@ +--- +type: reference +--- + # Configuring Gitaly for Scaled and High Availability Gitaly does not yet support full high availability. However, Gitaly is quite @@ -46,3 +50,15 @@ Continue configuration of other components by going back to: ``` 1. Run `sudo gitlab-ctl reconfigure` to compile the configuration. + +<!-- ## Troubleshooting + +Include any troubleshooting steps that you can foresee. If you know beforehand what issues +one might have when setting this up, or when something is changed, or on upgrading, it's +important to describe those, too. Think of things that may go wrong and include them here. +This is important to minimize requests for support, and to avoid doc comments with +questions that you know someone might ask. + +Each scenario can be a third-level heading, e.g. `### Getting error message X`. +If you have none to add when creating a doc, leave this section in place +but commented out to help encourage others to add to it in the future. --> diff --git a/doc/administration/high_availability/gitlab.md b/doc/administration/high_availability/gitlab.md index 83838928519..8818a9606de 100644 --- a/doc/administration/high_availability/gitlab.md +++ b/doc/administration/high_availability/gitlab.md @@ -1,4 +1,8 @@ -# Configuring GitLab Scaling and High Availability +--- +type: reference +--- + +# Configuring GitLab for Scaling and High Availability > **Note:** There is some additional configuration near the bottom for additional GitLab application servers. It's important to read and understand diff --git a/doc/administration/high_availability/load_balancer.md b/doc/administration/high_availability/load_balancer.md index 28b226cacd5..9e9f604317a 100644 --- a/doc/administration/high_availability/load_balancer.md +++ b/doc/administration/high_availability/load_balancer.md @@ -1,3 +1,7 @@ +--- +type: reference +--- + # Load Balancer for GitLab HA In an active/active GitLab configuration, you will need a load balancer to route @@ -114,3 +118,15 @@ Read more on high-availability configuration: if SSL was terminated at the load balancer. [gitlab-pages]: ../pages/index.md + +<!-- ## Troubleshooting + +Include any troubleshooting steps that you can foresee. If you know beforehand what issues +one might have when setting this up, or when something is changed, or on upgrading, it's +important to describe those, too. Think of things that may go wrong and include them here. +This is important to minimize requests for support, and to avoid doc comments with +questions that you know someone might ask. + +Each scenario can be a third-level heading, e.g. `### Getting error message X`. +If you have none to add when creating a doc, leave this section in place +but commented out to help encourage others to add to it in the future. --> diff --git a/doc/administration/high_availability/monitoring_node.md b/doc/administration/high_availability/monitoring_node.md index cbc1d4bcd52..b91a994d01e 100644 --- a/doc/administration/high_availability/monitoring_node.md +++ b/doc/administration/high_availability/monitoring_node.md @@ -1,7 +1,13 @@ +--- +type: reference +--- + # Configuring a Monitoring node for Scaling and High Availability > [Introduced](https://gitlab.com/gitlab-org/omnibus-gitlab/issues/3786) in GitLab 12.0. +You can configure a Prometheus node to monitor GitLab. + ## Standalone Monitoring node using GitLab Omnibus The GitLab Omnibus package can be used to configure a standalone Monitoring node running [Prometheus](../monitoring/prometheus/index.md) and [Grafana](../monitoring/performance/grafana_configuration.md). @@ -67,3 +73,15 @@ Once monitoring using Service Discovery is enabled with `consul['monitoring_serv ensure that `prometheus['scrape_configs']` is not set in `/etc/gitlab/gitlab.rb`. Setting both `consul['monitoring_service_discovery'] = true` and `prometheus['scrape_configs']` in `/etc/gitlab/gitlab.rb` will result in errors. + +<!-- ## Troubleshooting + +Include any troubleshooting steps that you can foresee. If you know beforehand what issues +one might have when setting this up, or when something is changed, or on upgrading, it's +important to describe those, too. Think of things that may go wrong and include them here. +This is important to minimize requests for support, and to avoid doc comments with +questions that you know someone might ask. + +Each scenario can be a third-level heading, e.g. `### Getting error message X`. +If you have none to add when creating a doc, leave this section in place +but commented out to help encourage others to add to it in the future. --> diff --git a/doc/administration/high_availability/nfs.md b/doc/administration/high_availability/nfs.md index 6ab6b8bed30..294f0e969d5 100644 --- a/doc/administration/high_availability/nfs.md +++ b/doc/administration/high_availability/nfs.md @@ -1,3 +1,7 @@ +--- +type: reference +--- + # NFS You can view information and options set for each of the mounted NFS file @@ -47,29 +51,15 @@ management between systems: ### Improving NFS performance with GitLab -NOTE: **Note:** This is only available starting in certain versions of GitLab: 11.5.11, -11.6.11, 11.7.12, 11.8.8, 11.9.0 and up (e.g. 11.10, 11.11, etc.) +NOTE: **Note:** From GitLab 12.1, it will automatically be detected if Rugged can and should be used per storage. -If you are using NFS to share Git data, we recommend that you enable a -number of feature flags that will allow GitLab application processes to -access Git data directly instead of going through the [Gitaly -service](../gitaly/index.md). Depending on your workload and disk -performance, these flags may help improve performance. See [the -issue](https://gitlab.com/gitlab-org/gitlab-ce/issues/57317) for more -details. - -To do this, run the Rake task: +If you previously enabled Rugged using the feature flag, you will need to unset the feature flag by using: ```sh -sudo gitlab-rake gitlab:features:enable_rugged +sudo gitlab-rake gitlab:features:unset_rugged ``` -If you need to undo this setting for some reason such as switching to [Gitaly without NFS](gitaly.md) -(recommended), run: - -```sh -sudo gitlab-rake gitlab:features:disable_rugged -``` +If the Rugged feature flag is explicitly set to either true or false, GitLab will use the value explicitly set. ### Known issues @@ -236,3 +226,15 @@ Read more on high-availability configuration: 1. [Configure the load balancers](load_balancer.md) [udp-log-shipping]: https://docs.gitlab.com/omnibus/settings/logs.html#udp-log-shipping-gitlab-enterprise-edition-only "UDP log shipping" + +<!-- ## Troubleshooting + +Include any troubleshooting steps that you can foresee. If you know beforehand what issues +one might have when setting this up, or when something is changed, or on upgrading, it's +important to describe those, too. Think of things that may go wrong and include them here. +This is important to minimize requests for support, and to avoid doc comments with +questions that you know someone might ask. + +Each scenario can be a third-level heading, e.g. `### Getting error message X`. +If you have none to add when creating a doc, leave this section in place +but commented out to help encourage others to add to it in the future. --> diff --git a/doc/administration/high_availability/nfs_host_client_setup.md b/doc/administration/high_availability/nfs_host_client_setup.md index a8d69b9ab0a..9b0e085fe25 100644 --- a/doc/administration/high_availability/nfs_host_client_setup.md +++ b/doc/administration/high_availability/nfs_host_client_setup.md @@ -1,3 +1,7 @@ +--- +type: reference +--- + # Configuring NFS for GitLab HA Setting up NFS for a GitLab HA setup allows all applications nodes in a cluster @@ -133,3 +137,15 @@ client with the command below. ```sh sudo ufw allow from <client-ip-address> to any port nfs ``` + +<!-- ## Troubleshooting + +Include any troubleshooting steps that you can foresee. If you know beforehand what issues +one might have when setting this up, or when something is changed, or on upgrading, it's +important to describe those, too. Think of things that may go wrong and include them here. +This is important to minimize requests for support, and to avoid doc comments with +questions that you know someone might ask. + +Each scenario can be a third-level heading, e.g. `### Getting error message X`. +If you have none to add when creating a doc, leave this section in place +but commented out to help encourage others to add to it in the future. --> diff --git a/doc/administration/high_availability/pgbouncer.md b/doc/administration/high_availability/pgbouncer.md index 6890b0f7db7..0b945bc6244 100644 --- a/doc/administration/high_availability/pgbouncer.md +++ b/doc/administration/high_availability/pgbouncer.md @@ -1,6 +1,8 @@ -# Working with the bundle Pgbouncer service +--- +type: reference +--- -## Overview +# Working with the bundle Pgbouncer service As part of its High Availability stack, GitLab Premium includes a bundled version of [Pgbouncer](https://pgbouncer.github.io/) that can be managed through `/etc/gitlab/gitlab.rb`. diff --git a/doc/administration/high_availability/redis.md b/doc/administration/high_availability/redis.md index c29514ed9f6..1b79dde9476 100644 --- a/doc/administration/high_availability/redis.md +++ b/doc/administration/high_availability/redis.md @@ -1,3 +1,7 @@ +--- +type: reference +--- + # Configuring Redis for Scaling and High Availability ## Provide your own Redis instance **(CORE ONLY)** diff --git a/doc/administration/high_availability/redis_source.md b/doc/administration/high_availability/redis_source.md index a5463e5128c..63915e5d96c 100644 --- a/doc/administration/high_availability/redis_source.md +++ b/doc/administration/high_availability/redis_source.md @@ -1,3 +1,7 @@ +--- +type: reference +--- + # Configuring non-Omnibus Redis for GitLab HA This is the documentation for configuring a Highly Available Redis setup when diff --git a/doc/administration/monitoring/prometheus/index.md b/doc/administration/monitoring/prometheus/index.md index 341ea3330d7..c8968c51393 100644 --- a/doc/administration/monitoring/prometheus/index.md +++ b/doc/administration/monitoring/prometheus/index.md @@ -134,17 +134,57 @@ To use an external Prometheus server: ```yaml scrape_configs: - - job_name: 'gitlab_exporters' + - job_name: nginx static_configs: - - targets: ['1.1.1.1:9168', '1.1.1.1:9236', '1.1.1.1:9236', '1.1.1.1:9100', '1.1.1.1:9121', '1.1.1.1:9187'] - - - job_name: 'gitlab_metrics' - metrics_path: /-/metrics + - targets: + - 1.1.1.1:8060 + - job_name: redis + static_configs: + - targets: + - 1.1.1.1:9121 + - job_name: postgres + static_configs: + - targets: + - 1.1.1.1:9187 + - job_name: node + static_configs: + - targets: + - 1.1.1.1:9100 + - job_name: gitlab-workhorse + static_configs: + - targets: + - 1.1.1.1:9229 + - job_name: gitlab-rails + metrics_path: "/-/metrics" + static_configs: + - targets: + - 1.1.1.1:8080 + - job_name: gitlab-sidekiq + static_configs: + - targets: + - 1.1.1.1:8082 + - job_name: gitlab_monitor_database + metrics_path: "/database" + static_configs: + - targets: + - 1.1.1.1:9168 + - job_name: gitlab_monitor_sidekiq + metrics_path: "/sidekiq" + static_configs: + - targets: + - 1.1.1.1:9168 + - job_name: gitlab_monitor_process + metrics_path: "/process" + static_configs: + - targets: + - 1.1.1.1:9168 + - job_name: gitaly static_configs: - - targets: ['1.1.1.1:443'] + - targets: + - 1.1.1.1:9236 ``` -1. Restart the Prometheus server. +1. Reload the Prometheus server. ## Viewing performance metrics diff --git a/doc/administration/pages/img/lets_encrypt_integration_v12_1.png b/doc/administration/pages/img/lets_encrypt_integration_v12_1.png Binary files differnew file mode 100644 index 00000000000..5ab63074e12 --- /dev/null +++ b/doc/administration/pages/img/lets_encrypt_integration_v12_1.png diff --git a/doc/administration/pages/index.md b/doc/administration/pages/index.md index 3cabe8eb16e..774e7056845 100644 --- a/doc/administration/pages/index.md +++ b/doc/administration/pages/index.md @@ -265,6 +265,23 @@ verification requirement. Navigate to `Admin area âž” Settings` and uncheck **Require users to prove ownership of custom domains** in the Pages section. This setting is enabled by default. +### Let's Encrypt integration + +> [Introduced](https://gitlab.com/gitlab-org/gitlab-ce/issues/28996) in GitLab 12.1. + +[GitLab Pages' Let's Encrypt integration](../../user/project/pages/custom_domains_ssl_tls_certification/lets_encrypt_integration.md) +allows users to add Let's Encrypt SSL certificates for GitLab Pages +sites served under a custom domain. + +To enable it, you'll need to: + +1. Choose an email on which you will recieve notifications about expiring domains. +1. Navigate to your instance's **Admin Area > Settings > Preferences** and expand **Pages** settings. +1. Enter the email for receiving notifications and accept Let's Encrypt's Terms of Service as shown below. +1. Click **Save changes**. + +![Let's Encrypt settings](img/lets_encrypt_integration_v12_1.png) + ### Access control > [Introduced](https://gitlab.com/gitlab-org/gitlab-ce/issues/33422) in GitLab 11.5. diff --git a/doc/administration/raketasks/maintenance.md b/doc/administration/raketasks/maintenance.md index 2b31233d429..8d0b5b42515 100644 --- a/doc/administration/raketasks/maintenance.md +++ b/doc/administration/raketasks/maintenance.md @@ -251,3 +251,22 @@ sudo gitlab-rake gitlab:exclusive_lease:clear[project_housekeeping:*] # to clear a lease for repository garbage collection in a specific project: (id=4) sudo gitlab-rake gitlab:exclusive_lease:clear[project_housekeeping:4] ``` + +## Display status of database migrations + +To check the status of migrations, you can use the following rake task: + +```bash +sudo gitlab-rake db:migrate:status +``` + +This will output a table with a `Status` of `up` or `down` for +each Migration ID. + +```bash +database: gitlabhq_production + + Status Migration ID Migration Name +-------------------------------------------------- + up migration_id migration_name +```
\ No newline at end of file diff --git a/doc/administration/uploads.md b/doc/administration/uploads.md index 2155cdda07d..51e267c865e 100644 --- a/doc/administration/uploads.md +++ b/doc/administration/uploads.md @@ -140,6 +140,75 @@ _The uploads are stored by default in 1. Save the file and [restart GitLab][] for the changes to take effect. 1. Migrate any existing local uploads to the object storage using [`gitlab:uploads:migrate` rake task](raketasks/uploads/migrate.md). +### OpenStack compatible connection settings + +The connection settings match those provided by [Fog](https://github.com/fog), and are as follows: + +| Setting | Description | Default | +|---------|-------------|---------| +| `provider` | Always `OpenStack` for compatible hosts | OpenStack | +| `openstack_username` | OpenStack username | | +| `openstack_api_key` | OpenStack api key | | +| `openstack_temp_url_key` | OpenStack key for generating temporary urls | | +| `openstack_auth_url` | OpenStack authentication endpont | | +| `openstack_region` | OpenStack region | | +| `openstack_tenant` | OpenStack tenant ID | + +**In Omnibus installations:** + +_The uploads are stored by default in +`/var/opt/gitlab/gitlab-rails/public/uploads/-/system`._ + +1. Edit `/etc/gitlab/gitlab.rb` and add the following lines by replacing with + the values you want: + + ```ruby + gitlab_rails['uploads_object_store_remote_directory'] = "OPENSTACK_OBJECT_CONTAINER_NAME" + gitlab_rails['uploads_object_store_connection'] = { + 'provider' => 'OpenStack', + 'openstack_username' => 'OPENSTACK_USERNAME', + 'openstack_api_key' => 'OPENSTACK_PASSWORD', + 'openstack_temp_url_key' => 'OPENSTACK_TEMP_URL_KEY', + 'openstack_auth_url' => 'https://auth.cloud.ovh.net/v2.0/', + 'openstack_region' => 'DE1', + 'openstack_tenant' => 'TENANT_ID', + } + ``` + +1. Save the file and [reconfigure GitLab][] for the changes to take effect. +1. Migrate any existing local uploads to the object storage using [`gitlab:uploads:migrate` rake task](raketasks/uploads/migrate.md). + +--- + +**In installations from source:** + +_The uploads are stored by default in +`/home/git/gitlab/public/uploads/-/system`._ + +1. Edit `/home/git/gitlab/config/gitlab.yml` and add or amend the following + lines: + + ```yaml + uploads: + object_store: + enabled: true + direct_upload: false + background_upload: true + proxy_download: false + remote_directory: OPENSTACK_OBJECT_CONTAINER_NAME + connection: + provider: OpenStack + openstack_username: OPENSTACK_USERNAME + openstack_api_key: OPENSTACK_PASSWORD + openstack_temp_url_key: OPENSTACK_TEMP_URL_KEY + openstack_auth_url: 'https://auth.cloud.ovh.net/v2.0/' + openstack_region: DE1 + openstack_tenant: 'TENANT_ID' + ``` + +1. Save the file and [reconfigure GitLab][] for the changes to take effect. +1. Migrate any existing local uploads to the object storage using [`gitlab:uploads:migrate` rake task](raketasks/uploads/migrate.md). + [reconfigure gitlab]: restart_gitlab.md#omnibus-gitlab-reconfigure "How to reconfigure Omnibus GitLab" [restart gitlab]: restart_gitlab.md#installations-from-source "How to restart GitLab" [eep]: https://about.gitlab.com/gitlab-ee/ "GitLab Premium" diff --git a/doc/api/README.md b/doc/api/README.md index 9d90677e2bb..8e60d1c61df 100644 --- a/doc/api/README.md +++ b/doc/api/README.md @@ -29,6 +29,7 @@ The following API resources are available in the project context: | [Commits](commits.md) | `/projects/:id/repository/commits`, `/projects/:id/statuses` | | [Container Registry](container_registry.md) | `/projects/:id/registry/repositories` | | [Custom attributes](custom_attributes.md) | `/projects/:id/custom_attributes` (also available for groups and users) | +| [Dependencies](dependencies.md) **[ULTIMATE]** | `/projects/:id/dependencies` | [Deploy keys](deploy_keys.md) | `/projects/:id/deploy_keys` (also available standalone) | | [Deployments](deployments.md) | `/projects/:id/deployments` | | [Discussions](discussions.md) (threaded comments) | `/projects/:id/issues/.../discussions`, `/projects/:id/snippets/.../discussions`, `/projects/:id/merge_requests/.../discussions`, `/projects/:id/commits/.../discussions` (also available for groups) | diff --git a/doc/api/dependencies.md b/doc/api/dependencies.md new file mode 100644 index 00000000000..ed5ebdade19 --- /dev/null +++ b/doc/api/dependencies.md @@ -0,0 +1,50 @@ +# Dependencies API **(ULTIMATE)** + +CAUTION: **Caution:** +This API is in an alpha stage and considered unstable. +The response payload may be subject to change or breakage +across GitLab releases. + +Every call to this endpoint requires authentication. To perform this call, user should be authorized to read +[Project Security Dashboard](../user/application_security/security_dashboard/index.md#project-security-dashboard). + +## List project dependencies + +Get a list of project dependencies. This API partially mirroring +[Dependency List](../user/application_security/dependency_scanning/index.md#dependency-list) feature. +This list can be generated only for [languages and package managers](../user/application_security/dependency_scanning/index.md#supported-languages-and-package-managers) +supported by Gemnasium. + +``` +GET /projects/:id/dependencies +GET /projects/:id/vulnerabilities?package_manger=maven +GET /projects/:id/vulnerabilities?package_manger=yarn,bundler +``` + +| Attribute | Type | Required | Description | +| ------------- | -------------- | -------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------| +| `id` | integer/string | yes | The ID or [URL-encoded path of the project](README.md#namespaced-path-encoding). | +| `package_manager` | string array | no | Returns dependencies belonging to specified package manager. Valid values: `bundler`, `composer`, `maven`, `npm`, `pip` or `yarn`. | + +```bash +curl --header "PRIVATE-TOKEN: <your_access_token>" https://gitlab.example.com/api/v4/projects/4/dependencies +``` + +Example response: + +```json +[ + { + "name": "rails", + "version": "5.0.1", + "package_manager": "bundler", + "dependency_file_path": "Gemfile.lock" + }, + { + "name": "hanami", + "version": "1.3.1", + "package_manager": "bundler", + "dependency_file_path": "Gemfile.lock" + } +] +``` diff --git a/doc/api/jobs.md b/doc/api/jobs.md index 1add5f432ac..0e45ee1a583 100644 --- a/doc/api/jobs.md +++ b/doc/api/jobs.md @@ -409,10 +409,10 @@ Possible response status codes: > - The use of `CI_JOB_TOKEN` in the artifacts download API was [introduced][ee-2346] > in [GitLab Premium][ee] 9.5. -Download the artifacts zipped archive from the given reference name and job, -provided the job finished successfully. This is the same as -[getting the job's artifacts](#get-job-artifacts), but by defining the job's -name instead of its ID. +Download the artifacts zipped archive from the latest successful pipeline for +the given reference name and job, provided the job finished successfully. This +is the same as [getting the job's artifacts](#get-job-artifacts), but by +defining the job's name instead of its ID. ``` GET /projects/:id/jobs/artifacts/:ref_name/download?job=name @@ -506,9 +506,9 @@ Possible response status codes: > [Introduced](https://gitlab.com/gitlab-org/gitlab-ce/merge_requests/23538) in GitLab 11.5. -Download a single artifact file from a specific tag or branch from within the -job's artifacts archive. The file is extracted from the archive and streamed to -the client. +Download a single artifact file for a specific job of the latest successful +pipeline for the given reference name from within the job's artifacts archive. +The file is extracted from the archive and streamed to the client. ``` GET /projects/:id/jobs/artifacts/:ref_name/raw/*artifact_path?job=name diff --git a/doc/api/merge_requests.md b/doc/api/merge_requests.md index 662a4b3e424..1ade46efb1c 100644 --- a/doc/api/merge_requests.md +++ b/doc/api/merge_requests.md @@ -44,7 +44,7 @@ Parameters: | `scope` | string | no | Return merge requests for the given scope: `created_by_me`, `assigned_to_me` or `all`. Defaults to `created_by_me`<br> For versions before 11.0, use the now deprecated `created-by-me` or `assigned-to-me` scopes instead. | | `author_id` | integer | no | Returns merge requests created by the given user `id`. Combine with `scope=all` or `scope=assigned_to_me` | | `assignee_id` | integer | no | Returns merge requests assigned to the given user `id`. `None` returns unassigned merge requests. `Any` returns merge requests with an assignee. | -| `approver_ids` **(STARTER)** | Array[integer] | no | Returns merge requests which have specified all the users with the given `id`s as individual approvers. `None` returns merge requests without approvers. `Any` returns merge requests with an approver. | +| `approver_ids` **(STARTER)** | integer array | no | Returns merge requests which have specified all the users with the given `id`s as individual approvers. `None` returns merge requests without approvers. `Any` returns merge requests with an approver. | | `my_reaction_emoji` | string | no | Return merge requests reacted by the authenticated user by the given `emoji`. `None` returns issues not given a reaction. `Any` returns issues given at least one reaction. _([Introduced][ce-14016] in GitLab 10.0)_ | | `source_branch` | string | no | Return merge requests with the given source branch | | `target_branch` | string | no | Return merge requests with the given target branch | @@ -206,7 +206,7 @@ Parameters: | `scope` | string | no | Return merge requests for the given scope: `created_by_me`, `assigned_to_me` or `all`.<br> For versions before 11.0, use the now deprecated `created-by-me` or `assigned-to-me` scopes instead.<br> _([Introduced][ce-13060] in GitLab 9.5. [Changed to snake_case][ce-18935] in GitLab 11.0)_ | | `author_id` | integer | no | Returns merge requests created by the given user `id` _([Introduced][ce-13060] in GitLab 9.5)_ | | `assignee_id` | integer | no | Returns merge requests assigned to the given user `id`. `None` returns unassigned merge requests. `Any` returns merge requests with an assignee. _([Introduced][ce-13060] in GitLab 9.5)_ | -| `approver_ids` **(STARTER)** | Array[integer] | no | Returns merge requests which have specified all the users with the given `id`s as individual approvers. `None` returns merge requests without approvers. `Any` returns merge requests with an approver. | +| `approver_ids` **(STARTER)** | integer array | no | Returns merge requests which have specified all the users with the given `id`s as individual approvers. `None` returns merge requests without approvers. `Any` returns merge requests with an approver. | | `my_reaction_emoji` | string | no | Return merge requests reacted by the authenticated user by the given `emoji`. `None` returns issues not given a reaction. `Any` returns issues given at least one reaction. _([Introduced][ce-14016] in GitLab 10.0)_ | | `source_branch` | string | no | Return merge requests with the given source branch | | `target_branch` | string | no | Return merge requests with the given target branch | @@ -358,7 +358,7 @@ Parameters: | `scope` | string | no | Return merge requests for the given scope: `created_by_me`, `assigned_to_me` or `all`.<br> | | `author_id` | integer | no | Returns merge requests created by the given user `id` _([Introduced][ce-13060] in GitLab 9.5)_ | | `assignee_id` | integer | no | Returns merge requests assigned to the given user `id`. `None` returns unassigned merge requests. `Any` returns merge requests with an assignee. _([Introduced][ce-13060] in GitLab 9.5)_ | -| `approver_ids` **(STARTER)** | Array[integer] | no | Returns merge requests which have specified all the users with the given `id`s as individual approvers. `None` returns merge requests without approvers. `Any` returns merge requests with an approver. | +| `approver_ids` **(STARTER)** | integer array | no | Returns merge requests which have specified all the users with the given `id`s as individual approvers. `None` returns merge requests without approvers. `Any` returns merge requests with an approver. | | `my_reaction_emoji` | string | no | Return merge requests reacted by the authenticated user by the given `emoji`. `None` returns issues not given a reaction. `Any` returns issues given at least one reaction. _([Introduced][ce-14016] in GitLab 10.0)_ | | `source_branch` | string | no | Return merge requests with the given source branch | | `target_branch` | string | no | Return merge requests with the given target branch | diff --git a/doc/api/releases/img/upcoming_release_v12_1.png b/doc/api/releases/img/upcoming_release_v12_1.png Binary files differnew file mode 100644 index 00000000000..8bd8573ce84 --- /dev/null +++ b/doc/api/releases/img/upcoming_release_v12_1.png diff --git a/doc/api/releases/index.md b/doc/api/releases/index.md index e7f79a0d359..e74b35fd959 100644 --- a/doc/api/releases/index.md +++ b/doc/api/releases/index.md @@ -6,7 +6,7 @@ ## List Releases -Paginated list of Releases, sorted by `created_at`. +Paginated list of Releases, sorted by `released_at`. ``` GET /projects/:id/releases @@ -32,6 +32,7 @@ Example response: "name":"Awesome app v0.2 beta", "description_html":"\u003ch2 dir=\"auto\"\u003e\n\u003ca id=\"user-content-changelog\" class=\"anchor\" href=\"#changelog\" aria-hidden=\"true\"\u003e\u003c/a\u003eCHANGELOG\u003c/h2\u003e\n\u003cul dir=\"auto\"\u003e\n\u003cli\u003eEscape label and milestone titles to prevent XSS in GFM autocomplete. !2740\u003c/li\u003e\n\u003cli\u003ePrevent private snippets from being embeddable.\u003c/li\u003e\n\u003cli\u003eAdd subresources removal to member destroy service.\u003c/li\u003e\n\u003c/ul\u003e", "created_at":"2019-01-03T01:56:19.539Z", + "released_at":"2019-01-03T01:56:19.539Z", "author":{ "id":1, "name":"Administrator", @@ -98,6 +99,7 @@ Example response: "name":"Awesome app v0.1 alpha", "description_html":"\u003ch2 dir=\"auto\"\u003e\n\u003ca id=\"user-content-changelog\" class=\"anchor\" href=\"#changelog\" aria-hidden=\"true\"\u003e\u003c/a\u003eCHANGELOG\u003c/h2\u003e\n\u003cul dir=\"auto\"\u003e\n\u003cli\u003eRemove limit of 100 when searching repository code. !8671\u003c/li\u003e\n\u003cli\u003eShow error message when attempting to reopen an MR and there is an open MR for the same branch. !16447 (Akos Gyimesi)\u003c/li\u003e\n\u003cli\u003eFix a bug where internal email pattern wasn't respected. !22516\u003c/li\u003e\n\u003c/ul\u003e", "created_at":"2019-01-03T01:55:18.203Z", + "released_at":"2019-01-03T01:55:18.203Z", "author":{ "id":1, "name":"Administrator", @@ -178,6 +180,7 @@ Example response: "name":"Awesome app v0.1 alpha", "description_html":"\u003ch2 dir=\"auto\"\u003e\n\u003ca id=\"user-content-changelog\" class=\"anchor\" href=\"#changelog\" aria-hidden=\"true\"\u003e\u003c/a\u003eCHANGELOG\u003c/h2\u003e\n\u003cul dir=\"auto\"\u003e\n\u003cli\u003eRemove limit of 100 when searching repository code. !8671\u003c/li\u003e\n\u003cli\u003eShow error message when attempting to reopen an MR and there is an open MR for the same branch. !16447 (Akos Gyimesi)\u003c/li\u003e\n\u003cli\u003eFix a bug where internal email pattern wasn't respected. !22516\u003c/li\u003e\n\u003c/ul\u003e", "created_at":"2019-01-03T01:55:18.203Z", + "released_at":"2019-01-03T01:55:18.203Z", "author":{ "id":1, "name":"Administrator", @@ -247,6 +250,7 @@ POST /projects/:id/releases | `assets:links`| array of hash | no | An array of assets links. | | `assets:links:name`| string | no (if `assets:links` specified, it's required) | The name of the link. | | `assets:links:url`| string | no (if `assets:links` specified, it's required) | The url of the link. | +| `released_at` | datetime | no | The date when the release will be/was ready. Defaults to the current time. Expected in ISO 8601 format (`2019-03-15T08:00:00Z`). | Example request: @@ -265,6 +269,7 @@ Example response: "name":"New release", "description_html":"\u003cp dir=\"auto\"\u003eSuper nice release\u003c/p\u003e", "created_at":"2019-01-03T02:22:45.118Z", + "released_at":"2019-01-03T02:22:45.118Z", "author":{ "id":1, "name":"Administrator", @@ -335,6 +340,7 @@ PUT /projects/:id/releases/:tag_name | `tag_name` | string | yes | The tag where the release will be created from. | | `name` | string | no | The release name. | | `description` | string | no | The description of the release. You can use [markdown](../../user/markdown.md). | +| `released_at` | datetime | no | The date when the release will be/was ready. Expected in ISO 8601 format (`2019-03-15T08:00:00Z`). | Example request: @@ -351,6 +357,7 @@ Example response: "name":"new name", "description_html":"\u003ch2 dir=\"auto\"\u003e\n\u003ca id=\"user-content-changelog\" class=\"anchor\" href=\"#changelog\" aria-hidden=\"true\"\u003e\u003c/a\u003eCHANGELOG\u003c/h2\u003e\n\u003cul dir=\"auto\"\u003e\n\u003cli\u003eRemove limit of 100 when searching repository code. !8671\u003c/li\u003e\n\u003cli\u003eShow error message when attempting to reopen an MR and there is an open MR for the same branch. !16447 (Akos Gyimesi)\u003c/li\u003e\n\u003cli\u003eFix a bug where internal email pattern wasn't respected. !22516\u003c/li\u003e\n\u003c/ul\u003e", "created_at":"2019-01-03T01:55:18.203Z", + "released_at":"2019-01-03T01:55:18.203Z", "author":{ "id":1, "name":"Administrator", @@ -430,6 +437,7 @@ Example response: "name":"new name", "description_html":"\u003ch2 dir=\"auto\"\u003e\n\u003ca id=\"user-content-changelog\" class=\"anchor\" href=\"#changelog\" aria-hidden=\"true\"\u003e\u003c/a\u003eCHANGELOG\u003c/h2\u003e\n\u003cul dir=\"auto\"\u003e\n\u003cli\u003eRemove limit of 100 when searching repository code. !8671\u003c/li\u003e\n\u003cli\u003eShow error message when attempting to reopen an MR and there is an open MR for the same branch. !16447 (Akos Gyimesi)\u003c/li\u003e\n\u003cli\u003eFix a bug where internal email pattern wasn't respected. !22516\u003c/li\u003e\n\u003c/ul\u003e", "created_at":"2019-01-03T01:55:18.203Z", + "released_at":"2019-01-03T01:55:18.203Z", "author":{ "id":1, "name":"Administrator", @@ -480,3 +488,11 @@ Example response: } } ``` + +## Upcoming Releases + +> [Introduced](https://gitlab.com/gitlab-org/gitlab-ce/issues/38105) in GitLab 12.1. + +A release with a `released_at` attribute set to a future date will be labeled an **Upcoming Release** in the UI: + +![Upcoming release](img/upcoming_release_v12_1.png) diff --git a/doc/api/users.md b/doc/api/users.md index 54641f4c862..fdc84826680 100644 --- a/doc/api/users.md +++ b/doc/api/users.md @@ -147,6 +147,21 @@ GET /users ] ``` +Users on GitLab [Starter, Bronze, or higher](https://about.gitlab.com/pricing/) will also see the `shared_runners_minutes_limit`, `extra_shared_runners_minutes_limit`, and `note` parameters. + +```json +[ + { + "id": 1, + ... + "shared_runners_minutes_limit": 133, + "extra_shared_runners_minutes_limit": 133, + "note": "DMCA Request: 2018-11-05 | DMCA Violation | Abuse | https://gitlab.zendesk.com/agent/tickets/123" + ... + } +] +``` + Users on GitLab [Silver or higher](https://about.gitlab.com/pricing/) will also see the `group_saml` provider option: @@ -284,14 +299,15 @@ Example Responses: ``` Users on GitLab [Starter, Bronze, or higher](https://about.gitlab.com/pricing/) will also see -the `shared_runners_minutes_limit` and `extra_shared_runners_minutes_limit` parameters. +the `shared_runners_minutes_limit`, `extra_shared_runners_minutes_limit`, and `note` parameters. ```json { "id": 1, "username": "john_smith", "shared_runners_minutes_limit": 133, - "extra_shared_runners_minutes_limit": 133 + "extra_shared_runners_minutes_limit": 133, + "note": "DMCA Request: 2018-11-05 | DMCA Violation | Abuse | https://gitlab.zendesk.com/agent/tickets/123" ... } ``` @@ -304,7 +320,8 @@ see the `group_saml` option: "id": 1, "username": "john_smith", "shared_runners_minutes_limit": 133, - "extra_shared_runners_minutes_limit": 133 + "extra_shared_runners_minutes_limit": 133, + "note": "DMCA Request: 2018-11-05 | DMCA Violation | Abuse | https://gitlab.zendesk.com/agent/tickets/123" "identities": [ {"provider": "github", "extern_uid": "2435223452345"}, {"provider": "bitbucket", "extern_uid": "john.smith"}, @@ -399,6 +416,7 @@ Parameters: - `private_profile` (optional) - User's profile is private - true or false (default) - `shared_runners_minutes_limit` (optional) - Pipeline minutes quota for this user **(STARTER)** - `extra_shared_runners_minutes_limit` (optional) - Extra pipeline minutes quota for this user **(STARTER)** +- `note` (optional) - Admin notes for this user **(STARTER)** On password update, user will be forced to change it upon next login. Note, at the moment this method does only return a `404` error, diff --git a/doc/ci/docker/using_docker_images.md b/doc/ci/docker/using_docker_images.md index 90565efe196..f3896c5232c 100644 --- a/doc/ci/docker/using_docker_images.md +++ b/doc/ci/docker/using_docker_images.md @@ -35,8 +35,8 @@ sudo gitlab-runner register \ --description "docker-ruby-2.1" \ --executor "docker" \ --docker-image ruby:2.1 \ - --docker-postgres latest \ - --docker-mysql latest + --docker-services postgres:latest \ + --docker-services mysql:latest ``` The registered runner will use the `ruby:2.1` Docker image and will run two @@ -193,13 +193,14 @@ You can simply define an image that will be used for all jobs and a list of services that you want to use during build time: ```yaml -image: ruby:2.2 +default: + image: ruby:2.2 -services: - - postgres:9.3 + services: + - postgres:9.3 -before_script: - - bundle install + before_script: + - bundle install test: script: @@ -209,8 +210,9 @@ test: It is also possible to define different images and services per job: ```yaml -before_script: - - bundle install +default: + before_script: + - bundle install test:2.1: image: ruby:2.1 @@ -231,18 +233,19 @@ Or you can pass some [extended configuration options](#extended-docker-configura for `image` and `services`: ```yaml -image: - name: ruby:2.2 - entrypoint: ["/bin/bash"] +default: + image: + name: ruby:2.2 + entrypoint: ["/bin/bash"] -services: -- name: my-postgres:9.4 - alias: db-postgres - entrypoint: ["/usr/local/bin/db-postgres"] - command: ["start"] + services: + - name: my-postgres:9.4 + alias: db-postgres + entrypoint: ["/usr/local/bin/db-postgres"] + command: ["start"] -before_script: -- bundle install + before_script: + - bundle install test: script: diff --git a/doc/ci/examples/README.md b/doc/ci/examples/README.md index 5a302392c54..9295dcfd4e0 100644 --- a/doc/ci/examples/README.md +++ b/doc/ci/examples/README.md @@ -37,6 +37,7 @@ The following table lists examples with step-by-step tutorials that are containe | Python on Heroku | [Test and deploy a Python application with GitLab CI/CD](test-and-deploy-python-application-to-heroku.md). | | Ruby on Heroku | [Test and deploy a Ruby application with GitLab CI/CD](test-and-deploy-ruby-application-to-heroku.md). | | Scala on Heroku | [Test and deploy a Scala application to Heroku](test-scala-application.md). | +| Parallel testing Ruby & JS | [GitLab CI parallel jobs testing for Ruby & JavaScript projects](https://docs.knapsackpro.com/2019/how-to-run-parallel-jobs-for-rspec-tests-on-gitlab-ci-pipeline-and-speed-up-ruby-javascript-testing). | ### Contributing examples diff --git a/doc/ci/yaml/README.md b/doc/ci/yaml/README.md index c2ef58acf15..001f951ebb8 100644 --- a/doc/ci/yaml/README.md +++ b/doc/ci/yaml/README.md @@ -119,6 +119,35 @@ The following table lists available parameters for jobs: NOTE: **Note:** Parameters `types` and `type` are [deprecated](#deprecated-parameters). +## Setting default parameters + +Some parameters can be set globally as the default for all jobs using the +`default:` keyword. Default parameters can then be overridden by job-specific +configuration. + +The following job parameters can be defined inside a `default:` block: + +- [`image`](#image) +- [`services`](#services) +- [`before_script`](#before_script-and-after_script) +- [`after_script`](#before_script-and-after_script) +- [`cache`](#cache) + +In the following example, the `ruby:2.5` image is set as the default for all +jobs except the `rspec 2.6` job, which uses the `ruby:2.6` image: + +```yaml +default: + image: ruby:2.5 + +rspec: + script: bundle exec rspec + +rspec 2.6: + image: ruby:2.6 + script: bundle exec rspec +``` + ## Parameter details The following are detailed explanations for parameters used to configure CI/CD pipelines. @@ -239,8 +268,9 @@ It's possible to overwrite the globally defined `before_script` and `after_scrip if you set it per-job: ```yaml -before_script: - - global before script +default: + before_script: + - global before script job: before_script: @@ -974,7 +1004,7 @@ review_app: stop_review_app: stage: deploy variables: - GIT_STRATEGY: none + GIT_STRATEGY: none script: make delete-app when: manual environment: @@ -1749,6 +1779,10 @@ test: parallel: 5 ``` +TIP: **Tip:** +Parallelize tests suites across parallel jobs. +Different languages have different tools to facilitate this. + ### `trigger` **(PREMIUM)** > [Introduced](https://gitlab.com/gitlab-org/gitlab-ee/issues/8997) in [GitLab Premium](https://about.gitlab.com/pricing/) 11.8. @@ -2221,10 +2255,10 @@ spinach: script: rake spinach ``` -It's also possible to use multiple parents for `extends`. -The algorithm used for merge is "closest scope wins", so keys -from the last member will always shadow anything defined on other levels. -For example: +In GitLab 12.0 and later, it's also possible to use multiple parents for +`extends`. The algorithm used for merge is "closest scope wins", so +keys from the last member will always shadow anything defined on other +levels. For example: ```yaml .only-important: @@ -2550,18 +2584,39 @@ You can set it globally or per-job in the [`variables`](#variables) section. The following parameters are deprecated. -### `types` +### Globally-defined `types` CAUTION: **Deprecated:** `types` is deprecated, and could be removed in a future release. Use [`stages`](#stages) instead. -### `type` +### Job-defined `type` CAUTION: **Deprecated:** `type` is deprecated, and could be removed in one of the future releases. Use [`stage`](#stage) instead. +### Globally-defined `image`, `services`, `cache`, `before_script`, `after_script` + +Defining `image`, `services`, `cache`, `before_script`, and +`after_script` globally is deprecated. Support could be removed +from a future release. + +Use [`default:`](#setting-default-parameters) instead. For example: + +```yaml +default: + image: ruby:2.5 + services: + - docker:dind + cache: + paths: [vendor/] + before_script: + - bundle install --path vendor/ + after_script: + - rm -rf tmp/ +``` + ## Custom build directories > [Introduced](https://gitlab.com/gitlab-org/gitlab-runner/merge_requests/1267) in Gitlab Runner 11.10 @@ -2644,7 +2699,7 @@ variables: The value of `GIT_CLONE_PATH` is expanded once into `$CI_BUILDS_DIR/go/src/namespace/project`, and results in failure -because `$CI_BUILDS_DIR` is not expanded. +because `$CI_BUILDS_DIR` is not expanded. ## Special YAML features diff --git a/doc/development/fe_guide/event_tracking.md b/doc/development/fe_guide/event_tracking.md index 6ab3fa4acf3..716f6ad7f92 100644 --- a/doc/development/fe_guide/event_tracking.md +++ b/doc/development/fe_guide/event_tracking.md @@ -47,7 +47,7 @@ There's a more convenient solution to this problem. When working with HAML templ Below is an example of `data-track-*` attributes assigned to a button in HAML: ```ruby -%button.btn{ data: { track_label: "create_from_template", track_property: "template_preview", track_event: "click_button", track_value: "my-template" } } +%button.btn{ data: { track_label: "template_preview", track_property: "my-template", track_event: "click_button", track_value: "" } } ``` By calling `bindTrackableContainer('.my-container')`, click handlers get bound to all elements located in `.my-container` provided that they have the necessary `data-track-*` attributes assigned to them. diff --git a/doc/development/testing_guide/end_to_end/page_objects.md b/doc/development/testing_guide/end_to_end/page_objects.md index 05cb03eb4bd..29ad49403fe 100644 --- a/doc/development/testing_guide/end_to_end/page_objects.md +++ b/doc/development/testing_guide/end_to_end/page_objects.md @@ -92,20 +92,25 @@ end The `view` DSL method will correspond to the rails View, partial, or vue component that renders the elements. The `element` DSL method in turn declares an element for which a corresponding -`qa-element-name-dasherized` CSS class will need to be added to the view file. +`data-qa-selector=element_name_snaked` data attribute will need to be added to the view file. You can also define a value (String or Regexp) to match to the actual view code but **this is deprecated** in favor of the above method for two reasons: - Consistency: there is only one way to define an element -- Separation of concerns: QA uses dedicated CSS classes instead of reusing code +- Separation of concerns: QA uses dedicated `data-qa-*` attributes instead of reusing code or classes used by other components (e.g. `js-*` classes etc.) ```ruby view 'app/views/my/view.html.haml' do - # Implicitly require `.qa-logout-button` CSS class to be present in the view + + ### Good ### + + # Implicitly require the CSS selector `[data-qa-selector="logout_button"]` to be present in the view element :logout_button + ### Bad ### + ## This is deprecated and forbidden by the `QA/ElementWithPattern` RuboCop cop. # Require `f.submit "Sign in"` to be present in `my/view.html.haml element :my_button, 'f.submit "Sign in"' # rubocop:disable QA/ElementWithPattern @@ -129,24 +134,39 @@ view 'app/views/my/view.html.haml' do end ``` -To add these elements to the view, you must change the rails View, partial, or vue component by adding a `qa-element-descriptor` class +To add these elements to the view, you must change the rails View, partial, or vue component by adding a `data-qa-selector` attribute for each element defined. -In our case, `qa-login-field`, `qa-password-field` and `qa-sign-in-button` +In our case, `data-qa-selector="login_field"`, `data-qa-selector="password_field"` and `data-qa-selector="sign_in_button"` **app/views/my/view.html.haml** ```haml -= f.text_field :login, class: "form-control top qa-login-field", autofocus: "autofocus", autocapitalize: "off", autocorrect: "off", required: true, title: "This field is required." -= f.password_field :password, class: "form-control bottom qa-password-field", required: true, title: "This field is required." -= f.submit "Sign in", class: "btn btn-success qa-sign-in-button" += f.text_field :login, class: "form-control top", autofocus: "autofocus", autocapitalize: "off", autocorrect: "off", required: true, title: "This field is required.", data: { qa_selector: 'login_field' } += f.password_field :password, class: "form-control bottom", required: true, title: "This field is required.", data: { qa_selector: 'password_field' } += f.submit "Sign in", class: "btn btn-success", data: { qa_selector: 'sign_in_button' } ``` Things to note: -- The CSS class must be `kebab-cased` (separated with hyphens "`-`") +- The name of the element and the qa_selector must match and be snake_cased - If the element appears on the page unconditionally, add `required: true` to the element. See [Dynamic element validation](dynamic_element_validation.md) +- You may see `.qa-selector` classes in existing Page Objects. We should prefer the [`data-qa-selector`](#data-qa-selector-vs-qa-selector) + method of definition over the `.qa-selector` CSS class + + +### `data-qa-selector` vs `.qa-selector` + +> Introduced in GitLab 12.1 + +There are two supported methods of defining elements within a view. + +1. `data-qa-selector` attribute +1. `.qa-selector` class + +Any existing `.qa-selector` class should be considered deprecated +and we should prefer the `data-qa-selector` method of definition. ## Running the test locally diff --git a/doc/development/testing_guide/end_to_end/quick_start_guide.md b/doc/development/testing_guide/end_to_end/quick_start_guide.md index efcfd44bc22..3bbf8feab39 100644 --- a/doc/development/testing_guide/end_to_end/quick_start_guide.md +++ b/doc/development/testing_guide/end_to_end/quick_start_guide.md @@ -101,7 +101,7 @@ it 'replaces an existing label if it has the same key' do page.find('#content-body').click page.refresh - labels_block = page.find('.qa-labels-block') + labels_block = page.find(%q([data-qa-selector="labels_block"])) expect(labels_block).to have_content('animal::dolphin') expect(labels_block).not_to have_content('animal::fox') @@ -130,7 +130,7 @@ it 'keeps both scoped labels when adding a label with a different key' do page.find('#content-body').click page.refresh - labels_block = page.find('.qa-labels-block') + labels_block = page.find(%q([data-qa-selector="labels_block"])) expect(labels_block).to have_content('animal::fox') expect(labels_block).to have_content('plant::orchid') @@ -139,7 +139,7 @@ it 'keeps both scoped labels when adding a label with a different key' do end ``` -> Note that elements are always located using CSS selectors, and a good practice is to add test-specific selectors (this is called adding testability to the application and we will talk more about it later.) For example, the `labels_block` element uses the selector `.qa-labels-block`, which was added specifically for testing purposes. +> Note that elements are always located using CSS selectors, and a good practice is to add test-specific selectors (this is called "testability"). For example, the `labels_block` element uses the CSS selector [`data-qa-selector="labels_block"`](page_objects.md#data-qa-selector-vs-qa-selector), which was added specifically for testing purposes. Below are the steps that the test covers: @@ -168,7 +168,7 @@ end it 'replaces an existing label if it has the same key' do select_label_and_refresh @new_label_same_scope - labels_block = page.find('.qa-labels-block') + labels_block = page.find(%q([data-qa-selector="labels_block"])) expect(labels_block).to have_content(@new_label_same_scope) expect(labels_block).not_to have_content(@initial_label) @@ -179,7 +179,7 @@ end it 'keeps both scoped label when adding a label with a different key' do select_label_and_refresh @new_label_different_scope - labels_block = page.find('.qa-labels-block') + labels_block = page.find(%q([data-qa-selector="labels_block"])) expect(labels_blocks).to have_content(@new_label_different_scope) expect(labels_blocks).to have_content(@initial_label) @@ -305,7 +305,7 @@ module QA it 'correctly applies scoped labels depending on if they are from the same or a different scope' do select_labels_and_refresh [@new_label_same_scope, @new_label_different_scope] - labels_block = page.all('.qa-labels-block') + labels_block = page.all(%q([data-qa-selector="labels_block"])) expect(labels_block).to have_content(@new_label_same_scope) expect(labels_block).to have_content(@new_label_different_scope) @@ -552,37 +552,36 @@ The `text_of_labels_block` method is a simple method that returns the `:labels_b #### Updates in the view (*.html.haml) and `dropdowns_helper.rb` files -Now let's change the view and the `dropdowns_helper` files to add the selectors that relate to the Page Object. +Now let's change the view and the `dropdowns_helper` files to add the selectors that relate to the [Page Objects]. -In the [app/views/shared/issuable/_sidebar.html.haml](https://gitlab.com/gitlab-org/gitlab-ee/blob/master/app/views/shared/issuable/_sidebar.html.haml) file, on [line 105 ](https://gitlab.com/gitlab-org/gitlab-ee/blob/84043fa72ca7f83ae9cde48ad670e6d5d16501a3/app/views/shared/issuable/_sidebar.html.haml#L105), add an extra class `qa-edit-link-labels`. +In [`app/views/shared/issuable/_sidebar.html.haml:105`](https://gitlab.com/gitlab-org/gitlab-ee/blob/7ca12defc7a965987b162a6ebef302f95dc8867f/app/views/shared/issuable/_sidebar.html.haml#L105), add a `data: { qa_selector: 'edit_link_labels' }` data attribute. The code should look like this: ```haml -= link_to _('Edit'), '#', class: 'js-sidebar-dropdown-toggle edit-link float-right qa-edit-link-labels' += link_to _('Edit'), '#', class: 'js-sidebar-dropdown-toggle edit-link float-right', data: { qa_selector: 'edit_link_labels' } ``` -In the same file, on [line 121](https://gitlab.com/gitlab-org/gitlab-ee/blob/84043fa72ca7f83ae9cde48ad670e6d5d16501a3/app/views/shared/issuable/_sidebar.html.haml#L121), add an extra class `.qa-dropdown-menu-labels`. +In the same file, on [line 121](https://gitlab.com/gitlab-org/gitlab-ee/blob/7ca12defc7a965987b162a6ebef302f95dc8867f/app/views/shared/issuable/_sidebar.html.haml#L121), add a `data: { qa_selector: 'dropdown_menu_labels' }` data attribute. The code should look like this: ```haml -.dropdown-menu.dropdown-select.dropdown-menu-paging.dropdown-menu-labels.dropdown-menu-selectable.qa-dropdown-menu-labels +.dropdown-menu.dropdown-select.dropdown-menu-paging.dropdown-menu-labels.dropdown-menu-selectable.dropdown-extended-height{ data: { qa_selector: 'dropdown_menu_labels' } } ``` -In the [`dropdowns_helper.rb`](https://gitlab.com/gitlab-org/gitlab-ee/blob/master/app/helpers/dropdowns_helper.rb) file, on [line 94](https://gitlab.com/gitlab-org/gitlab-ee/blob/99e51a374f2c20bee0989cac802e4b5621f72714/app/helpers/dropdowns_helper.rb#L94), add an extra class `qa-dropdown-input-field`. +In [`app/helpers/dropdowns_helper.rb:94`](https://gitlab.com/gitlab-org/gitlab-ee/blob/7ca12defc7a965987b162a6ebef302f95dc8867f/app/helpers/dropdowns_helper.rb#L94), add a `data: { qa_selector: 'dropdown_input_field' }` data attribute. The code should look like this: ```ruby -filter_output = search_field_tag search_id, nil, class: "dropdown-input-field qa-dropdown-input-field", placeholder: placeholder, autocomplete: 'off' +filter_output = search_field_tag search_id, nil, class: "dropdown-input-field", placeholder: placeholder, autocomplete: 'off', data: { qa_selector: 'dropdown_input_field' } ``` -> Classes starting with `qa-` are used for testing purposes only, and by defining such classes in the elements we add **testability** in the application. +> `data-qa-*` data attributes and CSS classes starting with `qa-` are used solely for the purpose of QA and testing. +> By defining these, we add **testability** to the application. -> When defining a class like `qa-labels-block`, it is transformed into `:labels_block` for usage in the Page Objects. So, `qa-edit-link-labels` is transformed into `:edit_link_labels`, `qa-dropdown-menu-labels` is transformed into `:dropdown_menu_labels`, and `qa-dropdown-input-field` is transformed into `:dropdown_input_field`. Also, we use a [sanity test](https://gitlab.com/gitlab-org/gitlab-ce/tree/master/qa/qa/page#how-did-we-solve-fragile-tests-problem) to check that defined elements have their respective `qa-` selectors in the specified views. - -> We did not define the `qa-labels-block` class in the `app/views/shared/issuable/_sidebar.html.haml` file because it was already there to be used. +> When defining a data attribute like: `qa_selector: 'labels_block'`, it should match the element definition: `element :labels_block`. We use a [sanity test](https://gitlab.com/gitlab-org/gitlab-ce/tree/master/qa/qa/page#how-did-we-solve-fragile-tests-problem) to check that defined elements have their respective selectors in the specified views. #### Updates in the `QA::Page::Base` class diff --git a/doc/development/testing_guide/frontend_testing.md b/doc/development/testing_guide/frontend_testing.md index bb44cc595e9..c909745b1ab 100644 --- a/doc/development/testing_guide/frontend_testing.md +++ b/doc/development/testing_guide/frontend_testing.md @@ -79,6 +79,34 @@ describe('Component', () => { Remember that the performance of each test depends on the environment. +### Manual module mocks +Jest supports [manual module mocks](https://jestjs.io/docs/en/manual-mocks) by placing a mock in a `__mocks__/` directory next to the source module. **Don't do this.** We want to keep all of our test-related code in one place (the `spec/` folder), and the logic that Jest uses to apply mocks from `__mocks__/` is rather inconsistent. + +Instead, our test runner detects manual mocks from `spec/frontend/mocks/`. Any mock placed here is automatically picked up and injected whenever you import its source module. + +- Files in `spec/frontend/mocks/ce` will mock the corresponding CE module from `app/assets/javascripts`, mirroring the source module's path. + - Example: `spec/frontend/mocks/ce/lib/utils/axios_utils` will mock the module `~/lib/utils/axios_utils`. +- Files in `spec/frontend/mocks/node` will mock NPM packages of the same name or path. +- We don't support mocking EE modules yet. + +If a mock is found for which a source module doesn't exist, the test suite will fail. 'Virtual' mocks, or mocks that don't have a 1-to-1 association with a source module, are not supported yet. + +#### Writing a mock +Create a JS module in the appropriate place in `spec/frontend/mocks/`. That's it. It will automatically mock its source package in all tests. + +Make sure that your mock's export has the same format as the mocked module. So, if you're mocking a CommonJS module, you'll need to use `module.exports` instead of the ES6 `export`. + +It might be useful for a mock to expose a property that indicates if the mock was loaded. This way, tests can assert the presence of a mock without calling any logic and causing side-effects. The `~/lib/utils/axios_utils` module mock has such a property, `isMock`, that is `true` in the mock and undefined in the original class. Jest's mock functions also have a `mock` property that you can test. + +#### Bypassing mocks +If you ever need to import the original module in your tests, use [`jest.requireActual()`](https://jestjs.io/docs/en/jest-object#jestrequireactualmodulename) (or `jest.requireActual().default` for the default export). The `jest.mock()` and `jest.unmock()` won't have an effect on modules that have a manual mock, because mocks are imported and cached before any tests are run. + +#### Keep mocks light +Global mocks introduce magic and can affect how modules are imported in your tests. Try to keep them as light as possible and dependency-free. A global mock should be useful for any unit test. For example, the `axios_utils` and `jquery` module mocks throw an error when an HTTP request is attempted, since this is useful behaviour in >99% of tests. + +When in doubt, construct mocks in your test file using [`jest.mock()`](https://jestjs.io/docs/en/jest-object#jestmockmodulename-factory-options), [`jest.spyOn()`](https://jestjs.io/docs/en/jest-object#jestspyonobject-methodname), etc. + + ## Karma test suite GitLab uses the [Karma][karma] test runner with [Jasmine] as its test diff --git a/doc/integration/elasticsearch.md b/doc/integration/elasticsearch.md index fff06254da7..20caac4cb74 100644 --- a/doc/integration/elasticsearch.md +++ b/doc/integration/elasticsearch.md @@ -42,7 +42,11 @@ use the packages that are available for your OS. In order to improve elasticsearch indexing performance, GitLab has made available a [new indexer written in Go](https://gitlab.com/gitlab-org/gitlab-elasticsearch-indexer). This will replace the included Ruby indexer in the future but should be considered beta software for now, so there may be some bugs. -If you would like to use it, please follow the instructions below. +The Elasticsearch Go indexer is included in Omnibus for GitLab 11.8 and newer. + +To use the new Elasticsearch indexer included in Omnibus, check the box "Use the new repository indexer (beta)" when [enabling the Elasticsearch integration](#enabling-elasticsearch). + +If you would like to use the Elasticsearch Go indexer with a source installation or an older version of GitLab, please follow the instructions below. ### Installation diff --git a/doc/security/information_exclusivity.md b/doc/security/information_exclusivity.md index 62a20d3f257..749ccf924b5 100644 --- a/doc/security/information_exclusivity.md +++ b/doc/security/information_exclusivity.md @@ -1,6 +1,7 @@ --- type: concepts --- + # Information exclusivity Git is a distributed version control system (DVCS). This means that everyone diff --git a/doc/security/password_length_limits.md b/doc/security/password_length_limits.md index d78293c75c6..9909ef4a8e4 100644 --- a/doc/security/password_length_limits.md +++ b/doc/security/password_length_limits.md @@ -1,19 +1,31 @@ --- type: reference, howto --- + # Custom password length limits -If you want to enforce longer user passwords you can create an extra Devise -initializer with the steps below. +The user password length is set to a minimum of 8 characters by default. +To change that for installations from source: + +1. Edit `devise_password_length.rb`: + + ```sh + cd /home/git/gitlab + sudo -u git -H cp config/initializers/devise_password_length.rb.example config/initializers/devise_password_length.rb + sudo -u git -H editor config/initializers/devise_password_length.rb + ``` + +1. Change the new password length limits: + + ```ruby + config.password_length = 12..128 + ``` -If you do not use the `devise_password_length.rb` initializer the password -length is set to a minimum of 8 characters in `config/initializers/devise.rb`. + In this example, the minimum length is 12 characters, and the maximum length + is 128 characters. -```bash -cd /home/git/gitlab -sudo -u git -H cp config/initializers/devise_password_length.rb.example config/initializers/devise_password_length.rb -sudo -u git -H editor config/initializers/devise_password_length.rb # inspect and edit the new password length limits -``` +1. [Restart GitLab](../administration/restart_gitlab.md#installations-from-source) + for the changes to take effect. <!-- ## Troubleshooting diff --git a/doc/security/rack_attack.md b/doc/security/rack_attack.md index 1b75798013d..1e5678ec47c 100644 --- a/doc/security/rack_attack.md +++ b/doc/security/rack_attack.md @@ -1,6 +1,7 @@ --- type: reference, howto --- + # Rack Attack [Rack Attack](https://github.com/kickstarter/rack-attack), also known as Rack::Attack, is a Ruby gem diff --git a/doc/security/reset_root_password.md b/doc/security/reset_root_password.md index a58d70f0ff2..6a6c5262179 100644 --- a/doc/security/reset_root_password.md +++ b/doc/security/reset_root_password.md @@ -1,6 +1,7 @@ --- type: howto --- + # How to reset your root password To reset your root password, first log into your server with root privileges. diff --git a/doc/security/ssh_keys_restrictions.md b/doc/security/ssh_keys_restrictions.md index ae4cc44519e..4c60daf77f4 100644 --- a/doc/security/ssh_keys_restrictions.md +++ b/doc/security/ssh_keys_restrictions.md @@ -1,6 +1,7 @@ --- type: reference, howto --- + # Restrict allowed SSH key technologies and minimum length `ssh-keygen` allows users to create RSA keys with as few as 768 bits, which diff --git a/doc/security/two_factor_authentication.md b/doc/security/two_factor_authentication.md index 6251f8e2f66..b08d9ffa26e 100644 --- a/doc/security/two_factor_authentication.md +++ b/doc/security/two_factor_authentication.md @@ -1,6 +1,7 @@ --- type: howto --- + # Enforce Two-factor Authentication (2FA) Two-factor Authentication (2FA) provides an additional level of security to your diff --git a/doc/security/unlock_user.md b/doc/security/unlock_user.md index 2e14e631d68..d34826c853c 100644 --- a/doc/security/unlock_user.md +++ b/doc/security/unlock_user.md @@ -2,37 +2,44 @@ type: howto --- -# How to unlock a locked user +# How to unlock a locked user from the command line -To unlock a locked user, first log into your server with root privileges. +After six failed login attempts a user gets in a locked state. -Start a Ruby on Rails console with this command: +To unlock a locked user: -```bash -gitlab-rails console production -``` +1. SSH into your GitLab server. +1. Start a Ruby on Rails console: -Wait until the console has loaded. + ```sh + ## For Omnibus GitLab + sudo gitlab-rails console production -There are multiple ways to find your user. You can search for email or username. + ## For installations from source + sudo -u git -H bundle exec rails console RAILS_ENV=production + ``` -```bash -user = User.where(id: 1).first -``` +1. Find the user to unlock. You can search by email or ID. -or + ```ruby + user = User.find_by(email: 'admin@local.host') + ``` -```bash -user = User.find_by(email: 'admin@local.host') -``` + or -Unlock the user: + ```ruby + user = User.where(id: 1).first + ``` -```bash -user.unlock_access! -``` +1. Unlock the user: -Exit the console, the user should now be able to log in again. + ```ruby + user.unlock_access! + ``` + +1. Exit the console with <kbd>Ctrl</kbd>+<kbd>d</kbd> + +The user should now be able to log in. <!-- ## Troubleshooting diff --git a/doc/security/user_email_confirmation.md b/doc/security/user_email_confirmation.md index f0af0a7ac6a..7ba50acbb06 100644 --- a/doc/security/user_email_confirmation.md +++ b/doc/security/user_email_confirmation.md @@ -1,6 +1,7 @@ --- type: howto --- + # User email confirmation at sign-up GitLab can be configured to require confirmation of a user's email address when diff --git a/doc/security/user_file_uploads.md b/doc/security/user_file_uploads.md index f34528a6e05..9fc8f7ec985 100644 --- a/doc/security/user_file_uploads.md +++ b/doc/security/user_file_uploads.md @@ -1,6 +1,7 @@ --- type: reference --- + # User File Uploads Images that are attached to issues, merge requests, or comments diff --git a/doc/security/webhooks.md b/doc/security/webhooks.md index d4fa088cb15..1194234a295 100644 --- a/doc/security/webhooks.md +++ b/doc/security/webhooks.md @@ -1,6 +1,7 @@ --- type: concepts, reference, howto --- + # Webhooks and insecure internal web services If you have non-GitLab web services running on your GitLab server or within its diff --git a/doc/topics/autodevops/index.md b/doc/topics/autodevops/index.md index 6dfe42f68cf..f9ad952aaad 100644 --- a/doc/topics/autodevops/index.md +++ b/doc/topics/autodevops/index.md @@ -222,7 +222,7 @@ full use of Auto DevOps are available. If this is your fist time, we recommend y [quick start guide](quick_start_guide.md). GitLab.com users can enable/disable Auto DevOps at the project-level only. Self-managed users -can enable/disable Auto DevOps at either the project-level or instance-level. +can enable/disable Auto DevOps at the project-level, group-level or instance-level. ### Enabling/disabling Auto DevOps at the instance-level (Administrators only) diff --git a/doc/user/admin_area/settings/continuous_integration.md b/doc/user/admin_area/settings/continuous_integration.md index 0fb7338a688..e56acac527f 100644 --- a/doc/user/admin_area/settings/continuous_integration.md +++ b/doc/user/admin_area/settings/continuous_integration.md @@ -164,3 +164,23 @@ questions that you know someone might ask. Each scenario can be a third-level heading, e.g. `### Getting error message X`. If you have none to add when creating a doc, leave this section in place but commented out to help encourage others to add to it in the future. --> + +## Required pipeline configuration **(PREMIUM ONLY)** + +GitLab administrators can force a pipeline configuration to run on every +pipeline. + +The configuration applies to all pipelines for a GitLab instance and is +sourced from: + +- The [instance template repository](instance_template_repository.md). +- GitLab-supplied configuration. + +To set required pipeline configuration: + +1. Go to **Admin area > Settings > CI/CD**. +1. Expand the **Required pipeline configuration** section. +1. Select the required configuration from the provided dropdown. +1. Click **Save changes**. + +![Required pipeline](img/admin_required_pipeline.png) diff --git a/doc/user/admin_area/settings/img/admin_required_pipeline.png b/doc/user/admin_area/settings/img/admin_required_pipeline.png Binary files differnew file mode 100644 index 00000000000..58488674d51 --- /dev/null +++ b/doc/user/admin_area/settings/img/admin_required_pipeline.png diff --git a/doc/user/admin_area/settings/sign_up_restrictions.md b/doc/user/admin_area/settings/sign_up_restrictions.md index cebf36c7ec1..d77e91156f8 100644 --- a/doc/user/admin_area/settings/sign_up_restrictions.md +++ b/doc/user/admin_area/settings/sign_up_restrictions.md @@ -4,8 +4,8 @@ type: reference # Sign-up restrictions -You can block email addresses of specific domains, or whitelist only some -specific domains via the **Application Settings** in the Admin area. +By implementing sign-up restrictions, you can blacklist or whitelist email addresses +belonging to specific domains. >**Note**: These restrictions are only applied during sign-up. An admin is able to add a user through the admin panel with a disallowed domain. Also @@ -24,17 +24,21 @@ domains list. > [Introduced][ce-5259] in GitLab 8.10. With this feature enabled, you can block email addresses of a specific domain -from creating an account on your GitLab server. This is particularly useful to -prevent spam. Disposable email addresses are usually used by malicious users to -create dummy accounts and spam issues. +from creating an account on your GitLab server. This is particularly useful to prevent spam. Disposable email addresses are usually used by malicious users to create dummy accounts and spam issues. ## Settings -This feature can be activated via the **Application Settings** in the Admin area, -and you have the option of entering the list manually, or uploading a file with -the list. +To access this feature: -Both whitelist and blacklist accept wildcards, so for example, you can use +1. Navigate to the **Settings > General** in the Admin area. +1. Expand the **Sign-up restrictions** section. + +For the: + +- Blacklist, you can enter the list manually, or upload a `.txt` file with it. +- Whitelist you must enter the list manually. + +Both the whitelist and blacklist accept wildcards. For example, you can use `*.company.com` to accept every `company.com` subdomain, or `*.io` to block all domains ending in `.io`. Domains should be separated by a whitespace, semicolon, comma, or a new line. diff --git a/doc/user/application_security/dependency_scanning/index.md b/doc/user/application_security/dependency_scanning/index.md index 0dd0fd3f136..09bd306363c 100644 --- a/doc/user/application_security/dependency_scanning/index.md +++ b/doc/user/application_security/dependency_scanning/index.md @@ -149,6 +149,8 @@ using environment variables. | `DS_DOCKER_CLIENT_NEGOTIATION_TIMEOUT` | Time limit for Docker client negotiation. Timeouts are parsed using Go's [`ParseDuration`](https://golang.org/pkg/time/#ParseDuration). Valid time units are `ns`, `us` (or `µs`), `ms`, `s`, `m`, `h`. For example, `300ms`, `1.5h`, or `2h45m`. | | `DS_PULL_ANALYZER_IMAGE_TIMEOUT` | Time limit when pulling the image of an analyzer. Timeouts are parsed using Go's [`ParseDuration`](https://golang.org/pkg/time/#ParseDuration). Valid time units are `ns`, `us` (or `µs`), `ms`, `s`, `m`, `h`. For example, `300ms`, `1.5h`, or `2h45m`. | | `DS_RUN_ANALYZER_TIMEOUT` | Time limit when running an analyzer. Timeouts are parsed using Go's [`ParseDuration`](https://golang.org/pkg/time/#ParseDuration). Valid time units are `ns`, `us` (or `µs`), `ms`, `s`, `m`, `h`. For example, `300ms`, `1.5h`, or `2h45m`. | +| `PIP_INDEX_URL` | Base URL of Python Package Index (default https://pypi.org/simple). | +| `PIP_EXTRA_INDEX_URL` | Array of [extra URLs](https://pip.pypa.io/en/stable/reference/pip_install/#cmdoption-extra-index-url) of package indexes to use in addition to `PIP_INDEX_URL`. Comma separated. | ## Reports JSON format diff --git a/doc/user/clusters/applications.md b/doc/user/clusters/applications.md index 2246ea8ed5a..6956086c382 100644 --- a/doc/user/clusters/applications.md +++ b/doc/user/clusters/applications.md @@ -251,6 +251,7 @@ The applications below can be uninstalled. | Application | GitLab version | Notes | | ----------- | -------------- | ----- | +| GitLab Runner | 12.2+ | Any running pipelines will be canceled. | | Ingress | 12.1+ | The associated load balancer and IP will be deleted and cannot be restored. Furthermore, it can only be uninstalled if JupyterHub is not installed. | | JupyterHub | 12.1+ | All data not committed to GitLab will be deleted and cannot be restored. | | Prometheus | 11.11+ | All data will be deleted and cannot be restored. | diff --git a/doc/user/markdown.md b/doc/user/markdown.md index a387cb43e0f..96e74125801 100644 --- a/doc/user/markdown.md +++ b/doc/user/markdown.md @@ -556,7 +556,7 @@ Inline `code` has `back-ticks around` it. Similarly, a whole block of code can be fenced with triple backticks ```` ``` ````, triple tildes (`~~~`), or indended 4 or more spaces to achieve a similar effect for -a larger body of code. test. +a larger body of code. ~~~ ``` diff --git a/doc/user/project/autocomplete_characters.md b/doc/user/project/autocomplete_characters.md new file mode 100644 index 00000000000..9ebf7f821a1 --- /dev/null +++ b/doc/user/project/autocomplete_characters.md @@ -0,0 +1,48 @@ +# Autocomplete characters + +The autocomplete characters provide a quick way of entering field values into +Markdown fields. When you start typing a word in a Markdown field with one of +the following characters, GitLab progressively autocompletes against a set of +matching values. The string matching is not case sensitive. + +| Character | Autocompletes | +| :-------- | :------------ | +| `~` | Labels | +| `%` | Milestones | +| `@` | Users and groups | +| `#` | Issues | +| `!` | Merge requests | +| `&` | Epics | +| `$` | Snippets | +| `:` | Emoji | +| `/` | Quick Actions | + +Up to 5 of the most relevant matches are displayed in a popup list. When you +select an item from the list, the value is entered in the field. The more +characters you enter, the more precise the matches are. + +Autocomplete characters are useful when combined with [Quick Actions](quick_actions.md). + +## Example + +Assume your GitLab instance includes the following users: + +| Username | Name | +| :-------------- | :--- | +| alessandra | Rosy Grant | +| lawrence.white | Kelsey Kerluke | +| leanna | Rosemarie Rogahn | +| logan_gutkowski | Lee Wuckert | +| shelba | Josefine Haley | + +In an Issue comment, entering `@l` results in the following popup list +appearing. Note that user `shelba` is not included, because the list includes +only the 5 users most relevant to the Issue. + +![Popup list which includes users whose username or name contains the letter `l`](img/autocomplete_characters_example1_v12_0.png) + +If you continue to type, `@le`, the popup list changes to the following. The +popup now only includes users where `le` appears in their username, or a word in +their name. + +![Popup list which includes users whose username or name contains the string `le`](img/autocomplete_characters_example2_v12_0.png) diff --git a/doc/user/project/img/autocomplete_characters_example1_v12_0.png b/doc/user/project/img/autocomplete_characters_example1_v12_0.png Binary files differnew file mode 100755 index 00000000000..9c6fa923b80 --- /dev/null +++ b/doc/user/project/img/autocomplete_characters_example1_v12_0.png diff --git a/doc/user/project/img/autocomplete_characters_example2_v12_0.png b/doc/user/project/img/autocomplete_characters_example2_v12_0.png Binary files differnew file mode 100755 index 00000000000..b2e8a782a0b --- /dev/null +++ b/doc/user/project/img/autocomplete_characters_example2_v12_0.png diff --git a/doc/user/project/index.md b/doc/user/project/index.md index 0ffa69b6b78..7307c5b8991 100644 --- a/doc/user/project/index.md +++ b/doc/user/project/index.md @@ -52,6 +52,9 @@ When you create a project in GitLab, you'll have access to a large number of templates for issue and merge request description fields for your project - [Slash commands (quick actions)](quick_actions.md): Textual shortcuts for common actions on issues or merge requests +- [Autocomplete characters](autocomplete_characters.md): Autocomplete + references to users, groups, issues, merge requests, and other GitLab + elements. - [Web IDE](web_ide/index.md) **GitLab CI/CD:** diff --git a/doc/user/project/integrations/prometheus.md b/doc/user/project/integrations/prometheus.md index 69fefbf3891..72f12972596 100644 --- a/doc/user/project/integrations/prometheus.md +++ b/doc/user/project/integrations/prometheus.md @@ -121,68 +121,91 @@ GitLab supports a limited set of [CI variables](../../../ci/variables/README.htm To specify a variable in a query, enclose it in curly braces with a leading percent. For example: `%{ci_environment_slug}`. -### Defining Dashboards for Prometheus Metrics per Project +### Defining custom dashboards per project -All projects include a GitLab-defined system dashboard, which includes a few key metrics. Optionally, additional dashboards can also be defined by including configuration files in the project repository under `.gitlab/dashboards`. Configuration files nested under subdirectories will not be available in the UI. Each file should define the layout of the dashboard and the prometheus queries used to populate data. Dashboards can be selected from the dropdown in the UI. +> [Introduced](https://gitlab.com/gitlab-org/gitlab-ce/issues/59974) in GitLab 12.1. -#### Relationship to Custom Metrics +By default, all projects include a GitLab-defined Prometheus dashboard, which +includes a few key metrics, but you can also define your own custom dashboards. -[Custom Metrics](#adding-additional-metrics-premium) are defined through the UI and, at this point, are unique from metrics defined in dashboard configuration files. Custom Metrics will appear on the system dashboard, as well as support alerting, whereas metrics defined in configuration files do not yet support alerts. +NOTE: **Note:** +The custom metrics as defined below do not support alerts, unlike +[additional metrics](#adding-additional-metrics-premium). -#### Dashboard Configuration +Dashboards have several components: -Dashboards have several components. A dashboard has many panel groups, which are comprised of panels, which support one or more metrics. The dashboard should be saved with the `.yml` extension. +- Panel groups, which comprise panels. +- Panels, which support one or more metrics. -Sample YML Configuration -``` -dashboard: 'Dashboard Title' -priority: 2 -panel_groups: - - group: 'Group Title' - panels: - - type: area-chart - title: "Chart Title" - y_label: "Y-Axis" - metrics: - - id: metric_of_ages - query_range: 'http_requests_total' - label: "Metric of Ages" - unit: "count" -``` +To configure a custom dashboard: + +1. Create a YAML file with the `.yml` extension under your repository's root + directory inside `.gitlab/dashboards/`. For example, create + `.gitlab/dashboards/prom_alerts.yml` with the following contents: + + ```yaml + dashboard: 'Dashboard Title' + panel_groups: + - group: 'Group Title' + panels: + - type: area-chart + title: "Chart Title" + y_label: "Y-Axis" + metrics: + - id: metric_of_ages + query_range: 'http_requests_total' + label: "Metric of Ages" + unit: "count" + ``` + + The above sample dashboard would display a single area chart. Each file should + define the layout of the dashboard and the Prometheus queries used to populate + data. + +1. Save the file, commit, and push to your repository. +1. Navigate to your project's **Operations > Metrics** and choose the custom + dashboard from the dropdown. -The above sample dashboard would display a single area chart. The following sections outline the details of expected properties. +NOTE: **Note:** +Configuration files nested under subdirectories of `.gitlab/dashboards` are not +supported and will not be available in the UI. -##### Dashboard Properties -| Property | Type | Required? | Meaning | +The following tables outline the details of expected properties. + +**Dashboard properties:** + +| Property | Type | Required | Description | | ------ | ------ | ------ | ------ | -| `dashboard` | string | required | Heading for the dashboard. Only one dashboard should be defined per file. | -| `priority` | number | optional, default to definition order | Order to appear in dashboard dropdown, higher priority should be higher in the dropdown. Numbers do not need to be consecutive. | -| `panel_groups` | array | required | The panel groups which should be on the dashboard. | +| `dashboard` | string | yes | Heading for the dashboard. Only one dashboard should be defined per file. | +| `panel_groups` | array | yes | The panel groups which should be on the dashboard. | + +**Panel group (`panel_groups`) properties:** -##### Panel Group Properties -| Property | Type | Required? | Meaning | +| Property | Type | Required | Description | | ------ | ------ | ------ | ------ | | `group` | string | required | Heading for the panel group. | -| `priority` | number | optional, defaults to order in file | Order to appear on the dashboard, higher priority will be higher on the page. Numbers do not need to be consecutive. | +| `priority` | number | optional, defaults to order in file | Order to appear on the dashboard. Higher number means higher priority, which will be higher on the page. Numbers do not need to be consecutive. | | `panels` | array | required | The panels which should be in the panel group. | -##### Panel Properties -| Property | Type | Required? | Meaning | +**Panel (`panels`) properties:** + +| Property | Type | Required | Description | | ------ | ------ | ------ | ------- | -| `type` | enum | optional, defaults to `area-chart` | Specifies the chart type to use. Only `area-chart` is currently supported. | -| `title` | string | required | Heading for the panel. | -| `y_label` | string | optional, but highly encouraged | Y-Axis label for the panel. | -| `weight` | number | optional, defaults to order in file | Order to appear within the grouping, higher priority will be higher on the page. Numbers do not need to be consecutive. | -| `metrics` | array | required | The metrics which should be displayed in the panel. | - -##### Metric Properties -| Property | Type | Required? | Meaning | +| `type` | enum | no, defaults to `area-chart` | Specifies the chart type to use. Only `area-chart` is currently supported. | +| `title` | string | yes | Heading for the panel. | +| `y_label` | string | no, but highly encouraged | Y-Axis label for the panel. | +| `weight` | number | no, defaults to order in file | Order to appear within the grouping. Lower number means higher priority, which will be higher on the page. Numbers do not need to be consecutive. | +| `metrics` | array | yes | The metrics which should be displayed in the panel. | + +**Metrics (`metrics`) properties:** + +| Property | Type | Required | Description | | ------ | ------ | ------ | ------ | -| `id` | string | optional | Used for associating dashboard metrics with database records. Must be unique across dashboard configuration files. Required for [alerting](#setting-up-alerts-for-prometheus-metrics-ultimate) (support not yet enabled, see [relevant issue](https://gitlab.com/gitlab-org/gitlab-ce/issues/60319)). | -| `unit` | string | required | Defines the unit of the query's return data. | -| `label` | string | optional, but highly encouraged | Defines the legend-label for the query. Should be unique within the panel's metrics. | -| `query` | string | required unless `query_range` is defined | Defines the Prometheus query to be used to populate the chart/panel. If defined, the `query` endpoint of the [Prometheus API](https://prometheus.io/docs/prometheus/latest/querying/api/) will be utilized. | -| `query_range` | string | required unless `query` is defined | Defines the Prometheus query to be used to populate the chart/panel. If defined, the `query_range` endpoint of the [Prometheus API](https://prometheus.io/docs/prometheus/latest/querying/api/) will be utilized. | +| `id` | string | no | Used for associating dashboard metrics with database records. Must be unique across dashboard configuration files. Required for [alerting](#setting-up-alerts-for-prometheus-metrics-ultimate) (support not yet enabled, see [relevant issue](https://gitlab.com/gitlab-org/gitlab-ce/issues/60319)). | +| `unit` | string | yes | Defines the unit of the query's return data. | +| `label` | string | no, but highly encouraged | Defines the legend-label for the query. Should be unique within the panel's metrics. | +| `query` | string | yes if `query_range` is not defined | Defines the Prometheus query to be used to populate the chart/panel. If defined, the `query` endpoint of the [Prometheus API](https://prometheus.io/docs/prometheus/latest/querying/api/) will be utilized. | +| `query_range` | string | yes if `query` is not defined | Defines the Prometheus query to be used to populate the chart/panel. If defined, the `query_range` endpoint of the [Prometheus API](https://prometheus.io/docs/prometheus/latest/querying/api/) will be utilized. | ### Setting up alerts for Prometheus metrics **(ULTIMATE)** diff --git a/doc/user/project/pages/custom_domains_ssl_tls_certification/img/lets_encrypt_integration_v12_1.png b/doc/user/project/pages/custom_domains_ssl_tls_certification/img/lets_encrypt_integration_v12_1.png Binary files differnew file mode 100644 index 00000000000..2e825e84d92 --- /dev/null +++ b/doc/user/project/pages/custom_domains_ssl_tls_certification/img/lets_encrypt_integration_v12_1.png diff --git a/doc/user/project/pages/custom_domains_ssl_tls_certification/index.md b/doc/user/project/pages/custom_domains_ssl_tls_certification/index.md index 6c0d3e9e9d3..54ecc42d2b9 100644 --- a/doc/user/project/pages/custom_domains_ssl_tls_certification/index.md +++ b/doc/user/project/pages/custom_domains_ssl_tls_certification/index.md @@ -179,20 +179,39 @@ From that page, you can view, add, and remove them. ### Redirecting `www.domain.com` to `domain.com` with Cloudflare -If you use Cloudflare, you can redirect `www` to `domain.com` without adding both -`www.domain.com` and `domain.com` to GitLab. This happens due to a [Cloudflare feature that creates -a 301 redirect as a "page rule"](https://gitlab.com/gitlab-org/gitlab-ce/issues/48848#note_87314849) for redirecting `www.domain.com` to `domain.com`. In this case, -you can use the following setup: +If you use Cloudflare, you can redirect `www` to `domain.com` +without adding both `www.domain.com` and `domain.com` to GitLab. + +To do so, you can use Cloudflare's page rules associated to a +CNAME record to redirect `www.domain.com` to `domain.com`. You +can use the following setup: 1. In Cloudflare, create a DNS `A` record pointing `domain.com` to `35.185.44.232`. -1. In GitLab, add the domain to GitLab Pages. +1. In GitLab, add the domain to GitLab Pages and get the verification code. 1. In Cloudflare, create a DNS `TXT` record to verify your domain. +1. In GitLab, verify your domain. 1. In Cloudflare, create a DNS `CNAME` record pointing `www` to `domain.com`. +1. In Cloudflare, add a Page Rule pointing `www.domain,com` to `domain.com`: + - Navigate to your domain's dashboard and click **Page Rules** + on the top nav. + - Click **Create Page Rule**. + - Enter the domain `www.domain.com` and click **+ Add a Setting**. + - From the dropdown menu, choose **Forwarding URL**, then select the + status code **301 - Permanent Redirect**. + - Enter the destination URL `https://domain.com`. ## Adding an SSL/TLS certificate to Pages Read this document for an [overview on SSL/TLS certification](ssl_tls_concepts.md). +To secure your custom domain with GitLab Pages you can opt by: + +- Using the [Let's Encrypt integration with GitLab Pages](lets_encrypt_integration.md), + which automatically obtains and renews SSL certificates + for your Pages domains. +- Manually adding SSL/TLS certificates to GitLab Pages websites + by following the steps below. + ### Requirements - A GitLab Pages website up and running accessible via a custom domain. @@ -244,6 +263,7 @@ To enable this setting: 1. Navigate to your project's **Settings > Pages**. 1. Tick the checkbox **Force HTTPS (requires valid certificates)**. + <!-- ## Troubleshooting Include any troubleshooting steps that you can foresee. If you know beforehand what issues @@ -254,4 +274,4 @@ questions that you know someone might ask. Each scenario can be a third-level heading, e.g. `### Getting error message X`. If you have none to add when creating a doc, leave this section in place -but commented out to help encourage others to add to it in the future. -->
\ No newline at end of file +but commented out to help encourage others to add to it in the future. --> diff --git a/doc/user/project/pages/custom_domains_ssl_tls_certification/lets_encrypt_integration.md b/doc/user/project/pages/custom_domains_ssl_tls_certification/lets_encrypt_integration.md new file mode 100644 index 00000000000..7675a5dd9d4 --- /dev/null +++ b/doc/user/project/pages/custom_domains_ssl_tls_certification/lets_encrypt_integration.md @@ -0,0 +1,68 @@ +--- +type: reference +description: "Automatic Let's Encrypt SSL certificates for GitLab Pages." +--- + +# GitLab Pages integration with Let's Encrypt + +> [Introduced](https://gitlab.com/gitlab-org/gitlab-ce/issues/28996) in GitLab 12.1. + +The GitLab Pages integration with Let's Encrypt (LE) allows you +to use LE certificates for your Pages website with custom domains +without the hassle of having to issue and update them yourself; +GitLab does it for you, out-of-the-box. + +[Let's Encrypt](https://letsencrypt.org) is a free, automated, and +open source Certificate Authority. + +## Requirements + +Before you can enable automatic provisioning of a SSL certificate for your domain, make sure you have: + +- Created a [project](../getting_started_part_two.md) in GitLab + containing your website's source code. +- Acquired a domain (`example.com`) and added a [DNS entry](index.md) + pointing it to your Pages website. +- [Added your domain to your Pages project](index.md#1-add-a-custom-domain-to-pages) + and verified your ownership. +- Have your website up and running, accessible through your custom domain. + +NOTE: **Note:** +GitLab's Let's Encrypt integration is enabled and available on GitLab.com. +For **self-managed** GitLab instances, make sure your administrator has +[enabled it](../../../../administration/pages/index.md#lets-encrypt-integration). + +## Enabling Let's Encrypt integration for your custom domain + +Once you've met the requirements, to enable Let's Encrypt integration: + +1. Navigate to your project's **Settings > Pages**. +1. Find your domain and click **Details**. +1. Click **Edit** in the top-right corner. +1. Enable Let's Encrypt integration by switching **Automatic certificate management using Let's Encrypt**: + + ![Enable Let's Encrypt](img/lets_encrypt_integration_v12_1.png) + +1. Click **Save changes**. + +Once enabled, GitLab will obtain a LE certificate and add it to the +associated Pages domain. It will be also renewed automatically by GitLab. + +> **Notes:** +> +> - Issuing the certificate and updating Pages configuration +> **can take up to an hour**. +> - If you already have SSL certificate in domain settings it +> will continue to work until it will be replaced by Let's Encrypt's certificate. + +<!-- ## Troubleshooting + +Include any troubleshooting steps that you can foresee. If you know beforehand what issues +one might have when setting this up, or when something is changed, or on upgrading, it's +important to describe those, too. Think of things that may go wrong and include them here. +This is important to minimize requests for support, and to avoid doc comments with +questions that you know someone might ask. + +Each scenario can be a third-level heading, e.g. `### Getting error message X`. +If you have none to add when creating a doc, leave this section in place +but commented out to help encourage others to add to it in the future. --> diff --git a/doc/user/project/pages/index.md b/doc/user/project/pages/index.md index e9d2e9a0059..25944b029d7 100644 --- a/doc/user/project/pages/index.md +++ b/doc/user/project/pages/index.md @@ -143,8 +143,8 @@ To learn more about configuration options for GitLab Pages, read the following: | [Exploring GitLab Pages](introduction.md) | Requirements, technical aspects, specific GitLab CI's configuration options, Access Control, custom 404 pages, limitations, FAQ. | |---+---| | [Custom domains and SSL/TLS Certificates](custom_domains_ssl_tls_certification/index.md) | How to add custom domains and subdomains to your website, configure DNS records and SSL/TLS certificates. | +| [Let's Encrypt integration](custom_domains_ssl_tls_certification/lets_encrypt_integration.md) | Secure your Pages sites with Let's Encrypt certificates automatically obtained and renewed by GitLab. | | [CloudFlare certificates](https://about.gitlab.com/2017/02/07/setting-up-gitlab-pages-with-cloudflare-certificates/) | Secure your Pages site with CloudFlare certificates. | -| [Let's Encrypt certificates](lets_encrypt_for_gitlab_pages.md) | Secure your Pages site with Let's Encrypt certificates. | |---+---| | [Static vs dynamic websites](https://about.gitlab.com/2016/06/03/ssg-overview-gitlab-pages-part-1-dynamic-x-static/) | A conceptual overview on static versus dynamic sites. | | [Modern static site generators](https://about.gitlab.com/2016/06/10/ssg-overview-gitlab-pages-part-2/) | A conceptual overview on SSGs. | diff --git a/doc/user/project/pages/lets_encrypt_for_gitlab_pages.md b/doc/user/project/pages/lets_encrypt_for_gitlab_pages.md index cc129f90b7a..1338c7e58f5 100644 --- a/doc/user/project/pages/lets_encrypt_for_gitlab_pages.md +++ b/doc/user/project/pages/lets_encrypt_for_gitlab_pages.md @@ -1,10 +1,15 @@ --- -description: "How to secure GitLab Pages websites with Let's Encrypt." +description: "How to secure GitLab Pages websites with Let's Encrypt (manual process, deprecated)." type: howto -last_updated: 2019-06-04 +last_updated: 2019-07-15 --- -# Let's Encrypt for GitLab Pages +# Let's Encrypt for GitLab Pages (manual process, deprecated) + +CAUTION: **Warning:** +This method is still valid but was **deprecated** in favor of the +[Let's Encrypt integration](custom_domains_ssl_tls_certification/lets_encrypt_integration.md) +introduced in GitLab 12.1. If you have a GitLab Pages website served under your own domain, you might want to secure it with a SSL/TSL certificate. diff --git a/doc/user/project/pipelines/settings.md b/doc/user/project/pipelines/settings.md index e60da6a3e59..df82daa3da3 100644 --- a/doc/user/project/pipelines/settings.md +++ b/doc/user/project/pipelines/settings.md @@ -89,6 +89,22 @@ in the jobs table. A few examples of known coverage tools for a variety of languages can be found in the pipelines settings page. +### Removing color codes + +Some test coverage tools output with ANSI color codes that won't be +parsed correctly by the regular expression and will cause coverage +parsing to fail. + +If your coverage tool doesn't provide an option to disable color +codes in the output, you can pipe the output of the coverage tool through a +small one line script that will strip the color codes off. + +For example: + +```bash +lein cloverage | perl -pe 's/\e\[?.*?[\@-~]//g' +``` + ## Visibility of pipelines Access to pipelines and job details (including output of logs and artifacts) diff --git a/doc/user/project/quick_actions.md b/doc/user/project/quick_actions.md index 8baac775910..948ac91a886 100644 --- a/doc/user/project/quick_actions.md +++ b/doc/user/project/quick_actions.md @@ -34,19 +34,19 @@ discussions, and descriptions: | `/remove_milestone` | Remove milestone | ✓ | ✓ | | `/label ~label1 ~label2` | Add label(s). Label names can also start without ~ but mixed syntax is not supported. | ✓ | ✓ | | `/unlabel ~label1 ~label2` | Remove all or specific label(s)| ✓ | ✓ | -| `/relabel ~label1 ~label2` | Replace label | ✓ | ✓ | -| <code>/copy_metadata <#issue | !merge_request></code> | Copy labels and milestone from other issue or merge request in the project | ✓ | ✓ | -| <code>/estimate <1w 3d 2h 14m></code> | Set time estimate | ✓ | ✓ | +| `/relabel ~label1 ~label2` | Replace existing label(s) with those specified | ✓ | ✓ | +| `/copy_metadata <#issue | !merge_request>` | Copy labels and milestone from other issue or merge request in the project | ✓ | ✓ | +| `/estimate <1w 3d 2h 14m>` | Set time estimate | ✓ | ✓ | | `/remove_estimate` | Remove time estimate | ✓ | ✓ | -| <code>/spend <time(1h 30m | -1h 5m)> <date(YYYY-MM-DD)></code> | Add or subtract spent time; optionally, specify the date that time was spent on | ✓ | ✓ | +| `/spend <time(1h 30m | -1h 5m)> <date(YYYY-MM-DD)>` | Add or subtract spent time; optionally, specify the date that time was spent on | ✓ | ✓ | | `/remove_time_spent` | Remove time spent | ✓ | ✓ | | `/lock` | Lock the thread | ✓ | ✓ | | `/unlock` | Unlock the thread | ✓ | ✓ | -| <code>/due <in 2 days | this Friday | December 31st></code>| Set due date | ✓ | | +| `/due <in 2 days | this Friday | December 31st>`| Set due date | ✓ | | | `/remove_due_date` | Remove due date | ✓ | | -| <code>/weight <0 | 1 | 2 | ...></code> | Set weight **(STARTER)** | ✓ | | +| `/weight <0 | 1 | 2 | ...>` | Set weight **(STARTER)** | ✓ | | | `/clear_weight` | Clears weight **(STARTER)** | ✓ | | -| <code>/epic <&epic | group&epic | Epic URL></code> | Add to epic **(ULTIMATE)** | ✓ | | +| `/epic <&epic | group&epic | Epic URL>` | Add to epic **(ULTIMATE)** | ✓ | | | `/remove_epic` | Removes from epic **(ULTIMATE)** | ✓ | | | `/promote` | Promote issue to epic **(ULTIMATE)** | ✓ | | | `/confidential` | Make confidential | ✓ | | @@ -85,8 +85,8 @@ The following quick actions are applicable for epics threads and description: | `/award :emoji:` | Toggle emoji award | | `/label ~label1 ~label2` | Add label(s) | | `/unlabel ~label1 ~label2` | Remove all or specific label(s) | -| `/relabel ~label1 ~label2` | Replace label | -| <code>/child_epic <&epic | group&epic | Epic URL></code> | Adds child epic to epic ([introduced in GitLab 12.0](https://gitlab.com/gitlab-org/gitlab-ee/issues/7330)) | -| <code>/remove_child_epic <&epic | group&epic | Epic URL></code> | Removes child epic from epic ([introduced in GitLab 12.0](https://gitlab.com/gitlab-org/gitlab-ee/issues/7330)) | -| <code>/parent_epic <&epic | group&epic | Epic URL></code> | Sets parent epic to epic ([introduced in GitLab 12.1](https://gitlab.com/gitlab-org/gitlab-ee/issues/10556)) | -| <code>/remove_parent_epic | Removes parent epic from epic ([introduced in GitLab 12.1](https://gitlab.com/gitlab-org/gitlab-ee/issues/10556)) | +| `/relabel ~label1 ~label2` | Replace existing label(s) with those specified | +| `/child_epic <&epic | group&epic | Epic URL>` | Adds child epic to epic ([introduced in GitLab 12.0](https://gitlab.com/gitlab-org/gitlab-ee/issues/7330)) | +| `/remove_child_epic <&epic | group&epic | Epic URL>` | Removes child epic from epic ([introduced in GitLab 12.0](https://gitlab.com/gitlab-org/gitlab-ee/issues/7330)) | +| `/parent_epic <&epic | group&epic | Epic URL>` | Sets parent epic to epic ([introduced in GitLab 12.1](https://gitlab.com/gitlab-org/gitlab-ee/issues/10556)) | +| `/remove_parent_epic` | Removes parent epic from epic ([introduced in GitLab 12.1](https://gitlab.com/gitlab-org/gitlab-ee/issues/10556)) | diff --git a/doc/workflow/time_tracking.md b/doc/workflow/time_tracking.md index 4286a3625a2..b55c6b2e3df 100644 --- a/doc/workflow/time_tracking.md +++ b/doc/workflow/time_tracking.md @@ -75,7 +75,7 @@ Default conversion rates are 1mo = 4w, 1w = 5d and 1d = 8h. ### Limit displayed units to hours -> Introduced in GitLab 12.0. +> Introduced in GitLab 12.1. The display of time units can be limited to hours through the option in **Admin Area > Settings > Preferences** under 'Localization'. diff --git a/lib/api/commits.rb b/lib/api/commits.rb index eebded87ebc..c414ad75d9d 100644 --- a/lib/api/commits.rb +++ b/lib/api/commits.rb @@ -126,7 +126,7 @@ module API if result[:status] == :success commit_detail = user_project.repository.commit(result[:result]) - Gitlab::WebIdeCommitsCounter.increment if find_user_from_warden + Gitlab::UsageDataCounters::WebIdeCommitsCounter.increment if find_user_from_warden present commit_detail, with: Entities::CommitDetail, stats: params[:stats] else diff --git a/lib/api/entities.rb b/lib/api/entities.rb index 0a9515f1dd2..494da770279 100644 --- a/lib/api/entities.rb +++ b/lib/api/entities.rb @@ -294,7 +294,6 @@ module API expose :statistics, using: 'API::Entities::ProjectStatistics', if: -> (project, options) { options[:statistics] && Ability.allowed?(options[:current_user], :read_statistics, project) } - expose :external_authorization_classification_label expose :auto_devops_enabled?, as: :auto_devops_enabled expose :auto_devops_deploy_strategy do |project, options| project.auto_devops.nil? ? 'continuous' : project.auto_devops.deploy_strategy diff --git a/lib/api/helpers/projects_helpers.rb b/lib/api/helpers/projects_helpers.rb index 0e21a7a66fd..833e3b9ebaf 100644 --- a/lib/api/helpers/projects_helpers.rb +++ b/lib/api/helpers/projects_helpers.rb @@ -42,7 +42,6 @@ module API optional :printing_merge_request_link_enabled, type: Boolean, desc: 'Show link to create/view merge request when pushing from the command line' optional :merge_method, type: String, values: %w(ff rebase_merge merge), desc: 'The merge method used when merging merge requests' optional :initialize_with_readme, type: Boolean, desc: "Initialize a project with a README.md" - optional :external_authorization_classification_label, type: String, desc: 'The classification label for the project' optional :ci_default_git_depth, type: Integer, desc: 'Default number of revisions for shallow cloning' optional :auto_devops_enabled, type: Boolean, desc: 'Flag indication if Auto DevOps is enabled' optional :auto_devops_deploy_strategy, type: String, values: %w(continuous manual timed_incremental), desc: 'Auto Deploy strategy' @@ -94,7 +93,6 @@ module API :visibility, :wiki_access_level, :avatar, - :external_authorization_classification_label, # TODO: remove in API v5, replaced by *_access_level :issues_enabled, @@ -105,6 +103,9 @@ module API :snippets_enabled ] end + + def filter_attributes_using_license!(attrs) + end end end end diff --git a/lib/api/projects.rb b/lib/api/projects.rb index a7d62014509..0923d31f5ff 100644 --- a/lib/api/projects.rb +++ b/lib/api/projects.rb @@ -145,6 +145,7 @@ module API post do attrs = declared_params(include_missing: false) attrs = translate_params_for_compatibility(attrs) + filter_attributes_using_license!(attrs) project = ::Projects::CreateService.new(current_user, attrs).execute if project.saved? @@ -179,6 +180,7 @@ module API attrs = declared_params(include_missing: false) attrs = translate_params_for_compatibility(attrs) + filter_attributes_using_license!(attrs) project = ::Projects::CreateService.new(user, attrs).execute if project.saved? @@ -292,7 +294,7 @@ module API authorize! :change_visibility_level, user_project if attrs[:visibility].present? attrs = translate_params_for_compatibility(attrs) - + filter_attributes_using_license!(attrs) verify_update_project_attrs!(user_project, attrs) result = ::Projects::UpdateService.new(user_project, current_user, attrs).execute diff --git a/lib/api/releases.rb b/lib/api/releases.rb index fdd8406388e..7a3d804c30c 100644 --- a/lib/api/releases.rb +++ b/lib/api/releases.rb @@ -78,7 +78,7 @@ module API requires :tag_name, type: String, desc: 'The name of the tag', as: :tag optional :name, type: String, desc: 'The name of the release' optional :description, type: String, desc: 'Release notes with markdown support' - optional :released_at, type: DateTime, desc: 'The date when the release will be/was ready. Defaults to the current time.' + optional :released_at, type: DateTime, desc: 'The date when the release will be/was ready.' end put ':id/releases/:tag_name', requirements: RELEASE_ENDPOINT_REQUIREMETS do authorize_update_release! diff --git a/lib/api/users.rb b/lib/api/users.rb index 30a278fdff1..a4ac5b629b8 100644 --- a/lib/api/users.rb +++ b/lib/api/users.rb @@ -148,7 +148,7 @@ module API end desc 'Create a user. Available only for admins.' do - success Entities::UserPublic + success Entities::UserWithAdmin end params do requires :email, type: String, desc: 'The email of the user' @@ -168,7 +168,7 @@ module API user = ::Users::CreateService.new(current_user, params).execute(skip_authorization: true) if user.persisted? - present user, with: Entities::UserPublic, current_user: current_user + present user, with: Entities::UserWithAdmin, current_user: current_user else conflict!('Email has already been taken') if User .by_any_email(user.email.downcase) @@ -183,7 +183,7 @@ module API end desc 'Update a user. Available only for admins.' do - success Entities::UserPublic + success Entities::UserWithAdmin end params do requires :id, type: Integer, desc: 'The ID of the user' @@ -215,7 +215,7 @@ module API result = ::Users::UpdateService.new(current_user, user_params.merge(user: user)).execute if result[:status] == :success - present user, with: Entities::UserPublic, current_user: current_user + present user, with: Entities::UserWithAdmin, current_user: current_user else render_validation_error!(user) end diff --git a/lib/banzai/filter/ascii_doc_sanitization_filter.rb b/lib/banzai/filter/ascii_doc_sanitization_filter.rb index a78bb60103c..9105e86ad04 100644 --- a/lib/banzai/filter/ascii_doc_sanitization_filter.rb +++ b/lib/banzai/filter/ascii_doc_sanitization_filter.rb @@ -6,11 +6,20 @@ module Banzai # # Extends Banzai::Filter::BaseSanitizationFilter with specific rules. class AsciiDocSanitizationFilter < Banzai::Filter::BaseSanitizationFilter + # Section anchor link pattern + SECTION_LINK_REF_PATTERN = /\A#{Gitlab::Asciidoc::DEFAULT_ADOC_ATTRS['idprefix']}(:?[[:alnum:]]|-|_)+\z/.freeze + SECTION_HEADINGS = %w(h2 h3 h4 h5 h6).freeze + + # Footnote link patterns + FOOTNOTE_LINK_ID_PATTERNS = { + a: /\A_footnoteref_\d+\z/, + div: /\A_footnotedef_\d+\z/ + }.freeze + # Classes used by Asciidoctor to style components ADMONITION_CLASSES = %w(fa icon-note icon-tip icon-warning icon-caution icon-important).freeze CALLOUT_CLASSES = ['conum'].freeze CHECKLIST_CLASSES = %w(fa fa-check-square-o fa-square-o).freeze - LIST_CLASSES = %w(checklist none no-bullet unnumbered unstyled).freeze ELEMENT_CLASSES_WHITELIST = { @@ -19,14 +28,15 @@ module Banzai td: ['icon'].freeze, i: ADMONITION_CLASSES + CALLOUT_CLASSES + CHECKLIST_CLASSES, ul: LIST_CLASSES, - ol: LIST_CLASSES + ol: LIST_CLASSES, + a: ['anchor'].freeze }.freeze def customize_whitelist(whitelist) # Allow marks whitelist[:elements].push('mark') - # Allow any classes in `span`, `i`, `div`, `td`, `ul` and `ol` elements + # Allow any classes in `span`, `i`, `div`, `td`, `ul`, `ol` and `a` elements # but then remove any unknown classes whitelist[:attributes]['span'] = %w(class) whitelist[:attributes]['div'].push('class') @@ -34,12 +44,51 @@ module Banzai whitelist[:attributes]['i'] = %w(class) whitelist[:attributes]['ul'] = %w(class) whitelist[:attributes]['ol'] = %w(class) + whitelist[:attributes]['a'].push('class') whitelist[:transformers].push(self.class.remove_element_classes) + # Allow `id` in heading elements for section anchors + SECTION_HEADINGS.each do |header| + whitelist[:attributes][header] = %w(id) + end + whitelist[:transformers].push(self.class.remove_non_heading_ids) + + # Allow `id` in footnote elements + FOOTNOTE_LINK_ID_PATTERNS.keys.each do |element| + whitelist[:attributes][element.to_s].push('id') + end + whitelist[:transformers].push(self.class.remove_non_footnote_ids) + whitelist end class << self + def remove_non_footnote_ids + lambda do |env| + node = env[:node] + + return unless (pattern = FOOTNOTE_LINK_ID_PATTERNS[node.name.to_sym]) + return unless node.has_attribute?('id') + + return if node['id'] =~ pattern + + node.remove_attribute('id') + end + end + + def remove_non_heading_ids + lambda do |env| + node = env[:node] + + return unless SECTION_HEADINGS.any?(node.name) + return unless node.has_attribute?('id') + + return if node['id'] =~ SECTION_LINK_REF_PATTERN + + node.remove_attribute('id') + end + end + def remove_element_classes lambda do |env| node = env[:node] diff --git a/lib/banzai/filter/redactor_filter.rb b/lib/banzai/filter/reference_redactor_filter.rb index 1f091f594f8..485d3fd5fc7 100644 --- a/lib/banzai/filter/redactor_filter.rb +++ b/lib/banzai/filter/reference_redactor_filter.rb @@ -7,12 +7,12 @@ module Banzai # # Expected to be run in its own post-processing pipeline. # - class RedactorFilter < HTML::Pipeline::Filter + class ReferenceRedactorFilter < HTML::Pipeline::Filter def call unless context[:skip_redaction] context = RenderContext.new(project, current_user) - Redactor.new(context).redact([doc]) + ReferenceRedactor.new(context).redact([doc]) end doc diff --git a/lib/banzai/object_renderer.rb b/lib/banzai/object_renderer.rb index 75661ffa233..d6d29f4bfab 100644 --- a/lib/banzai/object_renderer.rb +++ b/lib/banzai/object_renderer.rb @@ -72,7 +72,7 @@ module Banzai # # Returns an Array containing the redacted documents. def redact_documents(documents) - redactor = Redactor.new(context) + redactor = ReferenceRedactor.new(context) redactor.redact(documents) end diff --git a/lib/banzai/pipeline/post_process_pipeline.rb b/lib/banzai/pipeline/post_process_pipeline.rb index 5c199453638..54af26b41be 100644 --- a/lib/banzai/pipeline/post_process_pipeline.rb +++ b/lib/banzai/pipeline/post_process_pipeline.rb @@ -12,7 +12,7 @@ module Banzai def self.internal_link_filters [ - Filter::RedactorFilter, + Filter::ReferenceRedactorFilter, Filter::InlineMetricsRedactorFilter, Filter::RelativeLinkFilter, Filter::IssuableStateFilter, diff --git a/lib/banzai/redactor.rb b/lib/banzai/reference_redactor.rb index c2da7fec7cc..eb5c35da375 100644 --- a/lib/banzai/redactor.rb +++ b/lib/banzai/reference_redactor.rb @@ -3,7 +3,7 @@ module Banzai # Class for removing Markdown references a certain user is not allowed to # view. - class Redactor + class ReferenceRedactor attr_reader :context # context - An instance of `Banzai::RenderContext`. diff --git a/lib/banzai/renderer.rb b/lib/banzai/renderer.rb index 81f32ef5bcf..3cb9ec21e8f 100644 --- a/lib/banzai/renderer.rb +++ b/lib/banzai/renderer.rb @@ -134,7 +134,7 @@ module Banzai # # This method is used to perform state-dependent changes to a String of # HTML, such as removing references that the current user doesn't have - # permission to make (`RedactorFilter`). + # permission to make (`ReferenceRedactorFilter`). # # html - String to process # context - Hash of options to customize output diff --git a/lib/feature/gitaly.rb b/lib/feature/gitaly.rb index 67c0b902c0c..edfd2fb17f3 100644 --- a/lib/feature/gitaly.rb +++ b/lib/feature/gitaly.rb @@ -5,16 +5,12 @@ require 'set' class Feature class Gitaly # Server feature flags should use '_' to separate words. - # CATFILE_CACHE sets an incorrect example - CATFILE_CACHE = 'catfile-cache'.freeze - SERVER_FEATURE_FLAGS = [ - CATFILE_CACHE, 'get_commit_signatures'.freeze ].freeze - DEFAULT_ON_FLAGS = Set.new([CATFILE_CACHE]).freeze + DEFAULT_ON_FLAGS = Set.new([]).freeze class << self def enabled?(feature_flag) diff --git a/lib/gitlab/asciidoc.rb b/lib/gitlab/asciidoc.rb index 00c87cce7b6..da65caa6c9c 100644 --- a/lib/gitlab/asciidoc.rb +++ b/lib/gitlab/asciidoc.rb @@ -13,6 +13,7 @@ module Gitlab MAX_INCLUDE_DEPTH = 5 DEFAULT_ADOC_ATTRS = { 'showtitle' => true, + 'sectanchors' => true, 'idprefix' => 'user-content-', 'idseparator' => '-', 'env' => 'gitlab', diff --git a/lib/gitlab/background_migration/fix_pages_access_level.rb b/lib/gitlab/background_migration/fix_pages_access_level.rb new file mode 100644 index 00000000000..0d49f3dd8c5 --- /dev/null +++ b/lib/gitlab/background_migration/fix_pages_access_level.rb @@ -0,0 +1,128 @@ +# frozen_string_literal: true + +module Gitlab + module BackgroundMigration + # corrects stored pages access level on db depending on project visibility + class FixPagesAccessLevel + # Copy routable here to avoid relying on application logic + module Routable + def build_full_path + if parent && path + parent.build_full_path + '/' + path + else + path + end + end + end + + # Namespace + class Namespace < ApplicationRecord + self.table_name = 'namespaces' + self.inheritance_column = :_type_disabled + + include Routable + + belongs_to :parent, class_name: "Namespace" + end + + # Project + class Project < ActiveRecord::Base + self.table_name = 'projects' + self.inheritance_column = :_type_disabled + + include Routable + + belongs_to :namespace + alias_method :parent, :namespace + alias_attribute :parent_id, :namespace_id + + PRIVATE = 0 + INTERNAL = 10 + PUBLIC = 20 + + def pages_deployed? + Dir.exist?(public_pages_path) + end + + def public_pages_path + File.join(pages_path, 'public') + end + + def pages_path + # TODO: when we migrate Pages to work with new storage types, change here to use disk_path + File.join(Settings.pages.path, build_full_path) + end + end + + # ProjectFeature + class ProjectFeature < ActiveRecord::Base + include ::EachBatch + + self.table_name = 'project_features' + + belongs_to :project + + PRIVATE = 10 + ENABLED = 20 + PUBLIC = 30 + end + + def perform(start_id, stop_id) + fix_public_access_level(start_id, stop_id) + + make_internal_projects_public(start_id, stop_id) + + fix_private_access_level(start_id, stop_id) + end + + private + + def access_control_is_enabled + @access_control_is_enabled = Gitlab.config.pages.access_control + end + + # Public projects are allowed to have only enabled pages_access_level + # which is equivalent to public + def fix_public_access_level(start_id, stop_id) + project_features(start_id, stop_id, ProjectFeature::PUBLIC, Project::PUBLIC).each_batch do |features| + features.update_all(pages_access_level: ProjectFeature::ENABLED) + end + end + + # If access control is disabled and project has pages deployed + # project will become unavailable when access control will become enabled + # we make these projects public to avoid negative surprise to user + def make_internal_projects_public(start_id, stop_id) + return if access_control_is_enabled + + project_features(start_id, stop_id, ProjectFeature::ENABLED, Project::INTERNAL).find_each do |project_feature| + next unless project_feature.project.pages_deployed? + + project_feature.update(pages_access_level: ProjectFeature::PUBLIC) + end + end + + # Private projects are not allowed to have enabled access level, only `private` and `public` + # If access control is enabled, these projects currently behave as if the have `private` pages_access_level + # if access control is disabled, these projects currently behave as if the have `public` pages_access_level + # so we preserve this behaviour for projects with pages already deployed + # for project without pages we always set `private` access_level + def fix_private_access_level(start_id, stop_id) + project_features(start_id, stop_id, ProjectFeature::ENABLED, Project::PRIVATE).find_each do |project_feature| + if access_control_is_enabled + project_feature.update!(pages_access_level: ProjectFeature::PRIVATE) + else + fixed_access_level = project_feature.project.pages_deployed? ? ProjectFeature::PUBLIC : ProjectFeature::PRIVATE + project_feature.update!(pages_access_level: fixed_access_level) + end + end + end + + def project_features(start_id, stop_id, pages_access_level, project_visibility_level) + ProjectFeature.where(id: start_id..stop_id).joins(:project) + .where(pages_access_level: pages_access_level) + .where(projects: { visibility_level: project_visibility_level }) + end + end + end +end diff --git a/lib/gitlab/ci/pipeline/chain/command.rb b/lib/gitlab/ci/pipeline/chain/command.rb index c911bfa7ff6..afad391e8e0 100644 --- a/lib/gitlab/ci/pipeline/chain/command.rb +++ b/lib/gitlab/ci/pipeline/chain/command.rb @@ -20,6 +20,12 @@ module Gitlab end end + def uses_unsupported_legacy_trigger? + trigger_request.present? && + trigger_request.trigger.legacy? && + !trigger_request.trigger.supports_legacy_tokens? + end + def branch_exists? strong_memoize(:is_branch) do project.repository.branch_exists?(ref) diff --git a/lib/gitlab/ci/pipeline/chain/validate/abilities.rb b/lib/gitlab/ci/pipeline/chain/validate/abilities.rb index aaa3daddcc5..357a1d55b3b 100644 --- a/lib/gitlab/ci/pipeline/chain/validate/abilities.rb +++ b/lib/gitlab/ci/pipeline/chain/validate/abilities.rb @@ -14,6 +14,10 @@ module Gitlab return error('Pipelines are disabled!') end + if @command.uses_unsupported_legacy_trigger? + return error('Trigger token is invalid because is not owned by any user') + end + unless allowed_to_trigger_pipeline? if can?(current_user, :create_pipeline, project) return error("Insufficient permissions for protected ref '#{command.ref}'") diff --git a/lib/gitlab/ci/pipeline/expression/lexeme/pattern.rb b/lib/gitlab/ci/pipeline/expression/lexeme/pattern.rb index e4cf360a1c1..0212fa9d661 100644 --- a/lib/gitlab/ci/pipeline/expression/lexeme/pattern.rb +++ b/lib/gitlab/ci/pipeline/expression/lexeme/pattern.rb @@ -8,11 +8,10 @@ module Gitlab require_dependency 're2' class Pattern < Lexeme::Value - PATTERN = %r{^/.+/[ismU]*$}.freeze - NEW_PATTERN = %r{^\/([^\/]|\\/)+[^\\]\/[ismU]*}.freeze + PATTERN = %r{^\/([^\/]|\\/)+[^\\]\/[ismU]*}.freeze def initialize(regexp) - @value = self.class.eager_matching_with_escape_characters? ? regexp.gsub(/\\\//, '/') : regexp + @value = regexp.gsub(/\\\//, '/') unless Gitlab::UntrustedRegexp::RubySyntax.valid?(@value) raise Lexer::SyntaxError, 'Invalid regular expression!' @@ -26,16 +25,12 @@ module Gitlab end def self.pattern - eager_matching_with_escape_characters? ? NEW_PATTERN : PATTERN + PATTERN end def self.build(string) new(string) end - - def self.eager_matching_with_escape_characters? - Feature.enabled?(:ci_variables_complex_expressions, default_enabled: true) - end end end end diff --git a/lib/gitlab/ci/pipeline/expression/lexer.rb b/lib/gitlab/ci/pipeline/expression/lexer.rb index 22c210ae26b..7d7582612f9 100644 --- a/lib/gitlab/ci/pipeline/expression/lexer.rb +++ b/lib/gitlab/ci/pipeline/expression/lexer.rb @@ -17,17 +17,6 @@ module Gitlab Expression::Lexeme::Equals, Expression::Lexeme::Matches, Expression::Lexeme::NotEquals, - Expression::Lexeme::NotMatches - ].freeze - - NEW_LEXEMES = [ - Expression::Lexeme::Variable, - Expression::Lexeme::String, - Expression::Lexeme::Pattern, - Expression::Lexeme::Null, - Expression::Lexeme::Equals, - Expression::Lexeme::Matches, - Expression::Lexeme::NotEquals, Expression::Lexeme::NotMatches, Expression::Lexeme::And, Expression::Lexeme::Or @@ -58,7 +47,7 @@ module Gitlab return tokens if @scanner.eos? - lexeme = available_lexemes.find do |type| + lexeme = LEXEMES.find do |type| type.scan(@scanner).tap do |token| tokens.push(token) if token.present? end @@ -71,10 +60,6 @@ module Gitlab raise Lexer::SyntaxError, 'Too many tokens!' end - - def available_lexemes - Feature.enabled?(:ci_variables_complex_expressions, default_enabled: true) ? NEW_LEXEMES : LEXEMES - end end end end diff --git a/lib/gitlab/ci/pipeline/expression/parser.rb b/lib/gitlab/ci/pipeline/expression/parser.rb index 589bf32a4d7..edb55edf356 100644 --- a/lib/gitlab/ci/pipeline/expression/parser.rb +++ b/lib/gitlab/ci/pipeline/expression/parser.rb @@ -13,39 +13,6 @@ module Gitlab end def tree - if Feature.enabled?(:ci_variables_complex_expressions, default_enabled: true) - rpn_parse_tree - else - reverse_descent_parse_tree - end - end - - def self.seed(statement) - new(Expression::Lexer.new(statement).tokens) - end - - private - - # This produces a reverse descent parse tree. - # It does not support precedence of operators. - def reverse_descent_parse_tree - while token = @tokens.next - case token.type - when :operator - token.build(@nodes.pop, tree).tap do |node| - @nodes.push(node) - end - when :value - token.build.tap do |leaf| - @nodes.push(leaf) - end - end - end - rescue StopIteration - @nodes.last || Lexeme::Null.new - end - - def rpn_parse_tree results = [] tokens_rpn.each do |token| @@ -70,6 +37,12 @@ module Gitlab results.pop end + def self.seed(statement) + new(Expression::Lexer.new(statement).tokens) + end + + private + # Parse the expression into Reverse Polish Notation # (See: Shunting-yard algorithm) def tokens_rpn 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 f176771775e..89eccce69f6 100644 --- a/lib/gitlab/ci/templates/Security/Dependency-Scanning.gitlab-ci.yml +++ b/lib/gitlab/ci/templates/Security/Dependency-Scanning.gitlab-ci.yml @@ -41,6 +41,8 @@ dependency_scanning: DS_PULL_ANALYZER_IMAGE_TIMEOUT \ DS_RUN_ANALYZER_TIMEOUT \ DS_PYTHON_VERSION \ + PIP_INDEX_URL \ + PIP_EXTRA_INDEX_URL \ ) \ --volume "$PWD:/code" \ --volume /var/run/docker.sock:/var/run/docker.sock \ diff --git a/lib/gitlab/git/repository.rb b/lib/gitlab/git/repository.rb index b7b7578cef9..a7d9ba51277 100644 --- a/lib/gitlab/git/repository.rb +++ b/lib/gitlab/git/repository.rb @@ -464,6 +464,18 @@ module Gitlab end end + # Returns path to url mappings for submodules + # + # Ex. + # @repository.submodule_urls_for('master') + # # => { 'rack' => 'git@localhost:rack.git' } + # + def submodule_urls_for(ref) + wrapped_gitaly_errors do + gitaly_submodule_urls_for(ref) + end + end + # Return total commits count accessible from passed ref def commit_count(ref) wrapped_gitaly_errors do @@ -1059,12 +1071,16 @@ module Gitlab return unless commit_object && commit_object.type == :COMMIT + urls = gitaly_submodule_urls_for(ref) + urls && urls[path] + end + + def gitaly_submodule_urls_for(ref) gitmodules = gitaly_commit_client.tree_entry(ref, '.gitmodules', Gitlab::Git::Blob::MAX_DATA_DISPLAY_SIZE) return unless gitmodules - found_module = GitmodulesParser.new(gitmodules.data).parse[path] - - found_module && found_module['url'] + submodules = GitmodulesParser.new(gitmodules.data).parse + submodules.transform_values { |submodule| submodule['url'] } end # Returns true if the given ref name exists diff --git a/lib/gitlab/gitaly_client.rb b/lib/gitlab/gitaly_client.rb index 3cafa49ec51..e7319a7b7f0 100644 --- a/lib/gitlab/gitaly_client.rb +++ b/lib/gitlab/gitaly_client.rb @@ -211,8 +211,7 @@ module Gitlab metadata['call_site'] = feature.to_s if feature metadata['gitaly-servers'] = address_metadata(remote_storage) if remote_storage metadata['x-gitlab-correlation-id'] = Labkit::Correlation::CorrelationId.current_id if Labkit::Correlation::CorrelationId.current_id - metadata['gitaly-session-id'] = session_id if Feature::Gitaly.enabled?(Feature::Gitaly::CATFILE_CACHE) - + metadata['gitaly-session-id'] = session_id metadata.merge!(Feature::Gitaly.server_feature_flags) result = { metadata: metadata } diff --git a/lib/gitlab/gitaly_client/repository_service.rb b/lib/gitlab/gitaly_client/repository_service.rb index d8e9dccb644..ca3e5b51ecc 100644 --- a/lib/gitlab/gitaly_client/repository_service.rb +++ b/lib/gitlab/gitaly_client/repository_service.rb @@ -5,7 +5,7 @@ module Gitlab class RepositoryService include Gitlab::EncodingHelper - MAX_MSG_SIZE = 128.kilobytes.freeze + MAX_MSG_SIZE = 128.kilobytes def initialize(repository) @repository = repository diff --git a/lib/gitlab/graphql/representation/submodule_tree_entry.rb b/lib/gitlab/graphql/representation/submodule_tree_entry.rb new file mode 100644 index 00000000000..65716dff75d --- /dev/null +++ b/lib/gitlab/graphql/representation/submodule_tree_entry.rb @@ -0,0 +1,34 @@ +# frozen_string_literal: true + +module Gitlab + module Graphql + module Representation + class SubmoduleTreeEntry < SimpleDelegator + class << self + def decorate(submodules, tree) + repository = tree.repository + submodule_links = Gitlab::SubmoduleLinks.new(repository) + + submodules.map do |submodule| + self.new(submodule, submodule_links.for(submodule, tree.sha)) + end + end + end + + def initialize(submodule, submodule_links) + @submodule_links = submodule_links + + super(submodule) + end + + def web_url + @submodule_links.first + end + + def tree_url + @submodule_links.last + end + end + end + end +end diff --git a/lib/gitlab/import_export/relation_factory.rb b/lib/gitlab/import_export/relation_factory.rb index efd3f550a22..1b545b1d049 100644 --- a/lib/gitlab/import_export/relation_factory.rb +++ b/lib/gitlab/import_export/relation_factory.rb @@ -28,7 +28,7 @@ module Gitlab links: 'Releases::Link', metrics_setting: 'ProjectMetricsSetting' }.freeze - USER_REFERENCES = %w[author_id assignee_id updated_by_id merged_by_id latest_closed_by_id user_id created_by_id last_edited_by_id merge_user_id resolved_by_id closed_by_id].freeze + USER_REFERENCES = %w[author_id assignee_id updated_by_id merged_by_id latest_closed_by_id user_id created_by_id last_edited_by_id merge_user_id resolved_by_id closed_by_id owner_id].freeze PROJECT_REFERENCES = %w[project_id source_project_id target_project_id].freeze @@ -78,6 +78,9 @@ module Gitlab def create return if unknown_service? + # Do not import legacy triggers + return if !Feature.enabled?(:use_legacy_pipeline_triggers, @project) && legacy_trigger? + setup_models generate_imported_object @@ -278,6 +281,10 @@ module Gitlab !Object.const_defined?(parsed_relation_hash['type']) end + def legacy_trigger? + @relation_name == 'Ci::Trigger' && @relation_hash['owner_id'].nil? + end + def find_or_create_object! return relation_class.find_or_create_by(project_id: @project.id) if @relation_name == :project_feature diff --git a/lib/gitlab/submodule_links.rb b/lib/gitlab/submodule_links.rb new file mode 100644 index 00000000000..a6c0369d864 --- /dev/null +++ b/lib/gitlab/submodule_links.rb @@ -0,0 +1,26 @@ +# frozen_string_literal: true + +module Gitlab + class SubmoduleLinks + include Gitlab::Utils::StrongMemoize + + def initialize(repository) + @repository = repository + end + + def for(submodule, sha) + submodule_url = submodule_url_for(sha)[submodule.path] + SubmoduleHelper.submodule_links_for_url(submodule.id, submodule_url, repository) + end + + private + + attr_reader :repository + + def submodule_url_for(sha) + strong_memoize(:"submodule_links_for_#{sha}") do + repository.submodule_urls_for(sha) + end + end + end +end diff --git a/lib/gitlab/usage_data.rb b/lib/gitlab/usage_data.rb index 0180fe7fa71..055e01a9399 100644 --- a/lib/gitlab/usage_data.rb +++ b/lib/gitlab/usage_data.rb @@ -130,7 +130,7 @@ module Gitlab def usage_counters { - web_ide_commits: Gitlab::WebIdeCommitsCounter.total_count + web_ide_commits: Gitlab::UsageDataCounters::WebIdeCommitsCounter.total_count } end diff --git a/lib/gitlab/usage_data_counters/redis_counter.rb b/lib/gitlab/usage_data_counters/redis_counter.rb new file mode 100644 index 00000000000..123b8e1bef1 --- /dev/null +++ b/lib/gitlab/usage_data_counters/redis_counter.rb @@ -0,0 +1,19 @@ +# frozen_string_literal: true + +module Gitlab + module UsageDataCounters + module RedisCounter + def increment + Gitlab::Redis::SharedState.with { |redis| redis.incr(redis_counter_key) } + end + + def total_count + Gitlab::Redis::SharedState.with { |redis| redis.get(redis_counter_key).to_i } + end + + def redis_counter_key + raise NotImplementedError + end + end + end +end diff --git a/lib/gitlab/usage_data_counters/web_ide_commits_counter.rb b/lib/gitlab/usage_data_counters/web_ide_commits_counter.rb new file mode 100644 index 00000000000..62236fa07a3 --- /dev/null +++ b/lib/gitlab/usage_data_counters/web_ide_commits_counter.rb @@ -0,0 +1,13 @@ +# frozen_string_literal: true + +module Gitlab + module UsageDataCounters + class WebIdeCommitsCounter + extend RedisCounter + + def self.redis_counter_key + 'WEB_IDE_COMMITS_COUNT' + end + end + end +end diff --git a/lib/gitlab/web_ide_commits_counter.rb b/lib/gitlab/web_ide_commits_counter.rb deleted file mode 100644 index 1cd9b5295b9..00000000000 --- a/lib/gitlab/web_ide_commits_counter.rb +++ /dev/null @@ -1,17 +0,0 @@ -# frozen_string_literal: true - -module Gitlab - module WebIdeCommitsCounter - WEB_IDE_COMMITS_KEY = "WEB_IDE_COMMITS_COUNT".freeze - - class << self - def increment - Gitlab::Redis::SharedState.with { |redis| redis.incr(WEB_IDE_COMMITS_KEY) } - end - - def total_count - Gitlab::Redis::SharedState.with { |redis| redis.get(WEB_IDE_COMMITS_KEY).to_i } - end - end - end -end diff --git a/lib/gitlab/zoom_link_extractor.rb b/lib/gitlab/zoom_link_extractor.rb new file mode 100644 index 00000000000..d9994898a08 --- /dev/null +++ b/lib/gitlab/zoom_link_extractor.rb @@ -0,0 +1,21 @@ +# frozen_string_literal: true + +# Detect links matching the following formats: +# Zoom Start links: https://zoom.us/s/<meeting-id> +# Zoom Join links: https://zoom.us/j/<meeting-id> +# Personal Zoom links: https://zoom.us/my/<meeting-id> +# Vanity Zoom links: https://gitlab.zoom.us/j/<meeting-id> (also /s and /my) + +module Gitlab + class ZoomLinkExtractor + ZOOM_REGEXP = %r{https://(?:[\w-]+\.)?zoom\.us/(?:s|j|my)/\S+}.freeze + + def initialize(text) + @text = text.to_s + end + + def links + @text.scan(ZOOM_REGEXP) + end + end +end diff --git a/lib/tasks/gitlab/features.rake b/lib/tasks/gitlab/features.rake index d88bcca0819..9cf568c07fe 100644 --- a/lib/tasks/gitlab/features.rake +++ b/lib/tasks/gitlab/features.rake @@ -10,14 +10,22 @@ namespace :gitlab do set_rugged_feature_flags(false) puts 'All Rugged feature flags were disabled.' end + + task unset_rugged: :environment do + set_rugged_feature_flags(nil) + puts 'All Rugged feature flags were unset.' + end end def set_rugged_feature_flags(status) Gitlab::Git::RuggedImpl::Repository::FEATURE_FLAGS.each do |flag| - if status - Feature.enable(flag) - else + case status + when nil Feature.get(flag).remove + when true + Feature.enable(flag) + when false + Feature.disable(flag) end end end diff --git a/lib/tasks/gitlab/seed.rake b/lib/tasks/gitlab/seed.rake index 155ba979b36..d76e38b73b5 100644 --- a/lib/tasks/gitlab/seed.rake +++ b/lib/tasks/gitlab/seed.rake @@ -1,7 +1,9 @@ namespace :gitlab do namespace :seed do desc "GitLab | Seed | Seeds issues" - task :issues, [:project_full_path] => :environment do |t, args| + task :issues, [:project_full_path, :backfill_weeks, :average_issues_per_week] => :environment do |t, args| + args.with_defaults(backfill_weeks: 5, average_issues_per_week: 2) + projects = if args.project_full_path project = Project.find_by_full_path(args.project_full_path) @@ -26,7 +28,8 @@ namespace :gitlab do projects.each do |project| puts "\nSeeding issues for the '#{project.full_path}' project" seeder = Quality::Seeders::Issues.new(project: project) - issues_created = seeder.seed(backfill_weeks: 5, average_issues_per_week: 2) + issues_created = seeder.seed(backfill_weeks: args.backfill_weeks.to_i, + average_issues_per_week: args.average_issues_per_week.to_i) puts "\n#{issues_created} issues created!" end end diff --git a/locale/gitlab.pot b/locale/gitlab.pot index 49224502958..7b742660a4c 100644 --- a/locale/gitlab.pot +++ b/locale/gitlab.pot @@ -138,9 +138,15 @@ msgstr[1] "" msgid "%{edit_in_new_fork_notice} Try to cherry-pick this commit again." msgstr "" +msgid "%{edit_in_new_fork_notice} Try to create a new directory again." +msgstr "" + msgid "%{edit_in_new_fork_notice} Try to revert this commit again." msgstr "" +msgid "%{edit_in_new_fork_notice} Try to upload a file again." +msgstr "" + msgid "%{filePath} deleted" msgstr "" @@ -704,6 +710,9 @@ msgstr "" msgid "Add to review" msgstr "" +msgid "Add to tree" +msgstr "" + msgid "Add user(s) to the group:" msgstr "" @@ -2978,6 +2987,12 @@ msgstr "" msgid "ContainerRegistry|Container Registry" msgstr "" +msgid "ContainerRegistry|Copy build command to clipboard" +msgstr "" + +msgid "ContainerRegistry|Copy push command to clipboard" +msgstr "" + msgid "ContainerRegistry|Docker connection error" msgstr "" @@ -3011,13 +3026,13 @@ msgstr "" msgid "ContainerRegistry|There are no container images stored for this project" msgstr "" -msgid "ContainerRegistry|We are having trouble connecting to Docker, which could be due to an issue with your project name or path. For more information, please review the %{docLinkStart}Container Registry documentation%{docLinkEnd}." +msgid "ContainerRegistry|We are having trouble connecting to Docker, which could be due to an issue with your project name or path. %{docLinkStart}More Information%{docLinkEnd}" msgstr "" -msgid "ContainerRegistry|With the Container Registry, every project can have its own space to store its Docker images. Learn more about the %{docLinkStart}Container Registry%{docLinkEnd}." +msgid "ContainerRegistry|With the Container Registry, every project can have its own space to store its Docker images. %{docLinkStart}More Information%{docLinkEnd}" msgstr "" -msgid "ContainerRegistry|With the Docker Container Registry integrated into GitLab, every project can have its own space to store its Docker images. Learn more about the %{docLinkStart}Container Registry%{docLinkEnd}." +msgid "ContainerRegistry|With the Docker Container Registry integrated into GitLab, every project can have its own space to store its Docker images. %{docLinkStart}More Information%{docLinkEnd}" msgstr "" msgid "ContainerRegistry|You are about to delete the image <b>%{title}</b>. This will delete the image and all tags pointing to this image." @@ -5702,6 +5717,21 @@ msgstr "" msgid "IssueBoards|Boards" msgstr "" +msgid "IssueBoards|Create new board" +msgstr "" + +msgid "IssueBoards|Delete board" +msgstr "" + +msgid "IssueBoards|No matching boards found" +msgstr "" + +msgid "IssueBoards|Some of your boards are hidden, activate a license to see them again." +msgstr "" + +msgid "IssueBoards|Switch board" +msgstr "" + msgid "IssueTracker|Bugzilla issue tracker" msgstr "" @@ -8683,6 +8713,9 @@ msgstr "" msgid "Receive notifications about your own activity" msgstr "" +msgid "Recent" +msgstr "" + msgid "Recent Project Activity" msgstr "" @@ -9727,6 +9760,9 @@ msgstr "" msgid "Some email servers do not support overriding the email sender name. Enable this option to include the name of the author of the issue, merge request or comment in the email body instead." msgstr "" +msgid "Someone edited the issue at the same time you did. Please check out %{linkStart}the issue%{linkEnd} and make sure your changes will not unintentionally remove theirs." +msgstr "" + msgid "Someone edited this %{issueType} at the same time you did. The description has been updated and you will need to make your changes again." msgstr "" @@ -12510,6 +12546,9 @@ msgstr "" msgid "You'll need to use different branch names to get a valid comparison." msgstr "" +msgid "You're not allowed to %{tag_start}edit%{tag_end} files in this project directly. Please fork this project, make your changes there, and submit a merge request." +msgstr "" + msgid "You're not allowed to make changes to this project directly. A fork of this project has been created that you can make changes in, so you can submit a merge request." msgstr "" diff --git a/package.json b/package.json index 44aa850860e..4ba9a0d9a1b 100644 --- a/package.json +++ b/package.json @@ -191,6 +191,7 @@ "pixelmatch": "^4.0.2", "postcss": "^7.0.14", "prettier": "1.18.2", + "readdir-enhanced": "^2.2.4", "stylelint": "^9.10.1", "stylelint-config-recommended": "^2.1.0", "stylelint-scss": "^3.5.4", diff --git a/qa/qa/page/main/oauth.rb b/qa/qa/page/main/oauth.rb index 5f6ddb9a114..2b1a9ab2b6a 100644 --- a/qa/qa/page/main/oauth.rb +++ b/qa/qa/page/main/oauth.rb @@ -5,7 +5,7 @@ module QA module Main class OAuth < Page::Base view 'app/views/doorkeeper/authorizations/new.html.haml' do - element :authorization_button, 'submit_tag _("Authorize")' # rubocop:disable QA/ElementWithPattern + element :authorization_button end def needs_authorization? @@ -13,7 +13,7 @@ module QA end def authorize! - click_button 'Authorize' + click_element :authorization_button end end end diff --git a/qa/qa/page/main/sign_up.rb b/qa/qa/page/main/sign_up.rb index 46a105003d0..c47d2ce9c74 100644 --- a/qa/qa/page/main/sign_up.rb +++ b/qa/qa/page/main/sign_up.rb @@ -5,28 +5,28 @@ module QA module Main class SignUp < Page::Base view 'app/views/devise/shared/_signup_box.html.haml' do - element :new_user_name - element :new_user_username - element :new_user_email - element :new_user_email_confirmation - element :new_user_password + element :new_user_name_field + element :new_user_username_field + element :new_user_email_field + element :new_user_email_confirmation_field + element :new_user_password_field element :new_user_register_button - element :new_user_accept_terms + element :new_user_accept_terms_checkbox end def sign_up!(user) - fill_element :new_user_name, user.name - fill_element :new_user_username, user.username - fill_element :new_user_email, user.email - fill_element :new_user_email_confirmation, user.email - fill_element :new_user_password, user.password + fill_element :new_user_name_field, user.name + fill_element :new_user_username_field, user.username + fill_element :new_user_email_field, user.email + fill_element :new_user_email_confirmation_field, user.email + fill_element :new_user_password_field, user.password - check_element :new_user_accept_terms if has_element?(:new_user_accept_terms) + check_element :new_user_accept_terms_checkbox if has_element?(:new_user_accept_terms_checkbox) signed_in = retry_until do click_element :new_user_register_button - Page::Main::Menu.act { has_personal_area? } + Page::Main::Menu.perform(&:has_personal_area?) end raise "Failed to register and sign in" unless signed_in diff --git a/qa/qa/page/project/operations/kubernetes/show.rb b/qa/qa/page/project/operations/kubernetes/show.rb index 4f625c5f0f0..eb30e0ea02a 100644 --- a/qa/qa/page/project/operations/kubernetes/show.rb +++ b/qa/qa/page/project/operations/kubernetes/show.rb @@ -28,16 +28,12 @@ module QA end end - def await_installed(application_name, button_text: 'Installed') + def await_installed(application_name) within(".js-cluster-application-row-#{application_name}") do - page.has_text?(button_text, wait: 300) + page.has_text?(/Installed|Uninstall/, wait: 300) end end - def await_uninstallable(application_name) - await_installed(application_name, button_text: 'Uninstall') - end - def ingress_ip # We need to wait longer since it can take some time before the # ip address is assigned for the ingress controller diff --git a/qa/qa/page/project/sub_menus/common.rb b/qa/qa/page/project/sub_menus/common.rb index c94e1e85256..3c9e8085748 100644 --- a/qa/qa/page/project/sub_menus/common.rb +++ b/qa/qa/page/project/sub_menus/common.rb @@ -12,7 +12,11 @@ module QA end def within_submenu - within('.fly-out-list') do + if has_css?('.fly-out-list') + within('.fly-out-list') do + yield + end + else yield end end diff --git a/qa/qa/resource/kubernetes_cluster.rb b/qa/qa/resource/kubernetes_cluster.rb index 1dd93dd5b88..27ab7b60211 100644 --- a/qa/qa/resource/kubernetes_cluster.rb +++ b/qa/qa/resource/kubernetes_cluster.rb @@ -47,7 +47,7 @@ module QA page.install!(:runner) if @install_runner page.await_installed(:ingress) if @install_ingress - page.await_uninstallable(:prometheus) if @install_prometheus + page.await_installed(:prometheus) if @install_prometheus page.await_installed(:runner) if @install_runner if @install_ingress diff --git a/qa/qa/resource/merge_request.rb b/qa/qa/resource/merge_request.rb index 45cb317e0eb..7969de726e4 100644 --- a/qa/qa/resource/merge_request.rb +++ b/qa/qa/resource/merge_request.rb @@ -9,6 +9,7 @@ module QA :description, :source_branch, :target_branch, + :target_new_branch, :assignee, :milestone, :labels, @@ -27,6 +28,7 @@ module QA Repository::ProjectPush.fabricate! do |resource| resource.project = project resource.branch_name = 'master' + resource.new_branch = @target_new_branch resource.remote_branch = target_branch end end @@ -52,6 +54,7 @@ module QA @labels = [] @file_name = "added_file.txt" @file_content = "File Added" + @target_new_branch = true end def fabricate! diff --git a/scripts/lint-doc.sh b/scripts/lint-doc.sh index 2055ce7f09d..8c9b8b9fb02 100755 --- a/scripts/lint-doc.sh +++ b/scripts/lint-doc.sh @@ -45,7 +45,7 @@ then then echo echo ' ✖ ERROR: New README.md file(s) detected, prefer index.md over README.md.' >&2 - echo ' https://docs.gitlab.com/ee/development/writing_documentation.html#location-and-naming-documents' + echo ' https://docs.gitlab.com/ee/development/documentation/styleguide.html#working-with-directories-and-files' echo exit 1 fi @@ -55,7 +55,7 @@ then then echo echo ' ✖ ERROR: New README.md file(s) detected, prefer index.md over README.md.' >&2 - echo ' https://docs.gitlab.com/ee/development/writing_documentation.html#location-and-naming-documents' + echo ' https://docs.gitlab.com/ee/development/documentation/styleguide.html#working-with-directories-and-files' echo exit 1 fi diff --git a/spec/controllers/admin/application_settings_controller_spec.rb b/spec/controllers/admin/application_settings_controller_spec.rb index 5ad5f9cdeea..4eb0545eb6c 100644 --- a/spec/controllers/admin/application_settings_controller_spec.rb +++ b/spec/controllers/admin/application_settings_controller_spec.rb @@ -41,7 +41,7 @@ describe Admin::ApplicationSettingsController do it 'returns JSON data' do get :usage_data, format: :json - body = JSON.parse(response.body) + body = json_response expect(body["version"]).to eq(Gitlab::VERSION) expect(body).to include('counts') expect(response.status).to eq(200) diff --git a/spec/controllers/application_controller_spec.rb b/spec/controllers/application_controller_spec.rb index 447a12b2fac..84bbbac39b0 100644 --- a/spec/controllers/application_controller_spec.rb +++ b/spec/controllers/application_controller_spec.rb @@ -63,8 +63,6 @@ describe ApplicationController do sign_in user end - let(:json_response) { JSON.parse(response.body) } - controller(described_class) do def index render json: Gon.all_variables diff --git a/spec/controllers/boards/issues_controller_spec.rb b/spec/controllers/boards/issues_controller_spec.rb index 6cad060d888..0db58fbefc1 100644 --- a/spec/controllers/boards/issues_controller_spec.rb +++ b/spec/controllers/boards/issues_controller_spec.rb @@ -52,10 +52,8 @@ describe Boards::IssuesController do list_issues user: user, board: board, list: list2 - parsed_response = JSON.parse(response.body) - expect(response).to match_response_schema('entities/issue_boards') - expect(parsed_response['issues'].length).to eq 2 + expect(json_response['issues'].length).to eq 2 expect(development.issues.map(&:relative_position)).not_to include(nil) end @@ -123,10 +121,8 @@ describe Boards::IssuesController do list_issues user: user, board: board - parsed_response = JSON.parse(response.body) - expect(response).to match_response_schema('entities/issue_boards') - expect(parsed_response['issues'].length).to eq 2 + expect(json_response['issues'].length).to eq 2 end end @@ -164,7 +160,7 @@ describe Boards::IssuesController do end end - describe 'PUT move_multiple' do + describe 'PUT bulk_move' do let(:todo) { create(:group_label, group: group, name: 'Todo') } let(:development) { create(:group_label, group: group, name: 'Development') } let(:user) { create(:group_member, :maintainer, user: create(:user), group: group ).user } @@ -200,6 +196,20 @@ describe Boards::IssuesController do sign_in(signed_in_user) end + it 'responds as expected' do + put :bulk_move, params: move_issues_params + expect(response).to have_gitlab_http_status(expected_status) + + if expected_status == 200 + expect(json_response).to include( + 'count' => move_issues_params[:ids].size, + 'success' => true + ) + + expect(json_response['issues'].pluck('id')).to match_array(move_issues_params[:ids]) + end + end + it 'moves issues as expected' do put :bulk_move, params: move_issues_params expect(response).to have_gitlab_http_status(expected_status) diff --git a/spec/controllers/boards/lists_controller_spec.rb b/spec/controllers/boards/lists_controller_spec.rb index e1f75fa3395..418ca6f3210 100644 --- a/spec/controllers/boards/lists_controller_spec.rb +++ b/spec/controllers/boards/lists_controller_spec.rb @@ -26,10 +26,8 @@ describe Boards::ListsController do read_board_list user: user, board: board - parsed_response = JSON.parse(response.body) - expect(response).to match_response_schema('lists') - expect(parsed_response.length).to eq 3 + expect(json_response.length).to eq 3 end context 'with unauthorized user' do diff --git a/spec/controllers/groups/boards_controller_spec.rb b/spec/controllers/groups/boards_controller_spec.rb index 5e0f64ccca4..e4232c2c1ab 100644 --- a/spec/controllers/groups/boards_controller_spec.rb +++ b/spec/controllers/groups/boards_controller_spec.rb @@ -63,10 +63,8 @@ describe Groups::BoardsController do list_boards format: :json - parsed_response = JSON.parse(response.body) - expect(response).to match_response_schema('boards') - expect(parsed_response.length).to eq 1 + expect(json_response.length).to eq 1 end context 'with unauthorized user' do diff --git a/spec/controllers/groups/milestones_controller_spec.rb b/spec/controllers/groups/milestones_controller_spec.rb index 19b18091aef..bf164aeed38 100644 --- a/spec/controllers/groups/milestones_controller_spec.rb +++ b/spec/controllers/groups/milestones_controller_spec.rb @@ -73,7 +73,7 @@ describe Groups::MilestonesController do it 'lists legacy group milestones and group milestones' do get :index, params: { group_id: group.to_param }, format: :json - milestones = JSON.parse(response.body) + milestones = json_response expect(milestones.count).to eq(2) expect(milestones.first["title"]).to eq("group milestone") diff --git a/spec/controllers/health_check_controller_spec.rb b/spec/controllers/health_check_controller_spec.rb index 19d739fcf4f..92f005faf4a 100644 --- a/spec/controllers/health_check_controller_spec.rb +++ b/spec/controllers/health_check_controller_spec.rb @@ -5,7 +5,6 @@ require 'spec_helper' describe HealthCheckController do include StubENV - let(:json_response) { JSON.parse(response.body) } let(:xml_response) { Hash.from_xml(response.body)['hash'] } let(:token) { Gitlab::CurrentSettings.health_check_access_token } let(:whitelisted_ip) { '127.0.0.1' } diff --git a/spec/controllers/health_controller_spec.rb b/spec/controllers/health_controller_spec.rb index fc62a8310aa..e82dcfcdb64 100644 --- a/spec/controllers/health_controller_spec.rb +++ b/spec/controllers/health_controller_spec.rb @@ -5,7 +5,6 @@ require 'spec_helper' describe HealthController do include StubENV - let(:json_response) { JSON.parse(response.body) } let(:token) { Gitlab::CurrentSettings.health_check_access_token } let(:whitelisted_ip) { '127.0.0.1' } let(:not_whitelisted_ip) { '127.0.0.2' } diff --git a/spec/controllers/metrics_controller_spec.rb b/spec/controllers/metrics_controller_spec.rb index ee454a7818c..84027119491 100644 --- a/spec/controllers/metrics_controller_spec.rb +++ b/spec/controllers/metrics_controller_spec.rb @@ -5,7 +5,6 @@ require 'spec_helper' describe MetricsController do include StubENV - let(:json_response) { JSON.parse(response.body) } let(:metrics_multiproc_dir) { Dir.mktmpdir } let(:whitelisted_ip) { '127.0.0.1' } let(:whitelisted_ip_range) { '10.0.0.0/24' } diff --git a/spec/controllers/projects/blob_controller_spec.rb b/spec/controllers/projects/blob_controller_spec.rb index 44500d3cde3..45aebd1554c 100644 --- a/spec/controllers/projects/blob_controller_spec.rb +++ b/spec/controllers/projects/blob_controller_spec.rb @@ -160,7 +160,7 @@ describe Projects::BlobController do it 'renders diff context lines Gitlab::Diff::Line array' do do_get(since: 1, to: 2, offset: 0, from_merge_request: true) - lines = JSON.parse(response.body) + lines = json_response expect(lines.size).to eq(diff_lines.size) lines.each do |line| @@ -173,7 +173,7 @@ describe Projects::BlobController do it 'handles full being true' do do_get(full: true, from_merge_request: true) - lines = JSON.parse(response.body) + lines = json_response expect(lines.size).to eq(diff_lines.size) end diff --git a/spec/controllers/projects/boards_controller_spec.rb b/spec/controllers/projects/boards_controller_spec.rb index c07afc57aea..543479d8dd5 100644 --- a/spec/controllers/projects/boards_controller_spec.rb +++ b/spec/controllers/projects/boards_controller_spec.rb @@ -69,10 +69,8 @@ describe Projects::BoardsController do list_boards format: :json - parsed_response = JSON.parse(response.body) - expect(response).to match_response_schema('boards') - expect(parsed_response.length).to eq 2 + expect(json_response.length).to eq 2 end context 'with unauthorized user' do diff --git a/spec/controllers/projects/branches_controller_spec.rb b/spec/controllers/projects/branches_controller_spec.rb index b30966e70a7..f5bcea4a097 100644 --- a/spec/controllers/projects/branches_controller_spec.rb +++ b/spec/controllers/projects/branches_controller_spec.rb @@ -495,10 +495,8 @@ describe Projects::BranchesController do search: 'master' } - parsed_response = JSON.parse(response.body) - - expect(parsed_response.length).to eq 1 - expect(parsed_response.first).to eq 'master' + expect(json_response.length).to eq 1 + expect(json_response.first).to eq 'master' end end @@ -591,8 +589,7 @@ describe Projects::BranchesController do end it 'returns the commit counts behind and ahead of default branch' do - parsed_response = JSON.parse(response.body) - expect(parsed_response).to eq( + expect(json_response).to eq( "fix" => { "behind" => 29, "ahead" => 2 }, "branch-merged" => { "behind" => 1, "ahead" => 0 }, "add-pdf-file" => { "behind" => 0, "ahead" => 3 } diff --git a/spec/controllers/projects/commit_controller_spec.rb b/spec/controllers/projects/commit_controller_spec.rb index b5c6382a26d..58a1d96d010 100644 --- a/spec/controllers/projects/commit_controller_spec.rb +++ b/spec/controllers/projects/commit_controller_spec.rb @@ -378,8 +378,8 @@ describe Projects::CommitController do get_pipelines(id: commit.id, format: :json) expect(response).to be_ok - expect(JSON.parse(response.body)['pipelines']).not_to be_empty - expect(JSON.parse(response.body)['count']['all']).to eq 1 + expect(json_response['pipelines']).not_to be_empty + expect(json_response['count']['all']).to eq 1 expect(response).to include_pagination_headers end end diff --git a/spec/controllers/projects/compare_controller_spec.rb b/spec/controllers/projects/compare_controller_spec.rb index 92380a2bf09..48a92a772dc 100644 --- a/spec/controllers/projects/compare_controller_spec.rb +++ b/spec/controllers/projects/compare_controller_spec.rb @@ -302,8 +302,7 @@ describe Projects::CompareController do signatures_request expect(response).to have_gitlab_http_status(200) - parsed_body = JSON.parse(response.body) - signatures = parsed_body['signatures'] + signatures = json_response['signatures'] expect(signatures.size).to eq(1) expect(signatures.first['commit_sha']).to eq(signature_commit.sha) @@ -332,8 +331,7 @@ describe Projects::CompareController do signatures_request expect(response).to have_gitlab_http_status(200) - parsed_body = JSON.parse(response.body) - expect(parsed_body['signatures']).to be_empty + expect(json_response['signatures']).to be_empty end end @@ -345,8 +343,7 @@ describe Projects::CompareController do signatures_request expect(response).to have_gitlab_http_status(200) - parsed_body = JSON.parse(response.body) - expect(parsed_body['signatures']).to be_empty + expect(json_response['signatures']).to be_empty end end end diff --git a/spec/controllers/projects/deploy_keys_controller_spec.rb b/spec/controllers/projects/deploy_keys_controller_spec.rb index fcd14f13863..ccad76eaddd 100644 --- a/spec/controllers/projects/deploy_keys_controller_spec.rb +++ b/spec/controllers/projects/deploy_keys_controller_spec.rb @@ -52,12 +52,10 @@ describe Projects::DeployKeysController do it 'returns json in a correct format' do get :index, params: params.merge(format: :json) - json = JSON.parse(response.body) - - expect(json.keys).to match_array(%w(enabled_keys available_project_keys public_keys)) - expect(json['enabled_keys'].count).to eq(1) - expect(json['available_project_keys'].count).to eq(1) - expect(json['public_keys'].count).to eq(1) + expect(json_response.keys).to match_array(%w(enabled_keys available_project_keys public_keys)) + expect(json_response['enabled_keys'].count).to eq(1) + expect(json_response['available_project_keys'].count).to eq(1) + expect(json_response['public_keys'].count).to eq(1) end end end diff --git a/spec/controllers/projects/discussions_controller_spec.rb b/spec/controllers/projects/discussions_controller_spec.rb index 4c29162cd0f..e30b28a4bd5 100644 --- a/spec/controllers/projects/discussions_controller_spec.rb +++ b/spec/controllers/projects/discussions_controller_spec.rb @@ -112,7 +112,7 @@ describe Projects::DiscussionsController do it "returns the name of the resolving user" do post :resolve, params: request_params - expect(JSON.parse(response.body)['resolved_by']['name']).to eq(user.name) + expect(json_response['resolved_by']['name']).to eq(user.name) end it "returns status 200" do @@ -135,7 +135,7 @@ describe Projects::DiscussionsController do it "returns truncated diff lines" do post :resolve, params: request_params - expect(JSON.parse(response.body)['truncated_diff_lines']).to be_present + expect(json_response['truncated_diff_lines']).to be_present end end end diff --git a/spec/controllers/projects/environments_controller_spec.rb b/spec/controllers/projects/environments_controller_spec.rb index 4c2c6160c62..ebbbebf1bc0 100644 --- a/spec/controllers/projects/environments_controller_spec.rb +++ b/spec/controllers/projects/environments_controller_spec.rb @@ -3,6 +3,8 @@ require 'spec_helper' describe Projects::EnvironmentsController do + include MetricsDashboardHelpers + set(:user) { create(:user) } set(:project) { create(:project) } @@ -445,131 +447,186 @@ describe Projects::EnvironmentsController do end end - describe 'metrics_dashboard' do - context 'when prometheus endpoint is disabled' do - before do - stub_feature_flags(environment_metrics_use_prometheus_endpoint: false) - end + describe 'GET #metrics_dashboard' do + shared_examples_for 'correctly formatted response' do |status_code| + it 'returns a json object with the correct keys' do + get :metrics_dashboard, params: environment_params(dashboard_params) - it 'responds with status code 403' do - get :metrics_dashboard, params: environment_params(format: :json) + # Exlcude `all_dashboards` to handle separately. + found_keys = json_response.keys - ['all_dashboards'] - expect(response).to have_gitlab_http_status(:forbidden) + expect(response).to have_gitlab_http_status(status_code) + expect(found_keys).to contain_exactly(*expected_keys) end end - shared_examples_for '200 response' do |contains_all_dashboards: false| + shared_examples_for '200 response' do let(:expected_keys) { %w(dashboard status) } - before do - expected_keys << 'all_dashboards' if contains_all_dashboards - end - - it 'returns a json representation of the environment dashboard' do - get :metrics_dashboard, params: environment_params(dashboard_params) - - expect(response).to have_gitlab_http_status(:ok) - expect(json_response.keys).to contain_exactly(*expected_keys) - expect(json_response['dashboard']).to be_an_instance_of(Hash) - end + it_behaves_like 'correctly formatted response', :ok end - shared_examples_for 'error response' do |status_code, contains_all_dashboards: false| + shared_examples_for 'error response' do |status_code| let(:expected_keys) { %w(message status) } - before do - expected_keys << 'all_dashboards' if contains_all_dashboards - end + it_behaves_like 'correctly formatted response', status_code + end - it 'returns an error response' do + shared_examples_for 'includes all dashboards' do + it 'includes info for all findable dashboard' do get :metrics_dashboard, params: environment_params(dashboard_params) - expect(response).to have_gitlab_http_status(status_code) - expect(json_response.keys).to contain_exactly(*expected_keys) + expect(json_response).to have_key('all_dashboards') + expect(json_response['all_dashboards']).to be_an_instance_of(Array) + expect(json_response['all_dashboards']).to all( include('path', 'default', 'display_name') ) end end - shared_examples_for 'has all dashboards' do - it 'includes an index of all available dashboards' do + shared_examples_for 'the default dashboard' do + all_dashboards = Feature.enabled?(:environment_metrics_show_multiple_dashboards) + + it_behaves_like '200 response' + it_behaves_like 'includes all dashboards' if all_dashboards + + it 'is the default dashboard' do get :metrics_dashboard, params: environment_params(dashboard_params) - expect(json_response.keys).to include('all_dashboards') - expect(json_response['all_dashboards']).to be_an_instance_of(Array) - expect(json_response['all_dashboards']).to all( include('path', 'default') ) + expect(json_response['dashboard']['dashboard']).to eq('Environment metrics') end end - context 'when multiple dashboards is disabled' do - before do - stub_feature_flags(environment_metrics_show_multiple_dashboards: false) - end + shared_examples_for 'the specified dashboard' do |expected_dashboard| + it_behaves_like '200 response' + it_behaves_like 'includes all dashboards' - let(:dashboard_params) { { format: :json } } + it 'has the correct name' do + get :metrics_dashboard, params: environment_params(dashboard_params) - it_behaves_like '200 response' + dashboard_name = json_response['dashboard']['dashboard'] - context 'when the dashboard could not be provided' do + # 'Environment metrics' is the default dashboard. + expect(dashboard_name).not_to eq('Environment metrics') + expect(dashboard_name).to eq(expected_dashboard) + end + + context 'when the dashboard cannot not be processed' do before do allow(YAML).to receive(:safe_load).and_return({}) end it_behaves_like 'error response', :unprocessable_entity end - - context 'when a dashboard param is specified' do - let(:dashboard_params) { { format: :json, dashboard: '.gitlab/dashboards/not_there_dashboard.yml' } } - - it_behaves_like '200 response' - end end - context 'when multiple dashboards is enabled' do - let(:dashboard_params) { { format: :json } } + shared_examples_for 'the default dynamic dashboard' do + it_behaves_like '200 response' - it_behaves_like '200 response', contains_all_dashboards: true - it_behaves_like 'has all dashboards' + it 'contains only the Memory and CPU charts' do + get :metrics_dashboard, params: environment_params(dashboard_params) - context 'when a dashboard could not be provided' do - before do - allow(YAML).to receive(:safe_load).and_return({}) - end + dashboard = json_response['dashboard'] + panel_group = dashboard['panel_groups'].first + titles = panel_group['panels'].map { |panel| panel['title'] } - it_behaves_like 'error response', :unprocessable_entity, contains_all_dashboards: true - it_behaves_like 'has all dashboards' + expect(dashboard['dashboard']).to be_nil + expect(dashboard['panel_groups'].length).to eq 1 + expect(panel_group['group']).to be_nil + expect(titles).to eq ['Memory Usage (Total)', 'Core Usage (Total)'] end + end - context 'when a dashboard param is specified' do - let(:dashboard_params) { { format: :json, dashboard: '.gitlab/dashboards/test.yml' } } + shared_examples_for 'dashboard can be specified' do + context 'when dashboard is specified' do + let(:dashboard_path) { '.gitlab/dashboards/test.yml' } + let(:dashboard_params) { { format: :json, dashboard: dashboard_path } } + + it_behaves_like 'error response', :not_found - context 'when the dashboard is available' do + context 'when the project dashboard is available' do let(:dashboard_yml) { fixture_file('lib/gitlab/metrics/dashboard/sample_dashboard.yml') } - let(:dashboard_file) { { '.gitlab/dashboards/test.yml' => dashboard_yml } } - let(:project) { create(:project, :custom_repo, files: dashboard_file) } + let(:project) { project_with_dashboard(dashboard_path, dashboard_yml) } let(:environment) { create(:environment, name: 'production', project: project) } - it_behaves_like '200 response', contains_all_dashboards: true - it_behaves_like 'has all dashboards' + it_behaves_like 'the specified dashboard', 'Test Dashboard' end - context 'when the dashboard does not exist' do - it_behaves_like 'error response', :not_found, contains_all_dashboards: true - it_behaves_like 'has all dashboards' + context 'when the specified dashboard is the default dashboard' do + let(:dashboard_path) { Gitlab::Metrics::Dashboard::SystemDashboardService::SYSTEM_DASHBOARD_PATH } + + it_behaves_like 'the default dashboard' end end + end - context 'when the dashboard is intended for embedding' do + shared_examples_for 'dashboard can be embedded' do + context 'when the embedded flag is included' do let(:dashboard_params) { { format: :json, embedded: true } } - it_behaves_like '200 response' + it_behaves_like 'the default dynamic dashboard' - context 'when a dashboard path is provided' do - let(:dashboard_params) { { format: :json, dashboard: '.gitlab/dashboards/test.yml', embedded: true } } + context 'when the dashboard is specified' do + let(:dashboard_params) { { format: :json, embedded: true, dashboard: '.gitlab/dashboards/fake.yml' } } - # The dashboard path should simple be ignored. - it_behaves_like '200 response' + # The dashboard param should be ignored. + it_behaves_like 'the default dynamic dashboard' end end end + + shared_examples_for 'dashboard cannot be specified' do + context 'when dashboard is specified' do + let(:dashboard_params) { { format: :json, dashboard: '.gitlab/dashboards/test.yml' } } + + it_behaves_like 'the default dashboard' + end + end + + shared_examples_for 'dashboard cannot be embedded' do + context 'when the embedded flag is included' do + let(:dashboard_params) { { format: :json, embedded: true } } + + it_behaves_like 'the default dashboard' + end + end + + let(:dashboard_params) { { format: :json } } + + it_behaves_like 'the default dashboard' + it_behaves_like 'dashboard can be specified' + it_behaves_like 'dashboard can be embedded' + + context 'when multiple dashboards is enabled and embedding metrics is disabled' do + before do + stub_feature_flags(gfm_embedded_metrics: false) + end + + it_behaves_like 'the default dashboard' + it_behaves_like 'dashboard can be specified' + it_behaves_like 'dashboard cannot be embedded' + end + + context 'when multiple dashboards is disabled and embedding metrics is enabled' do + before do + stub_feature_flags(environment_metrics_show_multiple_dashboards: false) + end + + it_behaves_like 'the default dashboard' + it_behaves_like 'dashboard cannot be specified' + it_behaves_like 'dashboard can be embedded' + end + + context 'when multiple dashboards and embedding metrics are disabled' do + before do + stub_feature_flags( + environment_metrics_show_multiple_dashboards: false, + gfm_embedded_metrics: false + ) + end + + it_behaves_like 'the default dashboard' + it_behaves_like 'dashboard cannot be specified' + it_behaves_like 'dashboard cannot be embedded' + end end describe 'GET #search' do diff --git a/spec/controllers/projects/find_file_controller_spec.rb b/spec/controllers/projects/find_file_controller_spec.rb index 538dbb5ad0b..a493985f8a0 100644 --- a/spec/controllers/projects/find_file_controller_spec.rb +++ b/spec/controllers/projects/find_file_controller_spec.rb @@ -53,10 +53,9 @@ describe Projects::FindFileController do it 'returns an array of file path list' do go - json = JSON.parse(response.body) is_expected.to respond_with(:success) - expect(json).not_to eq(nil) - expect(json.length).to be >= 0 + expect(json_response).not_to eq(nil) + expect(json_response.length).to be >= 0 end end diff --git a/spec/controllers/projects/issues_controller_spec.rb b/spec/controllers/projects/issues_controller_spec.rb index bc5e0b4671e..32d14dce936 100644 --- a/spec/controllers/projects/issues_controller_spec.rb +++ b/spec/controllers/projects/issues_controller_spec.rb @@ -444,7 +444,7 @@ describe Projects::IssuesController do it 'renders json with recaptcha_html' do subject - expect(JSON.parse(response.body)).to have_key('recaptcha_html') + expect(json_response).to have_key('recaptcha_html') end end end @@ -484,10 +484,8 @@ describe Projects::IssuesController do it 'returns last edited time' do go(id: issue.iid) - data = JSON.parse(response.body) - - expect(data).to include('updated_at') - expect(data['updated_at']).to eq(issue.last_edited_at.to_time.iso8601) + expect(json_response).to include('updated_at') + expect(json_response['updated_at']).to eq(issue.last_edited_at.to_time.iso8601) end end @@ -520,10 +518,8 @@ describe Projects::IssuesController do it 'returns the necessary data' do go(id: issue.iid) - data = JSON.parse(response.body) - - expect(data).to include('title_text', 'description', 'description_text') - expect(data).to include('task_status', 'lock_version') + expect(json_response).to include('title_text', 'description', 'description_text') + expect(json_response).to include('task_status', 'lock_version') end end end @@ -692,9 +688,7 @@ describe Projects::IssuesController do update_issue(issue_params: { assignee_ids: [assignee.id] }) - body = JSON.parse(response.body) - - expect(body['assignees'].first.keys) + expect(json_response['assignees'].first.keys) .to match_array(%w(id name username avatar_url state web_url)) end end @@ -1314,7 +1308,7 @@ describe Projects::IssuesController do it 'filters notes that the user should not see' do get :discussions, params: { namespace_id: project.namespace, project_id: project, id: issue.iid } - expect(JSON.parse(response.body).count).to eq(1) + expect(json_response.count).to eq(1) end it 'does not result in N+1 queries' do diff --git a/spec/controllers/projects/merge_requests/diffs_controller_spec.rb b/spec/controllers/projects/merge_requests/diffs_controller_spec.rb index 13a28b738ca..d940d226176 100644 --- a/spec/controllers/projects/merge_requests/diffs_controller_spec.rb +++ b/spec/controllers/projects/merge_requests/diffs_controller_spec.rb @@ -112,7 +112,7 @@ describe Projects::MergeRequests::DiffsController do it 'only renders the diffs for the path given' do diff_for_path(old_path: existing_path, new_path: existing_path) - paths = JSON.parse(response.body)["diff_files"].map { |file| file['new_path'] } + paths = json_response["diff_files"].map { |file| file['new_path'] } expect(paths).to include(existing_path) end diff --git a/spec/controllers/projects/merge_requests_controller_spec.rb b/spec/controllers/projects/merge_requests_controller_spec.rb index cc6adc0a6c6..f11880122b1 100644 --- a/spec/controllers/projects/merge_requests_controller_spec.rb +++ b/spec/controllers/projects/merge_requests_controller_spec.rb @@ -242,9 +242,7 @@ describe Projects::MergeRequestsController do update_merge_request({ assignee_ids: [assignee.id] }, format: :json) - body = JSON.parse(response.body) - - expect(body['assignees']).to all(include(*%w(name username avatar_url id state web_url))) + expect(json_response['assignees']).to all(include(*%w(name username avatar_url id state web_url))) end end diff --git a/spec/controllers/projects/notes_controller_spec.rb b/spec/controllers/projects/notes_controller_spec.rb index 1db1963476c..98aea9056dc 100644 --- a/spec/controllers/projects/notes_controller_spec.rb +++ b/spec/controllers/projects/notes_controller_spec.rb @@ -29,7 +29,7 @@ describe Projects::NotesController do } end - let(:parsed_response) { JSON.parse(response.body).with_indifferent_access } + let(:parsed_response) { json_response.with_indifferent_access } let(:note_json) { parsed_response[:notes].first } before do @@ -614,7 +614,7 @@ describe Projects::NotesController do it "returns the name of the resolving user" do post :resolve, params: request_params.merge(html: true) - expect(JSON.parse(response.body)["resolved_by"]).to eq(user.name) + expect(json_response["resolved_by"]).to eq(user.name) end it "returns status 200" do diff --git a/spec/controllers/projects/templates_controller_spec.rb b/spec/controllers/projects/templates_controller_spec.rb index 9e7d34b10c0..d5ef2b0e114 100644 --- a/spec/controllers/projects/templates_controller_spec.rb +++ b/spec/controllers/projects/templates_controller_spec.rb @@ -7,7 +7,6 @@ describe Projects::TemplatesController do let(:user) { create(:user) } let(:file_path_1) { '.gitlab/issue_templates/issue_template.md' } let(:file_path_2) { '.gitlab/merge_request_templates/merge_request_template.md' } - let(:body) { JSON.parse(response.body) } let!(:file_1) { project.repository.create_file(user, file_path_1, 'issue content', message: 'message', branch_name: 'master') } let!(:file_2) { project.repository.create_file(user, file_path_2, 'merge request content', message: 'message', branch_name: 'master') } @@ -17,8 +16,8 @@ describe Projects::TemplatesController do get(:show, params: { namespace_id: project.namespace, template_type: 'issue', key: 'issue_template', project_id: project }, format: :json) expect(response.status).to eq(200) - expect(body['name']).to eq('issue_template') - expect(body['content']).to eq('issue content') + expect(json_response['name']).to eq('issue_template') + expect(json_response['content']).to eq('issue content') end end @@ -27,8 +26,8 @@ describe Projects::TemplatesController do get(:show, params: { namespace_id: project.namespace, template_type: 'merge_request', key: 'merge_request_template', project_id: project }, format: :json) expect(response.status).to eq(200) - expect(body['name']).to eq('merge_request_template') - expect(body['content']).to eq('merge request content') + expect(json_response['name']).to eq('merge_request_template') + expect(json_response['content']).to eq('merge request content') end end diff --git a/spec/controllers/projects/wikis_controller_spec.rb b/spec/controllers/projects/wikis_controller_spec.rb index f2e0b5e5c1d..a7e5a79b51d 100644 --- a/spec/controllers/projects/wikis_controller_spec.rb +++ b/spec/controllers/projects/wikis_controller_spec.rb @@ -103,7 +103,7 @@ describe Projects::WikisController do it 'renders json in a correct format' do post :preview_markdown, params: { namespace_id: project.namespace, project_id: project, id: 'page/path', text: '*Markdown* text' } - expect(JSON.parse(response.body).keys).to match_array(%w(body references)) + expect(json_response.keys).to match_array(%w(body references)) end end diff --git a/spec/controllers/projects_controller_spec.rb b/spec/controllers/projects_controller_spec.rb index 4e1cac67d23..083a1c1383a 100644 --- a/spec/controllers/projects_controller_spec.rb +++ b/spec/controllers/projects_controller_spec.rb @@ -740,20 +740,18 @@ describe ProjectsController do it 'gets a list of branches and tags' do get :refs, params: { namespace_id: project.namespace, id: project, sort: 'updated_desc' } - parsed_body = JSON.parse(response.body) - expect(parsed_body['Branches']).to include('master') - expect(parsed_body['Tags'].first).to eq('v1.1.0') - expect(parsed_body['Tags'].last).to eq('v1.0.0') - expect(parsed_body['Commits']).to be_nil + expect(json_response['Branches']).to include('master') + expect(json_response['Tags'].first).to eq('v1.1.0') + expect(json_response['Tags'].last).to eq('v1.0.0') + expect(json_response['Commits']).to be_nil end it "gets a list of branches, tags and commits" do get :refs, params: { namespace_id: project.namespace, id: project, ref: "123456" } - parsed_body = JSON.parse(response.body) - expect(parsed_body["Branches"]).to include("master") - expect(parsed_body["Tags"]).to include("v1.0.0") - expect(parsed_body["Commits"]).to include("123456") + expect(json_response["Branches"]).to include("master") + expect(json_response["Tags"]).to include("v1.0.0") + expect(json_response["Commits"]).to include("123456") end context "when preferred language is Japanese" do @@ -765,10 +763,9 @@ describe ProjectsController do it "gets a list of branches, tags and commits" do get :refs, params: { namespace_id: project.namespace, id: project, ref: "123456" } - parsed_body = JSON.parse(response.body) - expect(parsed_body["Branches"]).to include("master") - expect(parsed_body["Tags"]).to include("v1.0.0") - expect(parsed_body["Commits"]).to include("123456") + expect(json_response["Branches"]).to include("master") + expect(json_response["Tags"]).to include("v1.0.0") + expect(json_response["Commits"]).to include("123456") end end @@ -797,7 +794,7 @@ describe ProjectsController do it 'renders json in a correct format' do post :preview_markdown, params: { namespace_id: public_project.namespace, id: public_project, text: '*Markdown* text' } - expect(JSON.parse(response.body).keys).to match_array(%w(body references)) + expect(json_response.keys).to match_array(%w(body references)) end context 'when not authorized' do @@ -821,8 +818,6 @@ describe ProjectsController do text: issue.to_reference } - json_response = JSON.parse(response.body) - expect(json_response['body']).to match(/\##{issue.iid} \(closed\)/) end @@ -833,8 +828,6 @@ describe ProjectsController do text: merge_request.to_reference } - json_response = JSON.parse(response.body) - expect(json_response['body']).to match(/\!#{merge_request.iid} \(closed\)/) end end diff --git a/spec/controllers/snippets/notes_controller_spec.rb b/spec/controllers/snippets/notes_controller_spec.rb index 586d59c2d09..652533ac49f 100644 --- a/spec/controllers/snippets/notes_controller_spec.rb +++ b/spec/controllers/snippets/notes_controller_spec.rb @@ -26,7 +26,7 @@ describe Snippets::NotesController do end it "returns not empty array of notes" do - expect(JSON.parse(response.body)["notes"].empty?).to be_falsey + expect(json_response["notes"].empty?).to be_falsey end end @@ -97,7 +97,7 @@ describe Snippets::NotesController do it "returns 1 note" do get :index, params: { snippet_id: private_snippet } - expect(JSON.parse(response.body)['notes'].count).to eq(1) + expect(json_response['notes'].count).to eq(1) end end end @@ -114,7 +114,7 @@ describe Snippets::NotesController do it "does not return any note" do get :index, params: { snippet_id: public_snippet } - expect(JSON.parse(response.body)['notes'].count).to eq(0) + expect(json_response['notes'].count).to eq(0) end end end diff --git a/spec/controllers/snippets_controller_spec.rb b/spec/controllers/snippets_controller_spec.rb index 3aba02bf3ff..b0092bc8994 100644 --- a/spec/controllers/snippets_controller_spec.rb +++ b/spec/controllers/snippets_controller_spec.rb @@ -622,7 +622,7 @@ describe SnippetsController do post :preview_markdown, params: { id: snippet, text: '*Markdown* text' } - expect(JSON.parse(response.body).keys).to match_array(%w(body references)) + expect(json_response.keys).to match_array(%w(body references)) end end end diff --git a/spec/controllers/users_controller_spec.rb b/spec/controllers/users_controller_spec.rb index c3d6ea9cbcd..8b8d4c57000 100644 --- a/spec/controllers/users_controller_spec.rb +++ b/spec/controllers/users_controller_spec.rb @@ -291,7 +291,7 @@ describe UsersController do it 'response with snippets json data' do get :snippets, params: { username: user.username }, format: :json expect(response).to have_gitlab_http_status(200) - expect(JSON.parse(response.body)).to have_key('html') + expect(json_response).to have_key('html') end end diff --git a/spec/factories/projects.rb b/spec/factories/projects.rb index 743ec322885..0e8810b73a1 100644 --- a/spec/factories/projects.rb +++ b/spec/factories/projects.rb @@ -25,7 +25,9 @@ FactoryBot.define do issues_access_level ProjectFeature::ENABLED merge_requests_access_level ProjectFeature::ENABLED repository_access_level ProjectFeature::ENABLED - pages_access_level ProjectFeature::ENABLED + pages_access_level do + visibility_level == Gitlab::VisibilityLevel::PUBLIC ? ProjectFeature::ENABLED : ProjectFeature::PRIVATE + end # we can't assign the delegated `#ci_cd_settings` attributes directly, as the # `#ci_cd_settings` relation needs to be created first @@ -306,34 +308,18 @@ FactoryBot.define do factory :redmine_project, parent: :project do has_external_issue_tracker true - after :create do |project| - project.create_redmine_service( - active: true, - properties: { - 'project_url' => 'http://redmine/projects/project_name_in_redmine', - 'issues_url' => 'http://redmine/projects/project_name_in_redmine/issues/:id', - 'new_issue_url' => 'http://redmine/projects/project_name_in_redmine/issues/new' - } - ) - end + redmine_service end factory :youtrack_project, parent: :project do has_external_issue_tracker true - after :create do |project| - project.create_youtrack_service( - active: true, - properties: { - 'project_url' => 'http://youtrack/projects/project_guid_in_youtrack', - 'issues_url' => 'http://youtrack/issues/:id' - } - ) - end + youtrack_service end factory :jira_project, parent: :project do has_external_issue_tracker true + jira_service end diff --git a/spec/factories/services.rb b/spec/factories/services.rb index cd1d2c33373..daf842e3075 100644 --- a/spec/factories/services.rb +++ b/spec/factories/services.rb @@ -79,14 +79,12 @@ FactoryBot.define do trait :issue_tracker do properties( project_url: 'http://issue-tracker.example.com', - issues_url: 'http://issue-tracker.example.com', + issues_url: 'http://issue-tracker.example.com/issues/:id', new_issue_url: 'http://issue-tracker.example.com' ) end - factory :jira_cloud_service, class: JiraService do - project - active true + trait :jira_cloud_service do properties( url: 'https://mysite.atlassian.net', username: 'jira_user', diff --git a/spec/factories/services_data.rb b/spec/factories/services_data.rb index 387e130a743..5a3639895b6 100644 --- a/spec/factories/services_data.rb +++ b/spec/factories/services_data.rb @@ -1,18 +1,12 @@ # frozen_string_literal: true +# these factories should never be called directly, they are used when creating services FactoryBot.define do factory :jira_tracker_data do service - url 'http://jira.example.com' - api_url 'http://api-jira.example.com' - username 'jira_username' - password 'jira_password' end factory :issue_tracker_data do service - project_url 'http://issuetracker.example.com' - issues_url 'http://issues.example.com' - new_issue_url 'http://new-issue.example.com' end end diff --git a/spec/features/issues/bulk_assignment_labels_spec.rb b/spec/features/issues/bulk_assignment_labels_spec.rb index 06cb2e36334..7be5961af09 100644 --- a/spec/features/issues/bulk_assignment_labels_spec.rb +++ b/spec/features/issues/bulk_assignment_labels_spec.rb @@ -381,7 +381,7 @@ describe 'Issues > Labels bulk assignment' do if unmark items.map do |item| # Make sure we are unmarking the item no matter the state it has currently - click_link item until find('a', text: item)[:class] == 'label-item' + click_link item until find('a', text: item)[:class].include? 'label-item' end end end diff --git a/spec/features/markdown/mermaid_spec.rb b/spec/features/markdown/mermaid_spec.rb index 7008b361394..e3bcaca737e 100644 --- a/spec/features/markdown/mermaid_spec.rb +++ b/spec/features/markdown/mermaid_spec.rb @@ -21,4 +21,22 @@ describe 'Mermaid rendering', :js do expect(page).to have_selector('svg text', text: label) end end + + it 'renders linebreaks in Mermaid diagrams' do + description = <<~MERMAID + ```mermaid + graph TD; + A(Line 1<br>Line 2)-->B(Line 1<br/>Line 2); + C(Line 1<br />Line 2)-->D(Line 1<br />Line 2); + ``` + MERMAID + + project = create(:project, :public) + issue = create(:issue, project: project, description: description) + + visit project_issue_path(project, issue) + + expected = '<text><tspan xml:space="preserve" dy="1em" x="1">Line 1</tspan><tspan xml:space="preserve" dy="1em" x="1">Line 2</tspan></text>' + expect(page.html.scan(expected).count).to be(4) + end end diff --git a/spec/features/triggers_spec.rb b/spec/features/triggers_spec.rb index 919859c145a..7c44680e9f7 100644 --- a/spec/features/triggers_spec.rb +++ b/spec/features/triggers_spec.rb @@ -66,7 +66,7 @@ describe 'Triggers', :js do it 'edit "legacy" trigger and save' do # Create new trigger without owner association, i.e. Legacy trigger - create(:ci_trigger, owner: nil, project: @project) + create(:ci_trigger, owner: user, project: @project).update_attribute(:owner, nil) visit project_settings_ci_cd_path(@project) # See if the trigger can be edited and description is blank @@ -127,17 +127,19 @@ describe 'Triggers', :js do end describe 'show triggers workflow' do + before do + stub_feature_flags(use_legacy_pipeline_triggers: false) + end + it 'contains trigger description placeholder' do expect(page.find('#trigger_description')['placeholder']).to eq 'Trigger description' end - it 'show "legacy" badge for legacy trigger' do - create(:ci_trigger, owner: nil, project: @project) + it 'show "invalid" badge for legacy trigger' do + create(:ci_trigger, owner: user, project: @project).update_attribute(:owner, nil) visit project_settings_ci_cd_path(@project) - # See if trigger without owner (i.e. legacy) shows "legacy" badge and is editable - expect(page.find('.triggers-list')).to have_content 'legacy' - expect(page.find('.triggers-list')).to have_selector('a[title="Edit"]') + expect(page.find('.triggers-list')).to have_content 'invalid' end it 'show "invalid" badge for trigger with owner having insufficient permissions' do @@ -149,6 +151,19 @@ describe 'Triggers', :js do expect(page.find('.triggers-list')).not_to have_selector('a[title="Edit"]') end + it 'do not show "Edit" or full token for legacy trigger' do + create(:ci_trigger, owner: user, project: @project, description: trigger_title) + .update_attribute(:owner, nil) + visit project_settings_ci_cd_path(@project) + + # See if trigger not owned shows only first few token chars and doesn't have copy-to-clipboard button + expect(page.find('.triggers-list')).to have_content(@project.triggers.first.token[0..3]) + expect(page.find('.triggers-list')).not_to have_selector('button.btn-clipboard') + + # See if trigger is non-editable + expect(page.find('.triggers-list')).not_to have_selector('a[title="Edit"]') + end + it 'do not show "Edit" or full token for not owned trigger' do # Create trigger with user different from current_user create(:ci_trigger, owner: user2, project: @project, description: trigger_title) @@ -175,5 +190,56 @@ describe 'Triggers', :js do expect(page.find('.triggers-list .trigger-owner')).to have_content user.name expect(page.find('.triggers-list')).to have_selector('a[title="Edit"]') end + + context 'when :use_legacy_pipeline_triggers feature flag is enabled' do + before do + stub_feature_flags(use_legacy_pipeline_triggers: true) + end + + it 'show "legacy" badge for legacy trigger' do + create(:ci_trigger, owner: nil, project: @project) + visit project_settings_ci_cd_path(@project) + + # See if trigger without owner (i.e. legacy) shows "legacy" badge and is editable + expect(page.find('.triggers-list')).to have_content 'legacy' + expect(page.find('.triggers-list')).to have_selector('a[title="Edit"]') + end + + it 'show "invalid" badge for trigger with owner having insufficient permissions' do + create(:ci_trigger, owner: guest_user, project: @project, description: trigger_title) + visit project_settings_ci_cd_path(@project) + + # See if trigger without owner (i.e. legacy) shows "legacy" badge and is non-editable + expect(page.find('.triggers-list')).to have_content 'invalid' + expect(page.find('.triggers-list')).not_to have_selector('a[title="Edit"]') + end + + it 'do not show "Edit" or full token for not owned trigger' do + # Create trigger with user different from current_user + create(:ci_trigger, owner: user2, project: @project, description: trigger_title) + visit project_settings_ci_cd_path(@project) + + # See if trigger not owned by current_user shows only first few token chars and doesn't have copy-to-clipboard button + expect(page.find('.triggers-list')).to have_content(@project.triggers.first.token[0..3]) + expect(page.find('.triggers-list')).not_to have_selector('button.btn-clipboard') + + # See if trigger owner name doesn't match with current_user and trigger is non-editable + expect(page.find('.triggers-list .trigger-owner')).not_to have_content user.name + expect(page.find('.triggers-list')).not_to have_selector('a[title="Edit"]') + end + + it 'show "Edit" and full token for owned trigger' do + create(:ci_trigger, owner: user, project: @project, description: trigger_title) + visit project_settings_ci_cd_path(@project) + + # See if trigger shows full token and has copy-to-clipboard button + expect(page.find('.triggers-list')).to have_content @project.triggers.first.token + expect(page.find('.triggers-list')).to have_selector('button.btn-clipboard') + + # See if trigger owner name matches with current_user and is editable + expect(page.find('.triggers-list .trigger-owner')).to have_content user.name + expect(page.find('.triggers-list')).to have_selector('a[title="Edit"]') + end + end end end diff --git a/spec/frontend/create_merge_request_dropdown_spec.js b/spec/frontend/create_merge_request_dropdown_spec.js index 6e41fdabdce..dcc6fa96d18 100644 --- a/spec/frontend/create_merge_request_dropdown_spec.js +++ b/spec/frontend/create_merge_request_dropdown_spec.js @@ -1,6 +1,7 @@ import axios from '~/lib/utils/axios_utils'; import MockAdapter from 'axios-mock-adapter'; import CreateMergeRequestDropdown from '~/create_merge_request_dropdown'; +import confidentialState from '~/confidential_merge_request/state'; import { TEST_HOST } from './helpers/test_constants'; describe('CreateMergeRequestDropdown', () => { @@ -66,4 +67,37 @@ describe('CreateMergeRequestDropdown', () => { ); }); }); + + describe('enable', () => { + beforeEach(() => { + dropdown.createMergeRequestButton.classList.add('disabled'); + }); + + afterEach(() => { + confidentialState.selectedProject = {}; + }); + + it('enables button when not confidential issue', () => { + dropdown.enable(); + + expect(dropdown.createMergeRequestButton.classList).not.toContain('disabled'); + }); + + it('enables when can create confidential issue', () => { + document.querySelector('.js-create-mr').setAttribute('data-is-confidential', 'true'); + confidentialState.selectedProject = { name: 'test' }; + + dropdown.enable(); + + expect(dropdown.createMergeRequestButton.classList).not.toContain('disabled'); + }); + + it('does not enable when can not create confidential issue', () => { + document.querySelector('.js-create-mr').setAttribute('data-is-confidential', 'true'); + + dropdown.enable(); + + expect(dropdown.createMergeRequestButton.classList).toContain('disabled'); + }); + }); }); diff --git a/spec/frontend/issue_show/components/pinned_links_spec.js b/spec/frontend/issue_show/components/pinned_links_spec.js index 50041667a61..77da3390918 100644 --- a/spec/frontend/issue_show/components/pinned_links_spec.js +++ b/spec/frontend/issue_show/components/pinned_links_spec.js @@ -5,10 +5,6 @@ import PinnedLinks from '~/issue_show/components/pinned_links.vue'; const localVue = createLocalVue(); const plainZoomUrl = 'https://zoom.us/j/123456789'; -const vanityZoomUrl = 'https://gitlab.zoom.us/j/123456789'; -const startZoomUrl = 'https://zoom.us/s/123456789'; -const personalZoomUrl = 'https://zoom.us/my/hunter-zoloman'; -const randomUrl = 'https://zoom.us.com'; describe('PinnedLinks', () => { let wrapper; @@ -27,7 +23,7 @@ describe('PinnedLinks', () => { localVue, sync: false, propsData: { - descriptionHtml: '', + zoomMeetingUrl: null, ...props, }, }); @@ -35,55 +31,15 @@ describe('PinnedLinks', () => { it('displays Zoom link', () => { createComponent({ - descriptionHtml: `<a href="${plainZoomUrl}">Zoom</a>`, + zoomMeetingUrl: `<a href="${plainZoomUrl}">Zoom</a>`, }); expect(link.text).toBe('Join Zoom meeting'); }); - it('detects plain Zoom link', () => { + it('does not render if there are no links', () => { createComponent({ - descriptionHtml: `<a href="${plainZoomUrl}">Zoom</a>`, - }); - - expect(link.href).toBe(plainZoomUrl); - }); - - it('detects vanity Zoom link', () => { - createComponent({ - descriptionHtml: `<a href="${vanityZoomUrl}">Zoom</a>`, - }); - - expect(link.href).toBe(vanityZoomUrl); - }); - - it('detects Zoom start meeting link', () => { - createComponent({ - descriptionHtml: `<a href="${startZoomUrl}">Zoom</a>`, - }); - - expect(link.href).toBe(startZoomUrl); - }); - - it('detects personal Zoom room link', () => { - createComponent({ - descriptionHtml: `<a href="${personalZoomUrl}">Zoom</a>`, - }); - - expect(link.href).toBe(personalZoomUrl); - }); - - it('only renders final Zoom link in description', () => { - createComponent({ - descriptionHtml: `<a href="${plainZoomUrl}">Zoom</a><a href="${vanityZoomUrl}">Zoom</a>`, - }); - - expect(link.href).toBe(vanityZoomUrl); - }); - - it('does not render for other links', () => { - createComponent({ - descriptionHtml: `<a href="${randomUrl}">Some other link</a>`, + zoomMeetingUrl: null, }); expect(wrapper.find(GlLink).exists()).toBe(false); diff --git a/spec/frontend/mocks/ce/lib/utils/axios_utils.js b/spec/frontend/mocks/ce/lib/utils/axios_utils.js new file mode 100644 index 00000000000..b4065626b09 --- /dev/null +++ b/spec/frontend/mocks/ce/lib/utils/axios_utils.js @@ -0,0 +1,15 @@ +const axios = jest.requireActual('~/lib/utils/axios_utils').default; + +axios.isMock = true; + +// Fail tests for unmocked requests +axios.defaults.adapter = config => { + const message = + `Unexpected unmocked request: ${JSON.stringify(config, null, 2)}\n` + + 'Consider using the `axios-mock-adapter` in tests.'; + const error = new Error(message); + error.config = config; + throw error; +}; + +export default axios; diff --git a/spec/frontend/mocks/mocks_helper.js b/spec/frontend/mocks/mocks_helper.js new file mode 100644 index 00000000000..21c032cd3c9 --- /dev/null +++ b/spec/frontend/mocks/mocks_helper.js @@ -0,0 +1,60 @@ +/** + * @module + * + * This module implements auto-injected manual mocks that are cleaner than Jest's approach. + * + * See https://docs.gitlab.com/ee/development/testing_guide/frontend_testing.html + */ + +import fs from 'fs'; +import path from 'path'; + +import readdir from 'readdir-enhanced'; + +const MAX_DEPTH = 20; +const prefixMap = [ + // E.g. the mock ce/foo/bar maps to require path ~/foo/bar + { mocksRoot: 'ce', requirePrefix: '~' }, + // { mocksRoot: 'ee', requirePrefix: 'ee' }, // We'll deal with EE-specific mocks later + { mocksRoot: 'node', requirePrefix: '' }, + // { mocksRoot: 'virtual', requirePrefix: '' }, // We'll deal with virtual mocks later +]; + +const mockFileFilter = stats => stats.isFile() && stats.path.endsWith('.js'); + +const getMockFiles = root => readdir.sync(root, { deep: MAX_DEPTH, filter: mockFileFilter }); + +// Function that performs setting a mock. This has to be overridden by the unit test, because +// jest.setMock can't be overwritten across files. +// Use require() because jest.setMock expects the CommonJS exports object +const defaultSetMock = (srcPath, mockPath) => + jest.mock(srcPath, () => jest.requireActual(mockPath)); + +// eslint-disable-next-line import/prefer-default-export +export const setupManualMocks = function setupManualMocks(setMock = defaultSetMock) { + prefixMap.forEach(({ mocksRoot, requirePrefix }) => { + const mocksRootAbsolute = path.join(__dirname, mocksRoot); + if (!fs.existsSync(mocksRootAbsolute)) { + return; + } + + getMockFiles(path.join(__dirname, mocksRoot)).forEach(mockPath => { + const mockPathNoExt = mockPath.substring(0, mockPath.length - path.extname(mockPath).length); + const sourcePath = path.join(requirePrefix, mockPathNoExt); + const mockPathRelative = `./${path.join(mocksRoot, mockPathNoExt)}`; + + try { + setMock(sourcePath, mockPathRelative); + } catch (e) { + if (e.message.includes('Could not locate module')) { + // The corresponding mocked module doesn't exist. Raise a better error. + // Eventualy, we may support virtual mocks (mocks whose path doesn't directly correspond + // to a module, like with the `ee_else_ce` prefix). + throw new Error( + `A manual mock was defined for module ${sourcePath}, but the module doesn't exist!`, + ); + } + } + }); + }); +}; diff --git a/spec/frontend/mocks/mocks_helper_spec.js b/spec/frontend/mocks/mocks_helper_spec.js new file mode 100644 index 00000000000..34be110a7e3 --- /dev/null +++ b/spec/frontend/mocks/mocks_helper_spec.js @@ -0,0 +1,147 @@ +/* eslint-disable global-require, promise/catch-or-return */ + +import path from 'path'; + +import axios from '~/lib/utils/axios_utils'; + +const absPath = path.join.bind(null, __dirname); + +jest.mock('fs'); +jest.mock('readdir-enhanced'); + +describe('mocks_helper.js', () => { + let setupManualMocks; + const setMock = jest.fn().mockName('setMock'); + let fs; + let readdir; + + beforeAll(() => { + jest.resetModules(); + jest.setMock = jest.fn().mockName('jest.setMock'); + fs = require('fs'); + readdir = require('readdir-enhanced'); + + // We need to provide setupManualMocks with a mock function that pretends to do the setup of + // the mock. This is because we can't mock jest.setMock across files. + setupManualMocks = () => require('./mocks_helper').setupManualMocks(setMock); + }); + + afterEach(() => { + fs.existsSync.mockReset(); + readdir.sync.mockReset(); + setMock.mockReset(); + }); + + it('enumerates through mock file roots', () => { + setupManualMocks(); + expect(fs.existsSync).toHaveBeenCalledTimes(2); + expect(fs.existsSync).toHaveBeenNthCalledWith(1, absPath('ce')); + expect(fs.existsSync).toHaveBeenNthCalledWith(2, absPath('node')); + + expect(readdir.sync).toHaveBeenCalledTimes(0); + }); + + it("doesn't traverse the directory tree infinitely", () => { + fs.existsSync.mockReturnValue(true); + readdir.sync.mockReturnValue([]); + setupManualMocks(); + + readdir.mock.calls.forEach(call => { + expect(call[1].deep).toBeLessThan(100); + }); + }); + + it('sets up mocks for CE (the ~/ prefix)', () => { + fs.existsSync.mockImplementation(root => root.endsWith('ce')); + readdir.sync.mockReturnValue(['root.js', 'lib/utils/util.js']); + setupManualMocks(); + + expect(readdir.sync).toHaveBeenCalledTimes(1); + expect(readdir.sync.mock.calls[0][0]).toBe(absPath('ce')); + + expect(setMock).toHaveBeenCalledTimes(2); + expect(setMock).toHaveBeenNthCalledWith(1, '~/root', './ce/root'); + expect(setMock).toHaveBeenNthCalledWith(2, '~/lib/utils/util', './ce/lib/utils/util'); + }); + + it('sets up mocks for node_modules', () => { + fs.existsSync.mockImplementation(root => root.endsWith('node')); + readdir.sync.mockReturnValue(['jquery', '@babel/core']); + setupManualMocks(); + + expect(readdir.sync).toHaveBeenCalledTimes(1); + expect(readdir.sync.mock.calls[0][0]).toBe(absPath('node')); + + expect(setMock).toHaveBeenCalledTimes(2); + expect(setMock).toHaveBeenNthCalledWith(1, 'jquery', './node/jquery'); + expect(setMock).toHaveBeenNthCalledWith(2, '@babel/core', './node/@babel/core'); + }); + + it('sets up mocks for all roots', () => { + const files = { + [absPath('ce')]: ['root', 'lib/utils/util'], + [absPath('node')]: ['jquery', '@babel/core'], + }; + + fs.existsSync.mockReturnValue(true); + readdir.sync.mockImplementation(root => files[root]); + setupManualMocks(); + + expect(readdir.sync).toHaveBeenCalledTimes(2); + expect(readdir.sync.mock.calls[0][0]).toBe(absPath('ce')); + expect(readdir.sync.mock.calls[1][0]).toBe(absPath('node')); + + expect(setMock).toHaveBeenCalledTimes(4); + expect(setMock).toHaveBeenNthCalledWith(1, '~/root', './ce/root'); + expect(setMock).toHaveBeenNthCalledWith(2, '~/lib/utils/util', './ce/lib/utils/util'); + expect(setMock).toHaveBeenNthCalledWith(3, 'jquery', './node/jquery'); + expect(setMock).toHaveBeenNthCalledWith(4, '@babel/core', './node/@babel/core'); + }); + + it('fails when given a virtual mock', () => { + fs.existsSync.mockImplementation(p => p.endsWith('ce')); + readdir.sync.mockReturnValue(['virtual', 'shouldntBeImported']); + setMock.mockImplementation(() => { + throw new Error('Could not locate module'); + }); + + expect(setupManualMocks).toThrow( + new Error("A manual mock was defined for module ~/virtual, but the module doesn't exist!"), + ); + + expect(readdir.sync).toHaveBeenCalledTimes(1); + expect(readdir.sync.mock.calls[0][0]).toBe(absPath('ce')); + }); + + describe('auto-injection', () => { + it('handles ambiguous paths', () => { + jest.isolateModules(() => { + const axios2 = require('../../../app/assets/javascripts/lib/utils/axios_utils').default; + expect(axios2.isMock).toBe(true); + }); + }); + + it('survives jest.isolateModules()', done => { + jest.isolateModules(() => { + const axios2 = require('~/lib/utils/axios_utils').default; + expect(axios2.get('http://gitlab.com')) + .rejects.toThrow('Unexpected unmocked request') + .then(done); + }); + }); + + it('can be unmocked and remocked', () => { + jest.dontMock('~/lib/utils/axios_utils'); + jest.resetModules(); + const axios2 = require('~/lib/utils/axios_utils').default; + expect(axios2).not.toBe(axios); + expect(axios2.isMock).toBeUndefined(); + + jest.doMock('~/lib/utils/axios_utils'); + jest.resetModules(); + const axios3 = require('~/lib/utils/axios_utils').default; + expect(axios3).not.toBe(axios2); + expect(axios3.isMock).toBe(true); + }); + }); +}); diff --git a/spec/frontend/mocks/node/jquery.js b/spec/frontend/mocks/node/jquery.js new file mode 100644 index 00000000000..34a25772f67 --- /dev/null +++ b/spec/frontend/mocks/node/jquery.js @@ -0,0 +1,13 @@ +/* eslint-disable import/no-commonjs */ + +const $ = jest.requireActual('jquery'); + +// Fail tests for unmocked requests +$.ajax = () => { + throw new Error( + 'Unexpected unmocked jQuery.ajax() call! Make sure to mock jQuery.ajax() in tests.', + ); +}; + +// jquery is not an ES6 module +module.exports = $; diff --git a/spec/frontend/mocks_spec.js b/spec/frontend/mocks_spec.js new file mode 100644 index 00000000000..2d2324120fd --- /dev/null +++ b/spec/frontend/mocks_spec.js @@ -0,0 +1,13 @@ +import $ from 'jquery'; +import axios from '~/lib/utils/axios_utils'; + +describe('Mock auto-injection', () => { + describe('mocks', () => { + it('~/lib/utils/axios_utils', () => + expect(axios.get('http://gitlab.com')).rejects.toThrow('Unexpected unmocked request')); + + it('jQuery.ajax()', () => { + expect($.ajax).toThrow('Unexpected unmocked'); + }); + }); +}); diff --git a/spec/frontend/operation_settings/components/external_dashboard_spec.js b/spec/frontend/operation_settings/components/external_dashboard_spec.js index a881de8fbfe..39d7c19e731 100644 --- a/spec/frontend/operation_settings/components/external_dashboard_spec.js +++ b/spec/frontend/operation_settings/components/external_dashboard_spec.js @@ -7,7 +7,6 @@ import { refreshCurrentPage } from '~/lib/utils/url_utility'; import createFlash from '~/flash'; import { TEST_HOST } from 'helpers/test_constants'; -jest.mock('~/lib/utils/axios_utils'); jest.mock('~/lib/utils/url_utility'); jest.mock('~/flash'); @@ -32,6 +31,10 @@ describe('operation settings external dashboard component', () => { wrapper = shallow ? shallowMount(...config) : mount(...config); }; + beforeEach(() => { + jest.spyOn(axios, 'patch').mockImplementation(); + }); + afterEach(() => { if (wrapper.destroy) { wrapper.destroy(); diff --git a/spec/frontend/repository/components/breadcrumbs_spec.js b/spec/frontend/repository/components/breadcrumbs_spec.js index 068fa317a87..707eae34793 100644 --- a/spec/frontend/repository/components/breadcrumbs_spec.js +++ b/spec/frontend/repository/components/breadcrumbs_spec.js @@ -1,12 +1,14 @@ import { shallowMount, RouterLinkStub } from '@vue/test-utils'; +import { GlDropdown } from '@gitlab/ui'; import Breadcrumbs from '~/repository/components/breadcrumbs.vue'; let vm; -function factory(currentPath) { +function factory(currentPath, extraProps = {}) { vm = shallowMount(Breadcrumbs, { propsData: { currentPath, + ...extraProps, }, stubs: { RouterLink: RouterLinkStub, @@ -41,4 +43,20 @@ describe('Repository breadcrumbs component', () => { .attributes('aria-current'), ).toEqual('page'); }); + + it('does not render add to tree dropdown when permissions are false', () => { + factory('/', { canCollaborate: false }); + + vm.setData({ userPermissions: { forkProject: false, createMergeRequestIn: false } }); + + expect(vm.find(GlDropdown).exists()).toBe(false); + }); + + it('renders add to tree dropdown when permissions are true', () => { + factory('/', { canCollaborate: true }); + + vm.setData({ userPermissions: { forkProject: true, createMergeRequestIn: true } }); + + expect(vm.find(GlDropdown).exists()).toBe(true); + }); }); diff --git a/spec/frontend/repository/components/table/row_spec.js b/spec/frontend/repository/components/table/row_spec.js index c566057ad3f..e539c560975 100644 --- a/spec/frontend/repository/components/table/row_spec.js +++ b/spec/frontend/repository/components/table/row_spec.js @@ -1,5 +1,5 @@ import { shallowMount, RouterLinkStub } from '@vue/test-utils'; -import { GlBadge } from '@gitlab/ui'; +import { GlBadge, GlLink } from '@gitlab/ui'; import { visitUrl } from '~/lib/utils/url_utility'; import TableRow from '~/repository/components/table/row.vue'; @@ -142,4 +142,18 @@ describe('Repository table row component', () => { expect(vm.find(GlBadge).exists()).toBe(true); }); + + it('renders commit and web links with href for submodule', () => { + factory({ + id: '1', + path: 'test', + type: 'commit', + url: 'https://test.com', + submoduleTreeUrl: 'https://test.com/commit', + currentPath: '/', + }); + + expect(vm.find('a').attributes('href')).toEqual('https://test.com'); + expect(vm.find(GlLink).attributes('href')).toEqual('https://test.com/commit'); + }); }); diff --git a/spec/frontend/test_setup.js b/spec/frontend/test_setup.js index 15cf18700ed..634c78ec029 100644 --- a/spec/frontend/test_setup.js +++ b/spec/frontend/test_setup.js @@ -2,10 +2,10 @@ import Vue from 'vue'; import * as jqueryMatchers from 'custom-jquery-matchers'; import $ from 'jquery'; import Translate from '~/vue_shared/translate'; -import axios from '~/lib/utils/axios_utils'; import { config as testUtilsConfig } from '@vue/test-utils'; import { initializeTestTimeout } from './helpers/timeout'; import { loadHTMLFixture, setHTMLFixture } from './helpers/fixtures'; +import { setupManualMocks } from './mocks/mocks_helper'; // Expose jQuery so specs using jQuery plugins can be imported nicely. // Here is an issue to explore better alternatives: @@ -14,6 +14,8 @@ window.jQuery = $; process.on('unhandledRejection', global.promiseRejectionHandler); +setupManualMocks(); + afterEach(() => // give Promises a bit more time so they fail the right test new Promise(setImmediate).then(() => { @@ -24,18 +26,6 @@ afterEach(() => initializeTestTimeout(process.env.CI ? 5000 : 500); -// fail tests for unmocked requests -beforeEach(done => { - axios.defaults.adapter = config => { - const error = new Error(`Unexpected unmocked request: ${JSON.stringify(config, null, 2)}`); - error.config = config; - done.fail(error); - return Promise.reject(error); - }; - - done(); -}); - Vue.config.devtools = false; Vue.config.productionTip = false; diff --git a/spec/graphql/types/tree/submodule_type_spec.rb b/spec/graphql/types/tree/submodule_type_spec.rb index bdb3149b41c..768eccba68c 100644 --- a/spec/graphql/types/tree/submodule_type_spec.rb +++ b/spec/graphql/types/tree/submodule_type_spec.rb @@ -5,5 +5,5 @@ require 'spec_helper' describe Types::Tree::SubmoduleType do it { expect(described_class.graphql_name).to eq('Submodule') } - it { expect(described_class).to have_graphql_fields(:id, :name, :type, :path, :flat_path) } + it { expect(described_class).to have_graphql_fields(:id, :name, :type, :path, :flat_path, :web_url, :tree_url) } end diff --git a/spec/helpers/issuables_helper_spec.rb b/spec/helpers/issuables_helper_spec.rb index 1d1446eaa30..3c8179460ac 100644 --- a/spec/helpers/issuables_helper_spec.rb +++ b/spec/helpers/issuables_helper_spec.rb @@ -202,5 +202,46 @@ describe IssuablesHelper do } expect(helper.issuable_initial_data(issue)).to match(hash_including(expected_data)) end + + describe '#zoomMeetingUrl in issue' do + let(:issue) { create(:issue, author: user, description: description) } + + before do + assign(:project, issue.project) + end + + context 'no zoom links in the issue description' do + let(:description) { 'issue text' } + + it 'does not set zoomMeetingUrl' do + expect(helper.issuable_initial_data(issue)) + .not_to include(:zoomMeetingUrl) + end + end + + context 'no zoom links in the issue description if it has link but not a zoom link' do + let(:description) { 'issue text https://stackoverflow.com/questions/22' } + + it 'does not set zoomMeetingUrl' do + expect(helper.issuable_initial_data(issue)) + .not_to include(:zoomMeetingUrl) + end + end + + context 'with two zoom links in description' do + let(:description) do + <<~TEXT + issue text and + zoom call on https://zoom.us/j/123456789 this url + and new zoom url https://zoom.us/s/lastone and some more text + TEXT + end + + it 'sets zoomMeetingUrl value to the last url' do + expect(helper.issuable_initial_data(issue)) + .to include(zoomMeetingUrl: 'https://zoom.us/s/lastone') + end + end + end end end diff --git a/spec/javascripts/boards/components/board_form_spec.js b/spec/javascripts/boards/components/board_form_spec.js new file mode 100644 index 00000000000..e9014156a98 --- /dev/null +++ b/spec/javascripts/boards/components/board_form_spec.js @@ -0,0 +1,56 @@ +import $ from 'jquery'; +import Vue from 'vue'; +import boardsStore from '~/boards/stores/boards_store'; +import boardForm from '~/boards/components/board_form.vue'; +import mountComponent from 'spec/helpers/vue_mount_component_helper'; + +describe('board_form.vue', () => { + const props = { + canAdminBoard: false, + labelsPath: `${gl.TEST_HOST}/labels/path`, + milestonePath: `${gl.TEST_HOST}/milestone/path`, + }; + let vm; + + beforeEach(() => { + spyOn($, 'ajax'); + boardsStore.state.currentPage = 'edit'; + const Component = Vue.extend(boardForm); + vm = mountComponent(Component, props); + }); + + afterEach(() => { + vm.$destroy(); + }); + + describe('methods', () => { + describe('cancel', () => { + it('resets currentPage', done => { + vm.cancel(); + + Vue.nextTick() + .then(() => { + expect(boardsStore.state.currentPage).toBe(''); + }) + .then(done) + .catch(done.fail); + }); + }); + }); + + describe('buttons', () => { + it('cancel button triggers cancel()', done => { + spyOn(vm, 'cancel'); + + Vue.nextTick() + .then(() => { + const cancelButton = vm.$el.querySelector('button[data-dismiss="modal"]'); + cancelButton.click(); + + expect(vm.cancel).toHaveBeenCalled(); + }) + .then(done) + .catch(done.fail); + }); + }); +}); diff --git a/spec/javascripts/boards/components/boards_selector_spec.js b/spec/javascripts/boards/components/boards_selector_spec.js new file mode 100644 index 00000000000..504bc51778c --- /dev/null +++ b/spec/javascripts/boards/components/boards_selector_spec.js @@ -0,0 +1,205 @@ +import Vue from 'vue'; +import BoardService from '~/boards/services/board_service'; +import BoardsSelector from '~/boards/components/boards_selector.vue'; +import mountComponent from 'spec/helpers/vue_mount_component_helper'; +import { TEST_HOST } from 'spec/test_constants'; +import boardsStore from '~/boards/stores/boards_store'; + +const throttleDuration = 1; + +function boardGenerator(n) { + return new Array(n).fill().map((board, id) => { + const name = `board${id}`; + + return { + id, + name, + }; + }); +} + +describe('BoardsSelector', () => { + let vm; + let allBoardsResponse; + let recentBoardsResponse; + let fillSearchBox; + const boards = boardGenerator(20); + const recentBoards = boardGenerator(5); + + beforeEach(done => { + setFixtures('<div class="js-boards-selector"></div>'); + window.gl = window.gl || {}; + + boardsStore.setEndpoints({ + boardsEndpoint: '', + recentBoardsEndpoint: '', + listsEndpoint: '', + bulkUpdatePath: '', + boardId: '', + }); + window.gl.boardService = new BoardService(); + + allBoardsResponse = Promise.resolve({ + data: boards, + }); + recentBoardsResponse = Promise.resolve({ + data: recentBoards, + }); + + spyOn(BoardService.prototype, 'allBoards').and.returnValue(allBoardsResponse); + spyOn(BoardService.prototype, 'recentBoards').and.returnValue(recentBoardsResponse); + + const Component = Vue.extend(BoardsSelector); + vm = mountComponent( + Component, + { + throttleDuration, + currentBoard: { + id: 1, + name: 'Development', + milestone_id: null, + weight: null, + assignee_id: null, + labels: [], + }, + milestonePath: `${TEST_HOST}/milestone/path`, + boardBaseUrl: `${TEST_HOST}/board/base/url`, + hasMissingBoards: false, + canAdminBoard: true, + multipleIssueBoardsAvailable: true, + labelsPath: `${TEST_HOST}/labels/path`, + projectId: 42, + groupId: 19, + scopedIssueBoardFeatureEnabled: true, + weights: [], + }, + document.querySelector('.js-boards-selector'), + ); + + vm.$el.querySelector('.js-dropdown-toggle').click(); + + Promise.all([allBoardsResponse, recentBoardsResponse]) + .then(() => vm.$nextTick()) + .then(done) + .catch(done.fail); + + fillSearchBox = filterTerm => { + const { searchBox } = vm.$refs; + const searchBoxInput = searchBox.$el.querySelector('input'); + searchBoxInput.value = filterTerm; + searchBoxInput.dispatchEvent(new Event('input')); + }; + }); + + afterEach(() => { + vm.$destroy(); + window.gl.boardService = undefined; + }); + + describe('filtering', () => { + it('shows all boards without filtering', done => { + vm.$nextTick() + .then(() => { + const dropdownItem = vm.$el.querySelectorAll('.js-dropdown-item'); + + expect(dropdownItem.length).toBe(boards.length + recentBoards.length); + }) + .then(done) + .catch(done.fail); + }); + + it('shows only matching boards when filtering', done => { + const filterTerm = 'board1'; + const expectedCount = boards.filter(board => board.name.includes(filterTerm)).length; + + fillSearchBox(filterTerm); + + vm.$nextTick() + .then(() => { + const dropdownItems = vm.$el.querySelectorAll('.js-dropdown-item'); + + expect(dropdownItems.length).toBe(expectedCount); + }) + .then(done) + .catch(done.fail); + }); + + it('shows message if there are no matching boards', done => { + fillSearchBox('does not exist'); + + vm.$nextTick() + .then(() => { + const dropdownItems = vm.$el.querySelectorAll('.js-dropdown-item'); + + expect(dropdownItems.length).toBe(0); + expect(vm.$el).toContainText('No matching boards found'); + }) + .then(done) + .catch(done.fail); + }); + }); + + describe('recent boards section', () => { + it('shows only when boards are greater than 10', done => { + vm.$nextTick() + .then(() => { + const headerEls = vm.$el.querySelectorAll('.dropdown-bold-header'); + + const expectedCount = 2; // Recent + All + + expect(expectedCount).toBe(headerEls.length); + }) + .then(done) + .catch(done.fail); + }); + + it('does not show when boards are less than 10', done => { + spyOn(vm, 'initScrollFade'); + spyOn(vm, 'setScrollFade'); + + vm.$nextTick() + .then(() => { + vm.boards = vm.boards.slice(0, 5); + }) + .then(vm.$nextTick) + .then(() => { + const headerEls = vm.$el.querySelectorAll('.dropdown-bold-header'); + const expectedCount = 0; + + expect(expectedCount).toBe(headerEls.length); + }) + .then(done) + .catch(done.fail); + }); + + it('does not show when recentBoards api returns empty array', done => { + vm.$nextTick() + .then(() => { + vm.recentBoards = []; + }) + .then(vm.$nextTick) + .then(() => { + const headerEls = vm.$el.querySelectorAll('.dropdown-bold-header'); + const expectedCount = 0; + + expect(expectedCount).toBe(headerEls.length); + }) + .then(done) + .catch(done.fail); + }); + + it('does not show when search is active', done => { + fillSearchBox('Random string'); + + vm.$nextTick() + .then(() => { + const headerEls = vm.$el.querySelectorAll('.dropdown-bold-header'); + const expectedCount = 0; + + expect(expectedCount).toBe(headerEls.length); + }) + .then(done) + .catch(done.fail); + }); + }); +}); diff --git a/spec/javascripts/registry/components/app_spec.js b/spec/javascripts/registry/components/app_spec.js index 87237d2853d..7b9b8d2b039 100644 --- a/spec/javascripts/registry/components/app_spec.js +++ b/spec/javascripts/registry/components/app_spec.js @@ -90,7 +90,7 @@ describe('Registry List', () => { .textContent.trim() .replace(/[\r\n]+/g, ' '), ).toEqual( - 'With the Container Registry, every project can have its own space to store its Docker images. Learn more about the Container Registry.', + 'With the Container Registry, every project can have its own space to store its Docker images. More Information', ); done(); }, 0); diff --git a/spec/lib/banzai/filter/redactor_filter_spec.rb b/spec/lib/banzai/filter/reference_redactor_filter_spec.rb index 919825a6102..e87440895e0 100644 --- a/spec/lib/banzai/filter/redactor_filter_spec.rb +++ b/spec/lib/banzai/filter/reference_redactor_filter_spec.rb @@ -1,6 +1,6 @@ require 'spec_helper' -describe Banzai::Filter::RedactorFilter do +describe Banzai::Filter::ReferenceRedactorFilter do include ActionView::Helpers::UrlHelper include FilterSpecHelper diff --git a/spec/lib/banzai/object_renderer_spec.rb b/spec/lib/banzai/object_renderer_spec.rb index 7b855251a74..e3e6e22568c 100644 --- a/spec/lib/banzai/object_renderer_spec.rb +++ b/spec/lib/banzai/object_renderer_spec.rb @@ -22,8 +22,8 @@ describe Banzai::ObjectRenderer do expect(object.user_visible_reference_count).to eq 0 end - it 'calls Banzai::Redactor to perform redaction' do - expect_any_instance_of(Banzai::Redactor).to receive(:redact).and_call_original + it 'calls Banzai::ReferenceRedactor to perform redaction' do + expect_any_instance_of(Banzai::ReferenceRedactor).to receive(:redact).and_call_original renderer.render([object], :note) end @@ -82,8 +82,8 @@ describe Banzai::ObjectRenderer do expect(cacheless_thing.redacted_title_html).to eq("Merge branch 'branch-merged' into 'master'") end - it 'calls Banzai::Redactor to perform redaction' do - expect_any_instance_of(Banzai::Redactor).to receive(:redact).and_call_original + it 'calls Banzai::ReferenceRedactor to perform redaction' do + expect_any_instance_of(Banzai::ReferenceRedactor).to receive(:redact).and_call_original renderer.render([cacheless_thing], :title) end diff --git a/spec/lib/banzai/pipeline/gfm_pipeline_spec.rb b/spec/lib/banzai/pipeline/gfm_pipeline_spec.rb index 7119c826bca..469692f7b5a 100644 --- a/spec/lib/banzai/pipeline/gfm_pipeline_spec.rb +++ b/spec/lib/banzai/pipeline/gfm_pipeline_spec.rb @@ -32,7 +32,7 @@ describe Banzai::Pipeline::GfmPipeline do result = described_class.call(markdown, project: project)[:output] link = result.css('a').first - expect(link['href']).to eq 'http://redmine/projects/project_name_in_redmine/issues/12' + expect(link['href']).to eq 'http://issue-tracker.example.com/issues/12' end it 'parses cross-project references to regular issues' do @@ -61,7 +61,7 @@ describe Banzai::Pipeline::GfmPipeline do result = described_class.call(markdown, project: project)[:output] link = result.css('a').first - expect(link['href']).to eq 'http://redmine/projects/project_name_in_redmine/issues/12' + expect(link['href']).to eq 'http://issue-tracker.example.com/issues/12' end it 'allows to use long external reference syntax for Redmine' do @@ -70,7 +70,7 @@ describe Banzai::Pipeline::GfmPipeline do result = described_class.call(markdown, project: project)[:output] link = result.css('a').first - expect(link['href']).to eq 'http://redmine/projects/project_name_in_redmine/issues/12' + expect(link['href']).to eq 'http://issue-tracker.example.com/issues/12' end it 'parses cross-project references to regular issues' do diff --git a/spec/lib/banzai/redactor_spec.rb b/spec/lib/banzai/reference_redactor_spec.rb index 718649e0e10..a3b47c4d826 100644 --- a/spec/lib/banzai/redactor_spec.rb +++ b/spec/lib/banzai/reference_redactor_spec.rb @@ -1,6 +1,6 @@ require 'spec_helper' -describe Banzai::Redactor do +describe Banzai::ReferenceRedactor do let(:user) { create(:user) } let(:project) { build(:project) } let(:redactor) { described_class.new(Banzai::RenderContext.new(project, user)) } diff --git a/spec/lib/gitlab/asciidoc_spec.rb b/spec/lib/gitlab/asciidoc_spec.rb index ff002acbd35..cbd4a509a55 100644 --- a/spec/lib/gitlab/asciidoc_spec.rb +++ b/spec/lib/gitlab/asciidoc_spec.rb @@ -96,6 +96,125 @@ module Gitlab end end + context 'with passthrough' do + it 'removes non heading ids' do + input = <<~ADOC + ++++ + <h2 id="foo">Title</h2> + ++++ + ADOC + + output = <<~HTML + <h2>Title</h2> + HTML + + expect(render(input, context)).to include(output.strip) + end + + it 'removes non footnote def ids' do + input = <<~ADOC + ++++ + <div id="def">Footnote definition</div> + ++++ + ADOC + + output = <<~HTML + <div>Footnote definition</div> + HTML + + expect(render(input, context)).to include(output.strip) + end + + it 'removes non footnote ref ids' do + input = <<~ADOC + ++++ + <a id="ref">Footnote reference</a> + ++++ + ADOC + + output = <<~HTML + <a>Footnote reference</a> + HTML + + expect(render(input, context)).to include(output.strip) + end + end + + context 'with footnotes' do + it 'preserves ids and links' do + input = <<~ADOC + This paragraph has a footnote.footnote:[This is the text of the footnote.] + ADOC + + output = <<~HTML + <div> + <p>This paragraph has a footnote.<sup>[<a id="_footnoteref_1" href="#_footnotedef_1" title="View footnote.">1</a>]</sup></p> + </div> + <div> + <hr> + <div id="_footnotedef_1"> + <a href="#_footnoteref_1">1</a>. This is the text of the footnote. + </div> + </div> + HTML + + expect(render(input, context)).to include(output.strip) + end + end + + context 'with section anchors' do + it 'preserves ids and links' do + input = <<~ADOC + = Title + + == First section + + This is the first section. + + == Second section + + This is the second section. + + == Thunder ⚡ ! + + This is the third section. + ADOC + + output = <<~HTML + <h1>Title</h1> + <div> + <h2 id="user-content-first-section"> + <a class="anchor" href="#user-content-first-section"></a>First section</h2> + <div> + <div> + <p>This is the first section.</p> + </div> + </div> + </div> + <div> + <h2 id="user-content-second-section"> + <a class="anchor" href="#user-content-second-section"></a>Second section</h2> + <div> + <div> + <p>This is the second section.</p> + </div> + </div> + </div> + <div> + <h2 id="user-content-thunder"> + <a class="anchor" href="#user-content-thunder"></a>Thunder ⚡ !</h2> + <div> + <div> + <p>This is the third section.</p> + </div> + </div> + </div> + HTML + + expect(render(input, context)).to include(output.strip) + end + end + context 'with checklist' do it 'preserves classes' do input = <<~ADOC diff --git a/spec/lib/gitlab/auth_spec.rb b/spec/lib/gitlab/auth_spec.rb index d9c73cff01e..0403830f700 100644 --- a/spec/lib/gitlab/auth_spec.rb +++ b/spec/lib/gitlab/auth_spec.rb @@ -297,6 +297,70 @@ describe Gitlab::Auth do let(:project) { create(:project) } let(:auth_failure) { Gitlab::Auth::Result.new(nil, nil) } + context 'when deploy token and user have the same username' do + let(:username) { 'normal_user' } + let(:user) { create(:user, username: username, password: 'my-secret') } + let(:deploy_token) { create(:deploy_token, username: username, read_registry: false, projects: [project]) } + + before do + expect(gl_auth).to receive(:rate_limit!).with('ip', success: true, login: username) + end + + it 'succeeds for the token' do + auth_success = Gitlab::Auth::Result.new(deploy_token, project, :deploy_token, [:download_code]) + + expect(gl_auth.find_for_git_client(username, deploy_token.token, project: project, ip: 'ip')) + .to eq(auth_success) + end + + it 'succeeds for the user' do + auth_success = Gitlab::Auth::Result.new(user, nil, :gitlab_or_ldap, full_authentication_abilities) + + expect(gl_auth.find_for_git_client(username, 'my-secret', project: project, ip: 'ip')) + .to eq(auth_success) + end + end + + context 'when deploy tokens have the same username' do + context 'and belong to the same project' do + let!(:read_registry) { create(:deploy_token, username: 'deployer', read_repository: false, projects: [project]) } + let!(:read_repository) { create(:deploy_token, username: read_registry.username, read_registry: false, projects: [project]) } + + it 'succeeds for the right token' do + auth_success = Gitlab::Auth::Result.new(read_repository, project, :deploy_token, [:download_code]) + + expect(gl_auth).to receive(:rate_limit!).with('ip', success: true, login: 'deployer') + expect(gl_auth.find_for_git_client('deployer', read_repository.token, project: project, ip: 'ip')) + .to eq(auth_success) + end + + it 'fails for the wrong token' do + expect(gl_auth).to receive(:rate_limit!).with('ip', success: false, login: 'deployer') + expect(gl_auth.find_for_git_client('deployer', read_registry.token, project: project, ip: 'ip')) + .to eq(auth_failure) + end + end + + context 'and belong to different projects' do + let!(:read_registry) { create(:deploy_token, username: 'deployer', read_repository: false, projects: [create(:project)]) } + let!(:read_repository) { create(:deploy_token, username: read_registry.username, read_registry: false, projects: [project]) } + + it 'succeeds for the right token' do + auth_success = Gitlab::Auth::Result.new(read_repository, project, :deploy_token, [:download_code]) + + expect(gl_auth).to receive(:rate_limit!).with('ip', success: true, login: 'deployer') + expect(gl_auth.find_for_git_client('deployer', read_repository.token, project: project, ip: 'ip')) + .to eq(auth_success) + end + + it 'fails for the wrong token' do + expect(gl_auth).to receive(:rate_limit!).with('ip', success: false, login: 'deployer') + expect(gl_auth.find_for_git_client('deployer', read_registry.token, project: project, ip: 'ip')) + .to eq(auth_failure) + end + end + end + context 'when the deploy token has read_repository as scope' do let(:deploy_token) { create(:deploy_token, read_registry: false, projects: [project]) } let(:login) { deploy_token.username } diff --git a/spec/lib/gitlab/ci/pipeline/chain/validate/abilities_spec.rb b/spec/lib/gitlab/ci/pipeline/chain/validate/abilities_spec.rb index 7d750877d09..b3e58c3dfdb 100644 --- a/spec/lib/gitlab/ci/pipeline/chain/validate/abilities_spec.rb +++ b/spec/lib/gitlab/ci/pipeline/chain/validate/abilities_spec.rb @@ -10,7 +10,11 @@ describe Gitlab::Ci::Pipeline::Chain::Validate::Abilities do let(:command) do Gitlab::Ci::Pipeline::Chain::Command.new( - project: project, current_user: user, origin_ref: origin_ref, merge_request: merge_request) + project: project, + current_user: user, + origin_ref: origin_ref, + merge_request: merge_request, + trigger_request: trigger_request) end let(:step) { described_class.new(pipeline, command) } @@ -18,6 +22,7 @@ describe Gitlab::Ci::Pipeline::Chain::Validate::Abilities do let(:ref) { 'master' } let(:origin_ref) { ref } let(:merge_request) { nil } + let(:trigger_request) { nil } shared_context 'detached merge request pipeline' do let(:merge_request) do @@ -69,6 +74,43 @@ describe Gitlab::Ci::Pipeline::Chain::Validate::Abilities do end end + context 'when pipeline triggered by legacy trigger' do + let(:user) { nil } + let(:trigger_request) do + build_stubbed(:ci_trigger_request, trigger: build_stubbed(:ci_trigger, owner: nil)) + end + + context 'when :use_legacy_pipeline_triggers feature flag is enabled' do + before do + stub_feature_flags(use_legacy_pipeline_triggers: true) + step.perform! + end + + it 'allows legacy triggers to create a pipeline' do + expect(pipeline).to be_valid + end + + it 'does not break the chain' do + expect(step.break?).to eq false + end + end + + context 'when :use_legacy_pipeline_triggers feature flag is disabled' do + before do + stub_feature_flags(use_legacy_pipeline_triggers: false) + step.perform! + end + + it 'prevents legacy triggers from creating a pipeline' do + expect(pipeline.errors.to_a).to include /Trigger token is invalid/ + end + + it 'breaks the pipeline builder chain' do + expect(step.break?).to eq true + end + end + end + describe '#allowed_to_create?' do subject { step.allowed_to_create? } diff --git a/spec/lib/gitlab/ci/pipeline/expression/lexeme/pattern_spec.rb b/spec/lib/gitlab/ci/pipeline/expression/lexeme/pattern_spec.rb index 30ea3f3e28e..6ce4b321397 100644 --- a/spec/lib/gitlab/ci/pipeline/expression/lexeme/pattern_spec.rb +++ b/spec/lib/gitlab/ci/pipeline/expression/lexeme/pattern_spec.rb @@ -107,42 +107,6 @@ describe Gitlab::Ci::Pipeline::Expression::Lexeme::Pattern do expect(token.build.evaluate) .to eq Gitlab::UntrustedRegexp.new('some numeric \$ pattern') end - - context 'with the ci_variables_complex_expressions feature flag disabled' do - before do - stub_feature_flags(ci_variables_complex_expressions: false) - end - - it 'is a greedy scanner for regexp boundaries' do - scanner = StringScanner.new('/some .* / pattern/') - - token = described_class.scan(scanner) - - expect(token).not_to be_nil - expect(token.build.evaluate) - .to eq Gitlab::UntrustedRegexp.new('some .* / pattern') - end - - it 'does not recognize the \ escape character for /' do - scanner = StringScanner.new('/some .* \/ pattern/') - - token = described_class.scan(scanner) - - expect(token).not_to be_nil - expect(token.build.evaluate) - .to eq Gitlab::UntrustedRegexp.new('some .* \/ pattern') - end - - it 'does not recognize the \ escape character for $' do - scanner = StringScanner.new('/some numeric \$ pattern/') - - token = described_class.scan(scanner) - - expect(token).not_to be_nil - expect(token.build.evaluate) - .to eq Gitlab::UntrustedRegexp.new('some numeric \$ pattern') - end - end end describe '#evaluate' do diff --git a/spec/lib/gitlab/ci/pipeline/expression/lexer_spec.rb b/spec/lib/gitlab/ci/pipeline/expression/lexer_spec.rb index d8db9c262a1..7c98e729b0b 100644 --- a/spec/lib/gitlab/ci/pipeline/expression/lexer_spec.rb +++ b/spec/lib/gitlab/ci/pipeline/expression/lexer_spec.rb @@ -80,34 +80,6 @@ describe Gitlab::Ci::Pipeline::Expression::Lexer do it { is_expected.to eq(tokens) } end end - - context 'with the ci_variables_complex_expressions feature flag turned off' do - before do - stub_feature_flags(ci_variables_complex_expressions: false) - end - - it 'incorrectly tokenizes conjunctive match statements as one match statement' do - tokens = described_class.new('$PRESENT_VARIABLE =~ /my var/ && $EMPTY_VARIABLE =~ /nope/').tokens - - expect(tokens.map(&:value)).to eq(['$PRESENT_VARIABLE', '=~', '/my var/ && $EMPTY_VARIABLE =~ /nope/']) - end - - it 'incorrectly tokenizes disjunctive match statements as one statement' do - tokens = described_class.new('$PRESENT_VARIABLE =~ /my var/ || $EMPTY_VARIABLE =~ /nope/').tokens - - expect(tokens.map(&:value)).to eq(['$PRESENT_VARIABLE', '=~', '/my var/ || $EMPTY_VARIABLE =~ /nope/']) - end - - it 'raises an error about && operators' do - expect { described_class.new('$EMPTY_VARIABLE == "" && $PRESENT_VARIABLE').tokens } - .to raise_error(Gitlab::Ci::Pipeline::Expression::Lexer::SyntaxError).with_message('Unknown lexeme found!') - end - - it 'raises an error about || operators' do - expect { described_class.new('$EMPTY_VARIABLE == "" || $PRESENT_VARIABLE').tokens } - .to raise_error(Gitlab::Ci::Pipeline::Expression::Lexer::SyntaxError).with_message('Unknown lexeme found!') - end - end end describe '#lexemes' do diff --git a/spec/lib/gitlab/ci/pipeline/expression/statement_spec.rb b/spec/lib/gitlab/ci/pipeline/expression/statement_spec.rb index a2c2e3653d5..b259ef711aa 100644 --- a/spec/lib/gitlab/ci/pipeline/expression/statement_spec.rb +++ b/spec/lib/gitlab/ci/pipeline/expression/statement_spec.rb @@ -1,6 +1,4 @@ -# TODO switch this back after the "ci_variables_complex_expressions" feature flag is removed -# require 'fast_spec_helper' -require 'spec_helper' +require 'fast_spec_helper' require 'rspec-parameterized' describe Gitlab::Ci::Pipeline::Expression::Statement do @@ -118,54 +116,6 @@ describe Gitlab::Ci::Pipeline::Expression::Statement do expect(subject.evaluate).to eq(value) end end - - context 'with the ci_variables_complex_expressions feature flag disabled' do - before do - stub_feature_flags(ci_variables_complex_expressions: false) - end - - where(:expression, :value) do - '$PRESENT_VARIABLE == "my variable"' | true - '"my variable" == $PRESENT_VARIABLE' | true - '$PRESENT_VARIABLE == null' | false - '$EMPTY_VARIABLE == null' | false - '"" == $EMPTY_VARIABLE' | true - '$EMPTY_VARIABLE' | '' - '$UNDEFINED_VARIABLE == null' | true - 'null == $UNDEFINED_VARIABLE' | true - '$PRESENT_VARIABLE' | 'my variable' - '$UNDEFINED_VARIABLE' | nil - "$PRESENT_VARIABLE =~ /var.*e$/" | true - "$PRESENT_VARIABLE =~ /^var.*/" | false - "$EMPTY_VARIABLE =~ /var.*/" | false - "$UNDEFINED_VARIABLE =~ /var.*/" | false - "$PRESENT_VARIABLE =~ /VAR.*/i" | true - '$PATH_VARIABLE =~ /path/variable/' | true - '$PATH_VARIABLE =~ /path\/variable/' | true - '$FULL_PATH_VARIABLE =~ /^/a/full/path/variable/value$/' | true - '$FULL_PATH_VARIABLE =~ /^\/a\/full\/path\/variable\/value$/' | true - '$PRESENT_VARIABLE != "my variable"' | false - '"my variable" != $PRESENT_VARIABLE' | false - '$PRESENT_VARIABLE != null' | true - '$EMPTY_VARIABLE != null' | true - '"" != $EMPTY_VARIABLE' | false - '$UNDEFINED_VARIABLE != null' | false - 'null != $UNDEFINED_VARIABLE' | false - "$PRESENT_VARIABLE !~ /var.*e$/" | false - "$PRESENT_VARIABLE !~ /^var.*/" | true - "$EMPTY_VARIABLE !~ /var.*/" | true - "$UNDEFINED_VARIABLE !~ /var.*/" | true - "$PRESENT_VARIABLE !~ /VAR.*/i" | false - end - - with_them do - let(:text) { expression } - - it "evaluates to `#{params[:value].inspect}`" do - expect(subject.evaluate).to eq value - end - end - end end describe '#truthful?' do diff --git a/spec/lib/gitlab/ci/pipeline/seed/build_spec.rb b/spec/lib/gitlab/ci/pipeline/seed/build_spec.rb index 7991e2f48b5..46ea0d7554b 100644 --- a/spec/lib/gitlab/ci/pipeline/seed/build_spec.rb +++ b/spec/lib/gitlab/ci/pipeline/seed/build_spec.rb @@ -1,32 +1,30 @@ +# frozen_string_literal: true + require 'spec_helper' describe Gitlab::Ci::Pipeline::Seed::Build do let(:project) { create(:project, :repository) } let(:pipeline) { create(:ci_empty_pipeline, project: project) } + let(:attributes) { { name: 'rspec', ref: 'master' } } - let(:attributes) do - { name: 'rspec', ref: 'master' } - end - - subject do - described_class.new(pipeline, attributes) - end + let(:seed_build) { described_class.new(pipeline, attributes) } describe '#attributes' do - it 'returns hash attributes of a build' do - expect(subject.attributes).to be_a Hash - expect(subject.attributes) - .to include(:name, :project, :ref) - end + subject { seed_build.attributes } + + it { is_expected.to be_a(Hash) } + it { is_expected.to include(:name, :project, :ref) } end describe '#bridge?' do + subject { seed_build.bridge? } + context 'when job is a bridge' do let(:attributes) do { name: 'rspec', ref: 'master', options: { trigger: 'my/project' } } end - it { is_expected.to be_bridge } + it { is_expected.to be_truthy } end context 'when trigger definition is empty' do @@ -34,20 +32,20 @@ describe Gitlab::Ci::Pipeline::Seed::Build do { name: 'rspec', ref: 'master', options: { trigger: '' } } end - it { is_expected.not_to be_bridge } + it { is_expected.to be_falsey } end context 'when job is not a bridge' do - it { is_expected.not_to be_bridge } + it { is_expected.to be_falsey } end end describe '#to_resource' do + subject { seed_build.to_resource } + context 'when job is not a bridge' do - it 'returns a valid build resource' do - expect(subject.to_resource).to be_a(::Ci::Build) - expect(subject.to_resource).to be_valid - end + it { is_expected.to be_a(::Ci::Build) } + it { is_expected.to be_valid } end context 'when job is a bridge' do @@ -55,71 +53,117 @@ describe Gitlab::Ci::Pipeline::Seed::Build do { name: 'rspec', ref: 'master', options: { trigger: 'my/project' } } end - it 'returns a valid bridge resource' do - expect(subject.to_resource).to be_a(::Ci::Bridge) - expect(subject.to_resource).to be_valid - end + it { is_expected.to be_a(::Ci::Bridge) } + it { is_expected.to be_valid } end it 'memoizes a resource object' do - build = subject.to_resource - - expect(build.object_id).to eq subject.to_resource.object_id + expect(subject.object_id).to eq seed_build.to_resource.object_id end it 'can not be persisted without explicit assignment' do - build = subject.to_resource - pipeline.save! - expect(build).not_to be_persisted + expect(subject).not_to be_persisted end end - describe 'applying only/except policies' do + describe 'applying job inclusion policies' do + subject { seed_build } + context 'when no branch policy is specified' do - let(:attributes) { { name: 'rspec' } } + let(:attributes) do + { name: 'rspec' } + end it { is_expected.to be_included } end context 'when branch policy does not match' do context 'when using only' do - let(:attributes) { { name: 'rspec', only: { refs: ['deploy'] } } } + let(:attributes) do + { name: 'rspec', only: { refs: ['deploy'] } } + end it { is_expected.not_to be_included } end context 'when using except' do - let(:attributes) { { name: 'rspec', except: { refs: ['deploy'] } } } + let(:attributes) do + { name: 'rspec', except: { refs: ['deploy'] } } + end it { is_expected.to be_included } end + + context 'with both only and except policies' do + let(:attributes) do + { + name: 'rspec', + only: { refs: %w[deploy] }, + except: { refs: %w[deploy] } + } + end + + it { is_expected.not_to be_included } + end end context 'when branch regexp policy does not match' do context 'when using only' do - let(:attributes) { { name: 'rspec', only: { refs: ['/^deploy$/'] } } } + let(:attributes) do + { name: 'rspec', only: { refs: %w[/^deploy$/] } } + end it { is_expected.not_to be_included } end context 'when using except' do - let(:attributes) { { name: 'rspec', except: { refs: ['/^deploy$/'] } } } + let(:attributes) do + { name: 'rspec', except: { refs: %w[/^deploy$/] } } + end it { is_expected.to be_included } end + + context 'with both only and except policies' do + let(:attributes) do + { + name: 'rspec', + only: { refs: %w[/^deploy$/] }, + except: { refs: %w[/^deploy$/] } + } + end + + it { is_expected.not_to be_included } + end end context 'when branch policy matches' do context 'when using only' do - let(:attributes) { { name: 'rspec', only: { refs: %w[deploy master] } } } + let(:attributes) do + { name: 'rspec', only: { refs: %w[deploy master] } } + end it { is_expected.to be_included } end context 'when using except' do - let(:attributes) { { name: 'rspec', except: { refs: %w[deploy master] } } } + let(:attributes) do + { name: 'rspec', except: { refs: %w[deploy master] } } + end + + it { is_expected.not_to be_included } + end + + context 'when using both only and except policies' do + let(:attributes) do + { + name: 'rspec', + only: { refs: %w[deploy master] }, + except: { refs: %w[deploy master] } + } + end it { is_expected.not_to be_included } end @@ -127,13 +171,29 @@ describe Gitlab::Ci::Pipeline::Seed::Build do context 'when keyword policy matches' do context 'when using only' do - let(:attributes) { { name: 'rspec', only: { refs: ['branches'] } } } + let(:attributes) do + { name: 'rspec', only: { refs: %w[branches] } } + end it { is_expected.to be_included } end context 'when using except' do - let(:attributes) { { name: 'rspec', except: { refs: ['branches'] } } } + let(:attributes) do + { name: 'rspec', except: { refs: %w[branches] } } + end + + it { is_expected.not_to be_included } + end + + context 'when using both only and except policies' do + let(:attributes) do + { + name: 'rspec', + only: { refs: %w[branches] }, + except: { refs: %w[branches] } + } + end it { is_expected.not_to be_included } end @@ -141,50 +201,78 @@ describe Gitlab::Ci::Pipeline::Seed::Build do context 'when keyword policy does not match' do context 'when using only' do - let(:attributes) { { name: 'rspec', only: { refs: ['tags'] } } } + let(:attributes) do + { name: 'rspec', only: { refs: %w[tags] } } + end it { is_expected.not_to be_included } end context 'when using except' do - let(:attributes) { { name: 'rspec', except: { refs: ['tags'] } } } + let(:attributes) do + { name: 'rspec', except: { refs: %w[tags] } } + end it { is_expected.to be_included } end + + context 'when using both only and except policies' do + let(:attributes) do + { + name: 'rspec', + only: { refs: %w[tags] }, + except: { refs: %w[tags] } + } + end + + it { is_expected.not_to be_included } + end end context 'with source-keyword policy' do using RSpec::Parameterized - let(:pipeline) { build(:ci_empty_pipeline, ref: 'deploy', tag: false, source: source) } + let(:pipeline) do + build(:ci_empty_pipeline, ref: 'deploy', tag: false, source: source) + end context 'matches' do where(:keyword, :source) do [ - %w(pushes push), - %w(web web), - %w(triggers trigger), - %w(schedules schedule), - %w(api api), - %w(external external) + %w[pushes push], + %w[web web], + %w[triggers trigger], + %w[schedules schedule], + %w[api api], + %w[external external] ] end with_them do context 'using an only policy' do - let(:attributes) { { name: 'rspec', only: { refs: [keyword] } } } + let(:attributes) do + { name: 'rspec', only: { refs: [keyword] } } + end it { is_expected.to be_included } end context 'using an except policy' do - let(:attributes) { { name: 'rspec', except: { refs: [keyword] } } } + let(:attributes) do + { name: 'rspec', except: { refs: [keyword] } } + end it { is_expected.not_to be_included } end context 'using both only and except policies' do - let(:attributes) { { name: 'rspec', only: { refs: [keyword] }, except: { refs: [keyword] } } } + let(:attributes) do + { + name: 'rspec', + only: { refs: [keyword] }, + except: { refs: [keyword] } + } + end it { is_expected.not_to be_included } end @@ -193,29 +281,39 @@ describe Gitlab::Ci::Pipeline::Seed::Build do context 'non-matches' do where(:keyword, :source) do - %w(web trigger schedule api external).map { |source| ['pushes', source] } + - %w(push trigger schedule api external).map { |source| ['web', source] } + - %w(push web schedule api external).map { |source| ['triggers', source] } + - %w(push web trigger api external).map { |source| ['schedules', source] } + - %w(push web trigger schedule external).map { |source| ['api', source] } + - %w(push web trigger schedule api).map { |source| ['external', source] } + %w[web trigger schedule api external].map { |source| ['pushes', source] } + + %w[push trigger schedule api external].map { |source| ['web', source] } + + %w[push web schedule api external].map { |source| ['triggers', source] } + + %w[push web trigger api external].map { |source| ['schedules', source] } + + %w[push web trigger schedule external].map { |source| ['api', source] } + + %w[push web trigger schedule api].map { |source| ['external', source] } end with_them do context 'using an only policy' do - let(:attributes) { { name: 'rspec', only: { refs: [keyword] } } } + let(:attributes) do + { name: 'rspec', only: { refs: [keyword] } } + end it { is_expected.not_to be_included } end context 'using an except policy' do - let(:attributes) { { name: 'rspec', except: { refs: [keyword] } } } + let(:attributes) do + { name: 'rspec', except: { refs: [keyword] } } + end it { is_expected.to be_included } end context 'using both only and except policies' do - let(:attributes) { { name: 'rspec', only: { refs: [keyword] }, except: { refs: [keyword] } } } + let(:attributes) do + { + name: 'rspec', + only: { refs: [keyword] }, + except: { refs: [keyword] } + } + end it { is_expected.not_to be_included } end @@ -239,12 +337,24 @@ describe Gitlab::Ci::Pipeline::Seed::Build do it { is_expected.not_to be_included } end + + context 'when using both only and except policies' do + let(:attributes) do + { + name: 'rspec', + only: { refs: ["branches@#{pipeline.project_full_path}"] }, + except: { refs: ["branches@#{pipeline.project_full_path}"] } + } + end + + it { is_expected.not_to be_included } + end end context 'when repository path does not matches' do context 'when using only' do let(:attributes) do - { name: 'rspec', only: { refs: ['branches@fork'] } } + { name: 'rspec', only: { refs: %w[branches@fork] } } end it { is_expected.not_to be_included } @@ -252,11 +362,23 @@ describe Gitlab::Ci::Pipeline::Seed::Build do context 'when using except' do let(:attributes) do - { name: 'rspec', except: { refs: ['branches@fork'] } } + { name: 'rspec', except: { refs: %w[branches@fork] } } end it { is_expected.to be_included } end + + context 'when using both only and except policies' do + let(:attributes) do + { + name: 'rspec', + only: { refs: %w[branches@fork] }, + except: { refs: %w[branches@fork] } + } + end + + it { is_expected.not_to be_included } + end end end end diff --git a/spec/lib/gitlab/git/repository_spec.rb b/spec/lib/gitlab/git/repository_spec.rb index a28b95e5bff..41b898df112 100644 --- a/spec/lib/gitlab/git/repository_spec.rb +++ b/spec/lib/gitlab/git/repository_spec.rb @@ -256,6 +256,22 @@ describe Gitlab::Git::Repository, :seed_helper do end end + describe '#submodule_urls_for' do + let(:ref) { 'master' } + + it 'returns url mappings for submodules' do + urls = repository.submodule_urls_for(ref) + + expect(urls).to eq({ + "deeper/nested/six" => "git://github.com/randx/six.git", + "gitlab-grack" => "https://gitlab.com/gitlab-org/gitlab-grack.git", + "gitlab-shell" => "https://github.com/gitlabhq/gitlab-shell.git", + "nested/six" => "git://github.com/randx/six.git", + "six" => "git://github.com/randx/six.git" + }) + end + end + describe '#commit_count' do it { expect(repository.commit_count("master")).to eq(25) } it { expect(repository.commit_count("feature")).to eq(9) } diff --git a/spec/lib/gitlab/gitaly_client_spec.rb b/spec/lib/gitlab/gitaly_client_spec.rb index eed233f1f3e..b8debee3b58 100644 --- a/spec/lib/gitlab/gitaly_client_spec.rb +++ b/spec/lib/gitlab/gitaly_client_spec.rb @@ -171,17 +171,6 @@ describe Gitlab::GitalyClient do end end end - - context 'when catfile-cache feature is disabled' do - before do - stub_feature_flags({ 'gitaly_catfile-cache': false }) - end - - it 'does not set the gitaly-session-id in the metadata' do - results = described_class.request_kwargs('default', nil) - expect(results[:metadata]).not_to include('gitaly-session-id') - end - end end describe 'enforce_gitaly_request_limits?' do diff --git a/spec/lib/gitlab/graphql/representation/submodule_tree_entry_spec.rb b/spec/lib/gitlab/graphql/representation/submodule_tree_entry_spec.rb new file mode 100644 index 00000000000..28056a6085d --- /dev/null +++ b/spec/lib/gitlab/graphql/representation/submodule_tree_entry_spec.rb @@ -0,0 +1,30 @@ +# frozen_string_literal: true + +require 'spec_helper' + +describe Gitlab::Graphql::Representation::SubmoduleTreeEntry do + let(:project) { create(:project, :repository) } + let(:repository) { project.repository } + + describe '.decorate' do + let(:submodules) { repository.tree.submodules } + + it 'returns array of SubmoduleTreeEntry' do + entries = described_class.decorate(submodules, repository.tree) + + expect(entries.first).to be_a(described_class) + + expect(entries.map(&:web_url)).to contain_exactly( + "https://gitlab.com/gitlab-org/gitlab-grack", + "https://github.com/gitlabhq/gitlab-shell", + "https://github.com/randx/six" + ) + + expect(entries.map(&:tree_url)).to contain_exactly( + "https://gitlab.com/gitlab-org/gitlab-grack/tree/645f6c4c82fd3f5e06f67134450a570b795e55a6", + "https://github.com/gitlabhq/gitlab-shell/tree/79bceae69cb5750d6567b223597999bfa91cb3b9", + "https://github.com/randx/six/tree/409f37c4f05865e4fb208c771485f211a22c4c2d" + ) + end + end +end diff --git a/spec/lib/gitlab/import_export/project.json b/spec/lib/gitlab/import_export/project.json index 8be074f4b9b..c0b97486eeb 100644 --- a/spec/lib/gitlab/import_export/project.json +++ b/spec/lib/gitlab/import_export/project.json @@ -6630,8 +6630,16 @@ "id": 123, "token": "cdbfasdf44a5958c83654733449e585", "project_id": 5, + "owner_id": 1, "created_at": "2017-01-16T15:25:28.637Z", "updated_at": "2017-01-16T15:25:28.637Z" + }, + { + "id": 456, + "token": "33a66349b5ad01fc00174af87804e40", + "project_id": 5, + "created_at": "2017-01-16T15:25:29.637Z", + "updated_at": "2017-01-16T15:25:29.637Z" } ], "deploy_keys": [], diff --git a/spec/lib/gitlab/import_export/project_tree_restorer_spec.rb b/spec/lib/gitlab/import_export/project_tree_restorer_spec.rb index ca46006ea58..e6ce3f1bcea 100644 --- a/spec/lib/gitlab/import_export/project_tree_restorer_spec.rb +++ b/spec/lib/gitlab/import_export/project_tree_restorer_spec.rb @@ -32,6 +32,10 @@ describe Gitlab::ImportExport::ProjectTreeRestorer do end context 'JSON' do + before do + stub_feature_flags(use_legacy_pipeline_triggers: false) + end + it 'restores models based on JSON' do expect(@restored_project_json).to be_truthy end @@ -198,8 +202,9 @@ describe Gitlab::ImportExport::ProjectTreeRestorer do end context 'tokens are regenerated' do - it 'has a new CI trigger token' do - expect(Ci::Trigger.where(token: 'cdbfasdf44a5958c83654733449e585')).to be_empty + it 'has new CI trigger tokens' do + expect(Ci::Trigger.where(token: %w[cdbfasdf44a5958c83654733449e585 33a66349b5ad01fc00174af87804e40])) + .to be_empty end it 'has a new CI build token' do @@ -212,7 +217,7 @@ describe Gitlab::ImportExport::ProjectTreeRestorer do expect(@project.merge_requests.size).to eq(9) end - it 'has the correct number of triggers' do + it 'only restores valid triggers' do expect(@project.triggers.size).to eq(1) end diff --git a/spec/lib/gitlab/usage_data_counters/redis_counter_spec.rb b/spec/lib/gitlab/usage_data_counters/redis_counter_spec.rb new file mode 100644 index 00000000000..38b4c22e186 --- /dev/null +++ b/spec/lib/gitlab/usage_data_counters/redis_counter_spec.rb @@ -0,0 +1,54 @@ +# frozen_string_literal: true + +require 'spec_helper' + +describe Gitlab::UsageDataCounters::RedisCounter, :clean_gitlab_redis_shared_state do + context 'when redis_key is not defined' do + subject do + Class.new.extend(described_class) + end + + describe '.increment' do + it 'raises a NotImplementedError exception' do + expect { subject.increment}.to raise_error(NotImplementedError) + end + end + + describe '.total_count' do + it 'raises a NotImplementedError exception' do + expect { subject.total_count}.to raise_error(NotImplementedError) + end + end + end + + context 'when redis_key is defined' do + subject do + counter_module = described_class + + Class.new do + extend counter_module + + def self.redis_counter_key + 'foo_redis_key' + end + end + end + + describe '.increment' do + it 'increments the web ide commits counter by 1' do + expect do + subject.increment + end.to change { subject.total_count }.from(0).to(1) + end + end + + describe '.total_count' do + it 'returns the total amount of web ide commits' do + subject.increment + subject.increment + + expect(subject.total_count).to eq(2) + end + end + end +end diff --git a/spec/lib/gitlab/usage_data_spec.rb b/spec/lib/gitlab/usage_data_spec.rb index 67d49a30825..90a534de202 100644 --- a/spec/lib/gitlab/usage_data_spec.rb +++ b/spec/lib/gitlab/usage_data_spec.rb @@ -8,7 +8,7 @@ describe Gitlab::UsageData do before do create(:jira_service, project: projects[0]) create(:jira_service, project: projects[1]) - create(:jira_cloud_service, project: projects[2]) + create(:jira_service, :jira_cloud_service, project: projects[2]) create(:prometheus_service, project: projects[1]) create(:service, project: projects[0], type: 'SlackSlashCommandsService', active: true) create(:service, project: projects[1], type: 'SlackService', active: true) diff --git a/spec/lib/gitlab/web_ide_commits_counter_spec.rb b/spec/lib/gitlab/web_ide_commits_counter_spec.rb deleted file mode 100644 index c51889a1c63..00000000000 --- a/spec/lib/gitlab/web_ide_commits_counter_spec.rb +++ /dev/null @@ -1,19 +0,0 @@ -# frozen_string_literal: true - -require 'spec_helper' - -describe Gitlab::WebIdeCommitsCounter, :clean_gitlab_redis_shared_state do - describe '.increment' do - it 'increments the web ide commits counter by 1' do - expect do - described_class.increment - end.to change { described_class.total_count }.from(0).to(1) - end - end - - describe '.total_count' do - it 'returns the total amount of web ide commits' do - expect(described_class.total_count).to eq(0) - end - end -end diff --git a/spec/lib/gitlab/zoom_link_extractor_spec.rb b/spec/lib/gitlab/zoom_link_extractor_spec.rb new file mode 100644 index 00000000000..52387fc3688 --- /dev/null +++ b/spec/lib/gitlab/zoom_link_extractor_spec.rb @@ -0,0 +1,24 @@ +# frozen_string_literal: true + +require 'spec_helper' + +describe Gitlab::ZoomLinkExtractor do + describe "#links" do + using RSpec::Parameterized::TableSyntax + + where(:text, :links) do + 'issue text https://zoom.us/j/123 and https://zoom.us/s/1123433' | %w[https://zoom.us/j/123 https://zoom.us/s/1123433] + 'https://zoom.us/j/1123433 issue text' | %w[https://zoom.us/j/1123433] + 'issue https://zoom.us/my/1123433 text' | %w[https://zoom.us/my/1123433] + 'issue https://gitlab.com and https://gitlab.zoom.us/s/1123433' | %w[https://gitlab.zoom.us/s/1123433] + 'https://gitlab.zoom.us/j/1123433' | %w[https://gitlab.zoom.us/j/1123433] + 'https://gitlab.zoom.us/my/1123433' | %w[https://gitlab.zoom.us/my/1123433] + end + + with_them do + subject { described_class.new(text).links } + + it { is_expected.to eq(links) } + end + end +end diff --git a/spec/migrations/fix_wrong_pages_access_level_spec.rb b/spec/migrations/fix_wrong_pages_access_level_spec.rb new file mode 100644 index 00000000000..75ac5d919b2 --- /dev/null +++ b/spec/migrations/fix_wrong_pages_access_level_spec.rb @@ -0,0 +1,97 @@ +require 'spec_helper' +require Rails.root.join('db', 'post_migrate', '20190703185326_fix_wrong_pages_access_level.rb') + +describe FixWrongPagesAccessLevel, :migration, :sidekiq, schema: 20190628185004 do + using RSpec::Parameterized::TableSyntax + + let(:migration_class) { described_class::MIGRATION } + let(:migration_name) { migration_class.to_s.demodulize } + + project_class = ::Gitlab::BackgroundMigration::FixPagesAccessLevel::Project + feature_class = ::Gitlab::BackgroundMigration::FixPagesAccessLevel::ProjectFeature + + let(:namespaces_table) { table(:namespaces) } + let(:projects_table) { table(:projects) } + let(:features_table) { table(:project_features) } + + let(:subgroup) do + root_group = namespaces_table.create(path: "group", name: "group") + namespaces_table.create!(path: "subgroup", name: "group", parent_id: root_group.id) + end + + def create_project_feature(path, project_visibility, pages_access_level) + project = projects_table.create!(path: path, visibility_level: project_visibility, + namespace_id: subgroup.id) + features_table.create!(project_id: project.id, pages_access_level: pages_access_level) + end + + it 'correctly schedules background migrations' do + Sidekiq::Testing.fake! do + Timecop.freeze do + first_id = create_project_feature("project1", project_class::PRIVATE, feature_class::PRIVATE).id + last_id = create_project_feature("project2", project_class::PRIVATE, feature_class::PUBLIC).id + + migrate! + + expect(migration_name).to be_scheduled_delayed_migration(2.minutes, first_id, last_id) + expect(BackgroundMigrationWorker.jobs.size).to eq(1) + end + end + end + + def expect_migration + expect do + perform_enqueued_jobs do + migrate! + end + end + end + + where(:project_visibility, :pages_access_level, :access_control_is_enabled, + :pages_deployed, :resulting_pages_access_level) do + # update settings for public projects regardless of access_control being enabled + project_class::PUBLIC | feature_class::PUBLIC | true | true | feature_class::ENABLED + project_class::PUBLIC | feature_class::PUBLIC | false | true | feature_class::ENABLED + # don't update public level for private and internal projects + project_class::PRIVATE | feature_class::PUBLIC | true | true | feature_class::PUBLIC + project_class::INTERNAL | feature_class::PUBLIC | true | true | feature_class::PUBLIC + + # if access control is disabled but pages are deployed we make them public + project_class::INTERNAL | feature_class::ENABLED | false | true | feature_class::PUBLIC + # don't change anything if one of the conditions is not satisfied + project_class::INTERNAL | feature_class::ENABLED | true | true | feature_class::ENABLED + project_class::INTERNAL | feature_class::ENABLED | true | false | feature_class::ENABLED + + # private projects + # if access control is enabled update pages_access_level to private regardless of deployment + project_class::PRIVATE | feature_class::ENABLED | true | true | feature_class::PRIVATE + project_class::PRIVATE | feature_class::ENABLED | true | false | feature_class::PRIVATE + # if access control is disabled and pages are deployed update pages_access_level to public + project_class::PRIVATE | feature_class::ENABLED | false | true | feature_class::PUBLIC + # if access control is disabled but pages aren't deployed update pages_access_level to private + project_class::PRIVATE | feature_class::ENABLED | false | false | feature_class::PRIVATE + end + + with_them do + let!(:project_feature) do + create_project_feature("projectpath", project_visibility, pages_access_level) + end + + before do + tested_path = File.join(Settings.pages.path, "group/subgroup/projectpath", "public") + allow(Dir).to receive(:exist?).with(tested_path).and_return(pages_deployed) + + stub_pages_setting(access_control: access_control_is_enabled) + end + + it "sets proper pages_access_level" do + expect(project_feature.reload.pages_access_level).to eq(pages_access_level) + + perform_enqueued_jobs do + migrate! + end + + expect(project_feature.reload.pages_access_level).to eq(resulting_pages_access_level) + end + end +end diff --git a/spec/models/active_session_spec.rb b/spec/models/active_session_spec.rb index 2762eaeccd3..09c2878663a 100644 --- a/spec/models/active_session_spec.rb +++ b/spec/models/active_session_spec.rb @@ -132,6 +132,19 @@ RSpec.describe ActiveSession, :clean_gitlab_redis_shared_state do expect(ActiveSession.sessions_from_ids([])).to eq([]) end + + it 'uses redis lookup in batches' do + stub_const('ActiveSession::SESSION_BATCH_SIZE', 1) + + redis = double(:redis) + expect(Gitlab::Redis::SharedState).to receive(:with).and_yield(redis) + + sessions = ['session-a', 'session-b'] + mget_responses = sessions.map { |session| [Marshal.dump(session)]} + expect(redis).to receive(:mget).twice.and_return(*mget_responses) + + expect(ActiveSession.sessions_from_ids([1, 2])).to eql(sessions) + end end describe '.set' do diff --git a/spec/models/ci/trigger_spec.rb b/spec/models/ci/trigger_spec.rb index fde8375f2a5..5b5d6f51b33 100644 --- a/spec/models/ci/trigger_spec.rb +++ b/spec/models/ci/trigger_spec.rb @@ -54,19 +54,31 @@ describe Ci::Trigger do end describe '#can_access_project?' do + let(:owner) { create(:user) } let(:trigger) { create(:ci_trigger, owner: owner, project: project) } context 'when owner is blank' do - let(:owner) { nil } + before do + stub_feature_flags(use_legacy_pipeline_triggers: false) + trigger.update_attribute(:owner, nil) + end subject { trigger.can_access_project? } - it { is_expected.to eq(true) } + it { is_expected.to eq(false) } + + context 'when :use_legacy_pipeline_triggers feature flag is enabled' do + before do + stub_feature_flags(use_legacy_pipeline_triggers: true) + end + + subject { trigger.can_access_project? } + + it { is_expected.to eq(true) } + end end context 'when owner is set' do - let(:owner) { create(:user) } - subject { trigger.can_access_project? } context 'and is member of the project' do diff --git a/spec/models/clusters/applications/runner_spec.rb b/spec/models/clusters/applications/runner_spec.rb index 4f0cd0efe9c..4abe45a2152 100644 --- a/spec/models/clusters/applications/runner_spec.rb +++ b/spec/models/clusters/applications/runner_spec.rb @@ -18,7 +18,7 @@ describe Clusters::Applications::Runner do subject { gitlab_runner.can_uninstall? } - it { is_expected.to be_falsey } + it { is_expected.to be_truthy } end describe '#install_command' do @@ -156,4 +156,35 @@ describe Clusters::Applications::Runner do end end end + + describe '#prepare_uninstall' do + it 'pauses associated runner' do + active_runner = create(:ci_runner, contacted_at: 1.second.ago) + + expect(active_runner.status).to eq(:online) + + application_runner = create(:clusters_applications_runner, :scheduled, runner: active_runner) + application_runner.prepare_uninstall + + expect(active_runner.status).to eq(:paused) + end + end + + describe '#make_uninstalling!' do + subject { create(:clusters_applications_runner, :scheduled, runner: ci_runner) } + + it 'calls prepare_uninstall' do + expect_any_instance_of(described_class).to receive(:prepare_uninstall).and_call_original + + subject.make_uninstalling! + end + end + + describe '#post_uninstall' do + it 'destroys its runner' do + application_runner = create(:clusters_applications_runner, :scheduled, runner: ci_runner) + + expect { application_runner.post_uninstall }.to change { Ci::Runner.count }.by(-1) + end + end end diff --git a/spec/models/concerns/deployment_platform_spec.rb b/spec/models/concerns/deployment_platform_spec.rb index e2fc8a5d127..2378f400540 100644 --- a/spec/models/concerns/deployment_platform_spec.rb +++ b/spec/models/concerns/deployment_platform_spec.rb @@ -5,7 +5,7 @@ require 'rails_helper' describe DeploymentPlatform do let(:project) { create(:project) } - shared_examples '#deployment_platform' do + describe '#deployment_platform' do subject { project.deployment_platform } context 'with no Kubernetes configuration on CI/CD, no Kubernetes Service' do @@ -84,20 +84,4 @@ describe DeploymentPlatform do end end end - - context 'legacy implementation' do - before do - stub_feature_flags(clusters_cte: false) - end - - include_examples '#deployment_platform' - end - - context 'CTE implementation' do - before do - stub_feature_flags(clusters_cte: true) - end - - include_examples '#deployment_platform' - end end diff --git a/spec/models/concerns/stepable_spec.rb b/spec/models/concerns/stepable_spec.rb new file mode 100644 index 00000000000..5685de6a9bf --- /dev/null +++ b/spec/models/concerns/stepable_spec.rb @@ -0,0 +1,111 @@ +# frozen_string_literal: true + +require 'spec_helper' + +describe Stepable do + let(:described_class) do + Class.new do + include Stepable + + steps :method1, :method2, :method3 + + def execute + execute_steps + end + + private + + def method1 + { status: :success } + end + + def method2 + return { status: :error } unless @pass + + { status: :success, variable1: 'var1' } + end + + def method3 + { status: :success, variable2: 'var2' } + end + end + end + + let(:prepended_module) do + Module.new do + extend ActiveSupport::Concern + + prepended do + steps :appended_method1 + end + + private + + def appended_method1 + { status: :success } + end + end + end + + before do + described_class.prepend(prepended_module) + end + + it 'stops after the first error' do + expect(subject).not_to receive(:method3) + expect(subject).not_to receive(:appended_method1) + + expect(subject.execute).to eq( + status: :error, + failed_step: :method2 + ) + end + + context 'when all methods return success' do + before do + subject.instance_variable_set(:@pass, true) + end + + it 'calls all methods in order' do + expect(subject).to receive(:method1).and_call_original.ordered + expect(subject).to receive(:method2).and_call_original.ordered + expect(subject).to receive(:method3).and_call_original.ordered + expect(subject).to receive(:appended_method1).and_call_original.ordered + + subject.execute + end + + it 'merges variables returned by all steps' do + expect(subject.execute).to eq( + status: :success, + variable1: 'var1', + variable2: 'var2' + ) + end + end + + context 'with multiple stepable classes' do + let(:other_class) do + Class.new do + include Stepable + + steps :other_method1, :other_method2 + + private + + def other_method1 + { status: :success } + end + + def other_method2 + { status: :success } + end + end + end + + it 'does not leak steps' do + expect(other_class.new.steps).to contain_exactly(:other_method1, :other_method2) + expect(subject.steps).to contain_exactly(:method1, :method2, :method3, :appended_method1) + end + end +end diff --git a/spec/models/project_feature_spec.rb b/spec/models/project_feature_spec.rb index 50c9d5968ac..31e55bf6be6 100644 --- a/spec/models/project_feature_spec.rb +++ b/spec/models/project_feature_spec.rb @@ -150,4 +150,32 @@ describe ProjectFeature do end end end + + describe 'default pages access level' do + subject { project.project_feature.pages_access_level } + + before do + # project factory overrides all values in project_feature after creation + project.project_feature.destroy! + project.build_project_feature.save! + end + + context 'when new project is private' do + let(:project) { create(:project, :private) } + + it { is_expected.to eq(ProjectFeature::PRIVATE) } + end + + context 'when new project is internal' do + let(:project) { create(:project, :internal) } + + it { is_expected.to eq(ProjectFeature::PRIVATE) } + end + + context 'when new project is public' do + let(:project) { create(:project, :public) } + + it { is_expected.to eq(ProjectFeature::ENABLED) } + end + end end diff --git a/spec/policies/ci/trigger_policy_spec.rb b/spec/policies/ci/trigger_policy_spec.rb index d8a63066265..e9a85890082 100644 --- a/spec/policies/ci/trigger_policy_spec.rb +++ b/spec/policies/ci/trigger_policy_spec.rb @@ -3,52 +3,24 @@ require 'spec_helper' describe Ci::TriggerPolicy do let(:user) { create(:user) } let(:project) { create(:project) } - let(:trigger) { create(:ci_trigger, project: project, owner: owner) } + let(:trigger) { create(:ci_trigger, project: project, owner: create(:user)) } - let(:policies) do - described_class.new(user, trigger) - end - - shared_examples 'allows to admin and manage trigger' do - it 'does include ability to admin trigger' do - expect(policies).to be_allowed :admin_trigger - end - - it 'does include ability to manage trigger' do - expect(policies).to be_allowed :manage_trigger - end - end - - shared_examples 'allows to manage trigger' do - it 'does not include ability to admin trigger' do - expect(policies).not_to be_allowed :admin_trigger - end - - it 'does include ability to manage trigger' do - expect(policies).to be_allowed :manage_trigger - end - end - - shared_examples 'disallows to admin and manage trigger' do - it 'does not include ability to admin trigger' do - expect(policies).not_to be_allowed :admin_trigger - end - - it 'does not include ability to manage trigger' do - expect(policies).not_to be_allowed :manage_trigger - end - end + subject { described_class.new(user, trigger) } describe '#rules' do context 'when owner is undefined' do - let(:owner) { nil } + before do + stub_feature_flags(use_legacy_pipeline_triggers: false) + trigger.update_attribute(:owner, nil) + end context 'when user is maintainer of the project' do before do project.add_maintainer(user) end - it_behaves_like 'allows to admin and manage trigger' + it { is_expected.to be_allowed(:manage_trigger) } + it { is_expected.not_to be_allowed(:admin_trigger) } end context 'when user is developer of the project' do @@ -56,35 +28,63 @@ describe Ci::TriggerPolicy do project.add_developer(user) end - it_behaves_like 'disallows to admin and manage trigger' + it { is_expected.not_to be_allowed(:manage_trigger) } + it { is_expected.not_to be_allowed(:admin_trigger) } end - context 'when user is not member of the project' do - it_behaves_like 'disallows to admin and manage trigger' + context 'when :use_legacy_pipeline_triggers feature flag is enabled' do + before do + stub_feature_flags(use_legacy_pipeline_triggers: true) + end + + context 'when user is maintainer of the project' do + before do + project.add_maintainer(user) + end + + it { is_expected.to be_allowed(:manage_trigger) } + it { is_expected.to be_allowed(:admin_trigger) } + end + + context 'when user is developer of the project' do + before do + project.add_developer(user) + end + + it { is_expected.not_to be_allowed(:manage_trigger) } + it { is_expected.not_to be_allowed(:admin_trigger) } + end + + context 'when user is not member of the project' do + it { is_expected.not_to be_allowed(:manage_trigger) } + it { is_expected.not_to be_allowed(:admin_trigger) } + end end end context 'when owner is an user' do - let(:owner) { user } + before do + trigger.update!(owner: user) + end context 'when user is maintainer of the project' do before do project.add_maintainer(user) end - it_behaves_like 'allows to admin and manage trigger' + it { is_expected.to be_allowed(:manage_trigger) } + it { is_expected.to be_allowed(:admin_trigger) } end end context 'when owner is another user' do - let(:owner) { create(:user) } - context 'when user is maintainer of the project' do before do project.add_maintainer(user) end - it_behaves_like 'allows to manage trigger' + it { is_expected.to be_allowed(:manage_trigger) } + it { is_expected.not_to be_allowed(:admin_trigger) } end context 'when user is developer of the project' do @@ -92,11 +92,13 @@ describe Ci::TriggerPolicy do project.add_developer(user) end - it_behaves_like 'disallows to admin and manage trigger' + it { is_expected.not_to be_allowed(:manage_trigger) } + it { is_expected.not_to be_allowed(:admin_trigger) } end context 'when user is not member of the project' do - it_behaves_like 'disallows to admin and manage trigger' + it { is_expected.not_to be_allowed(:manage_trigger) } + it { is_expected.not_to be_allowed(:admin_trigger) } end end end diff --git a/spec/requests/api/commits_spec.rb b/spec/requests/api/commits_spec.rb index 3df5d9412f8..204e378f7be 100644 --- a/spec/requests/api/commits_spec.rb +++ b/spec/requests/api/commits_spec.rb @@ -281,7 +281,7 @@ describe API::Commits do end it 'does not increment the usage counters using access token authentication' do - expect(::Gitlab::WebIdeCommitsCounter).not_to receive(:increment) + expect(::Gitlab::UsageDataCounters::WebIdeCommitsCounter).not_to receive(:increment) post api(url, user), params: valid_c_params end diff --git a/spec/requests/api/projects_spec.rb b/spec/requests/api/projects_spec.rb index a2aae257352..fee300e9d7a 100644 --- a/spec/requests/api/projects_spec.rb +++ b/spec/requests/api/projects_spec.rb @@ -46,8 +46,6 @@ shared_examples 'languages and percentages JSON response' do end describe API::Projects do - include ExternalAuthorizationServiceHelpers - let(:user) { create(:user) } let(:user2) { create(:user) } let(:user3) { create(:user) } @@ -1425,39 +1423,6 @@ describe API::Projects do end end end - - context 'with external authorization' do - let(:project) do - create(:project, - namespace: user.namespace, - external_authorization_classification_label: 'the-label') - end - - context 'when the user has access to the project' do - before do - external_service_allow_access(user, project) - end - - it 'includes the label in the response' do - get api("/projects/#{project.id}", user) - - expect(response).to have_gitlab_http_status(200) - expect(json_response['external_authorization_classification_label']).to eq('the-label') - end - end - - context 'when the external service denies access' do - before do - external_service_deny_access(user, project) - end - - it 'returns a 404' do - get api("/projects/#{project.id}", user) - - expect(response).to have_gitlab_http_status(404) - end - end - end end describe 'GET /projects/:id/users' do @@ -2061,20 +2026,6 @@ describe API::Projects do expect(response).to have_gitlab_http_status(403) end end - - context 'when updating external classification' do - before do - enable_external_authorization_service_check - end - - it 'updates the classification label' do - put(api("/projects/#{project.id}", user), params: { external_authorization_classification_label: 'new label' }) - - expect(response).to have_gitlab_http_status(200) - - expect(project.reload.external_authorization_classification_label).to eq('new label') - end - end end describe 'POST /projects/:id/archive' do diff --git a/spec/requests/api/search_spec.rb b/spec/requests/api/search_spec.rb index 3e0b478abb3..8abdcaa2e0e 100644 --- a/spec/requests/api/search_spec.rb +++ b/spec/requests/api/search_spec.rb @@ -89,7 +89,7 @@ describe API::Search do it 'returns empty array' do get api('/search', user), params: { scope: 'milestones', search: 'awesome' } - milestones = JSON.parse(response.body) + milestones = json_response expect(milestones).to be_empty end @@ -356,7 +356,7 @@ describe API::Search do it 'returns empty array' do get api("/projects/#{project.id}/search", user), params: { scope: 'milestones', search: 'awesome' } - milestones = JSON.parse(response.body) + milestones = json_response expect(milestones).to be_empty end diff --git a/spec/requests/api/services_spec.rb b/spec/requests/api/services_spec.rb index 3f79e332b90..91cb8760a04 100644 --- a/spec/requests/api/services_spec.rb +++ b/spec/requests/api/services_spec.rb @@ -85,9 +85,7 @@ describe API::Services do include_context service # inject some properties into the service - before do - initialize_service(service) - end + let!(:initialized_service) { initialize_service(service) } it 'returns authentication error when unauthenticated' do get api("/projects/#{project.id}/services/#{dashed_service}") @@ -108,6 +106,15 @@ describe API::Services do expect(json_response['properties'].keys).to match_array(service_instance.api_field_names) end + it "returns empty hash if properties are empty" do + # deprecated services are not valid for update + initialized_service.update_attribute(:properties, {}) + get api("/projects/#{project.id}/services/#{dashed_service}", user) + + expect(response).to have_gitlab_http_status(200) + expect(json_response['properties'].keys).to be_empty + end + it "returns error when authenticated but not a project owner" do project.add_developer(user2) get api("/projects/#{project.id}/services/#{dashed_service}", user2) diff --git a/spec/requests/api/users_spec.rb b/spec/requests/api/users_spec.rb index 0ad50e5347a..af2bee4563a 100644 --- a/spec/requests/api/users_spec.rb +++ b/spec/requests/api/users_spec.rb @@ -448,6 +448,7 @@ describe API::Users do it "returns 201 Created on success" do post api("/users", admin), params: attributes_for(:user, projects_limit: 3) + expect(response).to match_response_schema('public_api/v4/user/admin') expect(response).to have_gitlab_http_status(201) end @@ -643,6 +644,13 @@ describe API::Users do describe "PUT /users/:id" do let!(:admin_user) { create(:admin) } + it "returns 200 OK on success" do + put api("/users/#{user.id}", admin), params: { bio: 'new test bio' } + + expect(response).to match_response_schema('public_api/v4/user/admin') + expect(response).to have_gitlab_http_status(200) + end + it "updates user with new bio" do put api("/users/#{user.id}", admin), params: { bio: 'new test bio' } diff --git a/spec/requests/lfs_http_spec.rb b/spec/requests/lfs_http_spec.rb index 1781759c54b..dc25e4d808e 100644 --- a/spec/requests/lfs_http_spec.rb +++ b/spec/requests/lfs_http_spec.rb @@ -1439,8 +1439,4 @@ describe 'Git LFS API and storage' do post(url, params: params, headers: headers) end - - def json_response - @json_response ||= JSON.parse(response.body) - end end diff --git a/spec/requests/lfs_locks_api_spec.rb b/spec/requests/lfs_locks_api_spec.rb index 5b7b3d2fdd6..11436e5cd0c 100644 --- a/spec/requests/lfs_locks_api_spec.rb +++ b/spec/requests/lfs_locks_api_spec.rb @@ -163,8 +163,4 @@ describe 'Git LFS File Locking API' do def do_get(url, params = nil, headers = nil) get(url, params: (params || {}), headers: (headers || {}).merge('Content-Type' => LfsRequest::CONTENT_TYPE)) end - - def json_response - @json_response ||= JSON.parse(response.body) - end end diff --git a/spec/serializers/diff_file_base_entity_spec.rb b/spec/serializers/diff_file_base_entity_spec.rb new file mode 100644 index 00000000000..68c5c665ed6 --- /dev/null +++ b/spec/serializers/diff_file_base_entity_spec.rb @@ -0,0 +1,26 @@ +# frozen_string_literal: true + +require 'spec_helper' + +describe DiffFileBaseEntity do + let(:project) { create(:project, :repository) } + let(:repository) { project.repository } + + context 'diff for a changed submodule' do + let(:commit_sha_with_changed_submodule) do + "cfe32cf61b73a0d5e9f13e774abde7ff789b1660" + end + let(:commit) { project.commit(commit_sha_with_changed_submodule) } + let(:diff_file) { commit.diffs.diff_files.to_a.last } + let(:options) { { request: {}, submodule_links: Gitlab::SubmoduleLinks.new(repository) } } + let(:entity) { described_class.new(diff_file, options).as_json } + + it do + expect(entity[:submodule]).to eq(true) + expect(entity[:submodule_link]).to eq("https://github.com/randx/six") + expect(entity[:submodule_tree_url]).to eq( + "https://github.com/randx/six/tree/409f37c4f05865e4fb208c771485f211a22c4c2d" + ) + end + end +end diff --git a/spec/services/boards/issues/move_service_spec.rb b/spec/services/boards/issues/move_service_spec.rb index 1bfb5602df2..cf84ec8fd4c 100644 --- a/spec/services/boards/issues/move_service_spec.rb +++ b/spec/services/boards/issues/move_service_spec.rb @@ -68,8 +68,8 @@ describe Boards::Issues::MoveService do project.add_developer(user) end - it 'returns false if list of issues is empty' do - expect(described_class.new(group, user, params).execute_multiple([])).to eq(false) + it 'returns the expected result if list of issues is empty' do + expect(described_class.new(group, user, params).execute_multiple([])).to eq({ count: 0, success: false, issues: [] }) end context 'moving multiple issues' do diff --git a/spec/services/clusters/applications/check_uninstall_progress_service_spec.rb b/spec/services/clusters/applications/check_uninstall_progress_service_spec.rb index 9ab83d913f5..a948b442441 100644 --- a/spec/services/clusters/applications/check_uninstall_progress_service_spec.rb +++ b/spec/services/clusters/applications/check_uninstall_progress_service_spec.rb @@ -41,7 +41,7 @@ describe Clusters::Applications::CheckUninstallProgressService do end end - context 'when application is installing' do + context 'when application is uninstalling' do RESCHEDULE_PHASES.each { |phase| it_behaves_like 'a not yet terminated installation', phase } context 'when installation POD succeeded' do @@ -56,6 +56,12 @@ describe Clusters::Applications::CheckUninstallProgressService do service.execute end + it 'runs application post_uninstall' do + expect(application).to receive(:post_uninstall).and_call_original + + service.execute + end + it 'destroys the application' do expect(worker_class).not_to receive(:perform_in) diff --git a/spec/services/projects/update_service_spec.rb b/spec/services/projects/update_service_spec.rb index 1dcfb739eb6..6bbaa410d56 100644 --- a/spec/services/projects/update_service_spec.rb +++ b/spec/services/projects/update_service_spec.rb @@ -347,13 +347,13 @@ describe Projects::UpdateService do context 'when updating #pages_access_level' do subject(:call_service) do - update_project(project, admin, project_feature_attributes: { pages_access_level: ProjectFeature::PRIVATE }) + update_project(project, admin, project_feature_attributes: { pages_access_level: ProjectFeature::ENABLED }) end it 'updates the attribute' do expect { call_service } .to change { project.project_feature.pages_access_level } - .to(ProjectFeature::PRIVATE) + .to(ProjectFeature::ENABLED) end it 'calls Projects::UpdatePagesConfigurationService' do diff --git a/spec/services/self_monitoring/project/create_service_spec.rb b/spec/services/self_monitoring/project/create_service_spec.rb new file mode 100644 index 00000000000..d11e27c6d52 --- /dev/null +++ b/spec/services/self_monitoring/project/create_service_spec.rb @@ -0,0 +1,201 @@ +# frozen_string_literal: true + +require 'spec_helper' + +describe SelfMonitoring::Project::CreateService do + describe '#execute' do + let(:result) { subject.execute } + + let(:prometheus_settings) do + OpenStruct.new( + enable: true, + listen_address: 'localhost:9090' + ) + end + + before do + allow(Gitlab.config).to receive(:prometheus).and_return(prometheus_settings) + end + + context 'without admin users' do + it 'returns error' do + expect(subject).to receive(:log_error).and_call_original + expect(result).to eq( + status: :error, + message: 'No active admin user found', + failed_step: :validate_admins + ) + end + end + + context 'with admin users' do + let(:project) { result[:project] } + + let!(:user) { create(:user, :admin) } + + before do + allow(ApplicationSetting) + .to receive(:current) + .and_return( + ApplicationSetting.build_from_defaults(allow_local_requests_from_hooks_and_services: true) + ) + end + + shared_examples 'has prometheus service' do |listen_address| + it do + expect(result[:status]).to eq(:success) + + prometheus = project.prometheus_service + expect(prometheus).not_to eq(nil) + expect(prometheus.api_url).to eq(listen_address) + expect(prometheus.active).to eq(true) + expect(prometheus.manual_configuration).to eq(true) + end + end + + it_behaves_like 'has prometheus service', 'http://localhost:9090' + + it 'creates project with internal visibility' do + expect(result[:status]).to eq(:success) + expect(project.visibility_level).to eq(Gitlab::VisibilityLevel::INTERNAL) + expect(project).to be_persisted + end + + it 'creates project with internal visibility even when internal visibility is restricted' do + stub_application_setting(restricted_visibility_levels: [Gitlab::VisibilityLevel::INTERNAL]) + + expect(result[:status]).to eq(:success) + expect(project.visibility_level).to eq(Gitlab::VisibilityLevel::INTERNAL) + expect(project).to be_persisted + end + + it 'creates project with correct name and description' do + expect(result[:status]).to eq(:success) + expect(project.name).to eq(described_class::DEFAULT_NAME) + expect(project.description).to eq(described_class::DEFAULT_DESCRIPTION) + end + + it 'adds all admins as maintainers' do + admin1 = create(:user, :admin) + admin2 = create(:user, :admin) + create(:user) + + expect(result[:status]).to eq(:success) + expect(project.owner).to eq(user) + expect(project.members.collect(&:user)).to contain_exactly(user, admin1, admin2) + expect(project.members.collect(&:access_level)).to contain_exactly( + Gitlab::Access::MAINTAINER, + Gitlab::Access::MAINTAINER, + Gitlab::Access::MAINTAINER + ) + end + + # This should pass when https://gitlab.com/gitlab-org/gitlab-ce/issues/44496 + # is complete and the prometheus listen address is added to the whitelist. + # context 'when local requests from hooks and services are not allowed' do + # before do + # allow(ApplicationSetting) + # .to receive(:current) + # .and_return( + # ApplicationSetting.build_from_defaults(allow_local_requests_from_hooks_and_services: false) + # ) + # end + + # it_behaves_like 'has prometheus service', 'http://localhost:9090' + # end + + context 'with non default prometheus address' do + before do + prometheus_settings.listen_address = 'https://localhost:9090' + end + + it_behaves_like 'has prometheus service', 'https://localhost:9090' + end + + context 'when prometheus setting is not present in gitlab.yml' do + before do + allow(Gitlab.config).to receive(:prometheus).and_raise(Settingslogic::MissingSetting) + end + + it 'does not fail' do + expect(result).to include(status: :success) + expect(project.prometheus_service).to be_nil + end + end + + context 'when prometheus setting is disabled in gitlab.yml' do + before do + prometheus_settings.enable = false + end + + it 'does not configure prometheus' do + expect(result).to include(status: :success) + expect(project.prometheus_service).to be_nil + end + end + + context 'when prometheus listen address is blank in gitlab.yml' do + before do + prometheus_settings.listen_address = '' + end + + it 'does not configure prometheus' do + expect(result).to include(status: :success) + expect(project.prometheus_service).to be_nil + end + end + + context 'when project cannot be created' do + let(:project) { build(:project) } + + before do + project.errors.add(:base, "Test error") + + expect_next_instance_of(::Projects::CreateService) do |project_create_service| + expect(project_create_service).to receive(:execute) + .and_return(project) + end + end + + it 'returns error' do + expect(subject).to receive(:log_error).and_call_original + expect(result).to eq({ + status: :error, + message: 'Could not create project', + failed_step: :create_project + }) + end + end + + context 'when user cannot be added to project' do + before do + subject.instance_variable_set(:@instance_admins, [user, build(:user, :admin)]) + end + + it 'returns error' do + expect(subject).to receive(:log_error).and_call_original + expect(result).to eq({ + status: :error, + message: 'Could not add admins as members', + failed_step: :add_project_members + }) + end + end + + context 'when prometheus manual configuration cannot be saved' do + before do + prometheus_settings.listen_address = 'httpinvalid://localhost:9090' + end + + it 'returns error' do + expect(subject).to receive(:log_error).and_call_original + expect(result).to eq( + status: :error, + message: 'Could not save prometheus manual configuration', + failed_step: :add_prometheus_manual_configuration + ) + end + end + end + end +end diff --git a/spec/support/features/rss_shared_examples.rb b/spec/support/features/rss_shared_examples.rb index 02d310a9afa..0de92aedba5 100644 --- a/spec/support/features/rss_shared_examples.rb +++ b/spec/support/features/rss_shared_examples.rb @@ -6,7 +6,7 @@ end shared_examples "it has an RSS button with current_user's feed token" do it "shows the RSS button with current_user's feed token" do - expect(page).to have_css("a:has(.fa-rss)[href*='feed_token=#{user.feed_token}'], .js-rss-button[href*='feed_token=#{user.feed_token}']") + expect(page).to have_css("a:has(.fa-rss)[href*='feed_token=#{user.feed_token}']") end end @@ -18,6 +18,6 @@ end shared_examples "it has an RSS button without a feed token" do it "shows the RSS button without a feed token" do - expect(page).to have_css("a:has(.fa-rss):not([href*='feed_token']), .js-rss-button:not([href*='feed_token'])") + expect(page).to have_css("a:has(.fa-rss):not([href*='feed_token'])") end end diff --git a/spec/support/helpers/stub_configuration.rb b/spec/support/helpers/stub_configuration.rb index c372a3f0e49..049702be1f6 100644 --- a/spec/support/helpers/stub_configuration.rb +++ b/spec/support/helpers/stub_configuration.rb @@ -65,6 +65,10 @@ module StubConfiguration allow(Gitlab.config.artifacts).to receive_messages(to_settings(messages)) end + def stub_pages_setting(messages) + allow(Gitlab.config.pages).to receive_messages(to_settings(messages)) + end + def stub_storage_settings(messages) messages.deep_stringify_keys! diff --git a/spec/support/json_response.rb b/spec/support/json_response.rb index 210b0e6d867..43d8ab73dde 100644 --- a/spec/support/json_response.rb +++ b/spec/support/json_response.rb @@ -1,5 +1,5 @@ RSpec.configure do |config| - config.include_context 'JSON response' + config.include_context 'JSON response', type: :controller config.include_context 'JSON response', type: :request config.include_context 'JSON response', :api end diff --git a/spec/support/shared_examples/controllers/issuable_notes_filter_shared_examples.rb b/spec/support/shared_examples/controllers/issuable_notes_filter_shared_examples.rb index 0acc9e2a836..f4b02dc5350 100644 --- a/spec/support/shared_examples/controllers/issuable_notes_filter_shared_examples.rb +++ b/spec/support/shared_examples/controllers/issuable_notes_filter_shared_examples.rb @@ -46,7 +46,7 @@ shared_examples 'issuable notes filter' do user.set_notes_filter(UserPreference::NOTES_FILTERS[:only_comments], issuable) get :discussions, params: params - discussions = JSON.parse(response.body) + discussions = json_response expect(discussions.count).to eq(1) expect(discussions.first["notes"].first["system"]).to be(false) @@ -56,7 +56,7 @@ shared_examples 'issuable notes filter' do user.set_notes_filter(UserPreference::NOTES_FILTERS[:only_activity], issuable) get :discussions, params: params - discussions = JSON.parse(response.body) + discussions = json_response expect(discussions.count).to eq(1) expect(discussions.first["notes"].first["system"]).to be(true) diff --git a/spec/support/shared_examples/update_invalid_issuable.rb b/spec/support/shared_examples/update_invalid_issuable.rb index 64568de424e..4cb6d001b9b 100644 --- a/spec/support/shared_examples/update_invalid_issuable.rb +++ b/spec/support/shared_examples/update_invalid_issuable.rb @@ -38,7 +38,7 @@ shared_examples 'update invalid issuable' do |klass| put :update, params: params expect(response.status).to eq(409) - expect(JSON.parse(response.body)).to have_key('errors') + expect(json_response).to have_key('errors') end end diff --git a/yarn.lock b/yarn.lock index eaa029281d6..a0fbdfd4541 100644 --- a/yarn.lock +++ b/yarn.lock @@ -4919,6 +4919,11 @@ glob-to-regexp@^0.3.0: resolved "https://registry.yarnpkg.com/glob-to-regexp/-/glob-to-regexp-0.3.0.tgz#8c5a1494d2066c570cc3bfe4496175acc4d502ab" integrity sha1-jFoUlNIGbFcMw7/kSWF1rMTVAqs= +glob-to-regexp@^0.4.0: + version "0.4.1" + resolved "https://registry.yarnpkg.com/glob-to-regexp/-/glob-to-regexp-0.4.1.tgz#c75297087c851b9a578bd217dd59a92f59fe546e" + integrity sha512-lkX1HJXwyMcprw/5YUZc2s7DrpAiHB21/V+E1rHUrVNokkvB6bqMzT0VfV6/86ZNabt1k14YOIaT7nDvOX3Iiw== + "glob@5 - 7", glob@^7.0.0, glob@^7.0.3, glob@^7.1.1, glob@^7.1.2, glob@^7.1.3, glob@~7.1.1: version "7.1.4" resolved "https://registry.yarnpkg.com/glob/-/glob-7.1.4.tgz#aa608a2f6c577ad357e1ae5a5c26d9a8d1969255" @@ -9099,6 +9104,14 @@ readable-stream@~2.0.6: string_decoder "~0.10.x" util-deprecate "~1.0.1" +readdir-enhanced@^2.2.4: + version "2.2.4" + resolved "https://registry.yarnpkg.com/readdir-enhanced/-/readdir-enhanced-2.2.4.tgz#773fb8a8de5f645fb13d9403746d490d4facb3e6" + integrity sha512-JQD83C9gAs5B5j2j40qLn/K83HhR8po3bUonebNeuJQUZbbn7q1HxL9kQuPBtxoXkaUpbtEmpFBw5kzyYnnJDA== + dependencies: + call-me-maybe "^1.0.1" + glob-to-regexp "^0.4.0" + readdirp@^2.0.0: version "2.1.0" resolved "https://registry.yarnpkg.com/readdirp/-/readdirp-2.1.0.tgz#4ed0ad060df3073300c48440373f72d1cc642d78" |