diff options
781 files changed, 11086 insertions, 6600 deletions
diff --git a/.gitattributes b/.gitattributes index f1c41c9bb76..7282c9e61b1 100644 --- a/.gitattributes +++ b/.gitattributes @@ -1 +1,2 @@ Dangerfile gitlab-language=ruby +db/schema.rb merge=merge_db_schema diff --git a/.gitlab-ci.yml b/.gitlab-ci.yml index 0d70eae0d1e..0e7a67f9cc1 100644 --- a/.gitlab-ci.yml +++ b/.gitlab-ci.yml @@ -694,7 +694,10 @@ gitlab:setup-mysql: # Frontend-related jobs gitlab:assets:compile: <<: *dedicated-no-docs-and-no-qa-pull-cache-job + image: dev.gitlab.org:5005/gitlab/gitlab-build-images:ruby-2.4.4-git-2.18-chrome-69.0-node-8.x-yarn-1.2-graphicsmagick-1.3.29-docker-18.06.1 dependencies: [] + services: + - docker:stable-dind variables: NODE_ENV: "production" RAILS_ENV: "production" @@ -703,18 +706,23 @@ gitlab:assets:compile: WEBPACK_REPORT: "true" # we override the max_old_space_size to prevent OOM errors NODE_OPTIONS: --max_old_space_size=3584 + DOCKER_DRIVER: overlay2 + DOCKER_HOST: tcp://docker:2375 script: - date - yarn install --frozen-lockfile --production --cache-folder .yarn-cache - date - free -m - bundle exec rake gitlab:assets:compile + - scripts/build_assets_image artifacts: name: webpack-report expire_in: 31d paths: - webpack-report/ - public/assets/ + tags: + - docker karma: <<: *dedicated-no-docs-pull-cache-job diff --git a/CHANGELOG.md b/CHANGELOG.md index 4c99f6ed059..6e5296e231e 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,27 @@ documentation](doc/development/changelog.md) for instructions on adding your own entry. +## 11.4.5 (2018-11-04) + +### Fixed (4 changes, 1 of them is from the community) + +- fix link to enable usage ping from convdev index. !22545 (Anand Capur) +- Update gitlab-ui dependency to 1.8.0-hotfix.1 to fix IE11 bug. +- Remove duplicate escape in job sidebar. +- Fixed merge request fill tree toggling not respecting fluid width preference. + +### Other (1 change) + +- Fix stage dropdown not rendering in different languages. + + +## 11.4.4 (2018-10-30) + +### Security (1 change) + +- Monkey kubeclient to not follow any redirects. + + ## 11.4.3 (2018-10-26) - No changes. @@ -250,6 +271,13 @@ entry. - Check frozen string in style builds. (gfyoung) +## 11.3.9 (2018-10-31) + +### Security (1 change) + +- Monkey kubeclient to not follow any redirects. + + ## 11.3.8 (2018-10-27) - No changes. @@ -555,6 +583,13 @@ entry. - Creates Vue component for artifacts block on job page. +## 11.2.8 (2018-10-31) + +### Security (1 change) + +- Monkey kubeclient to not follow any redirects. + + ## 11.2.7 (2018-10-27) - No changes. diff --git a/Dockerfile.assets b/Dockerfile.assets new file mode 100644 index 00000000000..403d16cc4ab --- /dev/null +++ b/Dockerfile.assets @@ -0,0 +1,4 @@ +# Simple container to store assets for later use +FROM scratch +ADD public/assets /assets/ +CMD /bin/true diff --git a/GITALY_SERVER_VERSION b/GITALY_SERVER_VERSION index bcc9c2840a7..4c2a8041846 100644 --- a/GITALY_SERVER_VERSION +++ b/GITALY_SERVER_VERSION @@ -1 +1 @@ -0.126.0 +0.128.0 diff --git a/app/assets/javascripts/blob_edit/blob_bundle.js b/app/assets/javascripts/blob_edit/blob_bundle.js index 3cc89ff1955..ec27ae8c291 100644 --- a/app/assets/javascripts/blob_edit/blob_bundle.js +++ b/app/assets/javascripts/blob_edit/blob_bundle.js @@ -13,7 +13,7 @@ export default () => { if (editBlobForm.length) { const urlRoot = editBlobForm.data('relativeUrlRoot'); const assetsPath = editBlobForm.data('assetsPrefix'); - const filePath = editBlobForm.data('blobFilename') + const filePath = editBlobForm.data('blobFilename'); const currentAction = $('.js-file-title').data('currentAction'); const projectId = editBlobForm.data('project-id'); diff --git a/app/assets/javascripts/boards/components/board.js b/app/assets/javascripts/boards/components/board.js index 623cda5679a..fb6e5291a61 100644 --- a/app/assets/javascripts/boards/components/board.js +++ b/app/assets/javascripts/boards/components/board.js @@ -42,7 +42,7 @@ export default Vue.extend({ required: true, }, }, - data () { + data() { return { detailIssue: boardsStore.detail, filter: boardsStore.filter, @@ -55,27 +55,26 @@ export default Vue.extend({ }, isNewIssueShown() { return this.list.type === 'backlog' || (!this.disabled && this.list.type !== 'closed'); - } + }, }, watch: { filter: { handler() { this.list.page = 1; - this.list.getIssues(true) - .catch(() => { - // TODO: handle request error - }); + this.list.getIssues(true).catch(() => { + // TODO: handle request error + }); }, deep: true, - } + }, }, - mounted () { + mounted() { this.sortableOptions = getBoardSortableDefaultOptions({ disabled: this.disabled, group: 'boards', draggable: '.is-draggable', handle: '.js-board-handle', - onEnd: (e) => { + onEnd: e => { sortableEnd(); if (e.newIndex !== undefined && e.oldIndex !== e.newIndex) { @@ -86,14 +85,15 @@ export default Vue.extend({ boardsStore.moveList(list, order); }); } - } + }, }); this.sortable = Sortable.create(this.$el.parentNode, this.sortableOptions); }, created() { if (this.list.isExpandable && AccessorUtilities.isLocalStorageAccessSafe()) { - const isCollapsed = localStorage.getItem(`boards.${this.boardId}.${this.list.type}.expanded`) === 'false'; + const isCollapsed = + localStorage.getItem(`boards.${this.boardId}.${this.list.type}.expanded`) === 'false'; this.list.isExpanded = !isCollapsed; } @@ -107,7 +107,10 @@ export default Vue.extend({ this.list.isExpanded = !this.list.isExpanded; if (AccessorUtilities.isLocalStorageAccessSafe()) { - localStorage.setItem(`boards.${this.boardId}.${this.list.type}.expanded`, this.list.isExpanded); + localStorage.setItem( + `boards.${this.boardId}.${this.list.type}.expanded`, + this.list.isExpanded, + ); } } }, diff --git a/app/assets/javascripts/boards/components/board_blank_state.vue b/app/assets/javascripts/boards/components/board_blank_state.vue index 38aaec73d7d..561a4636ef5 100644 --- a/app/assets/javascripts/boards/components/board_blank_state.vue +++ b/app/assets/javascripts/boards/components/board_blank_state.vue @@ -32,18 +32,18 @@ export default { boardsStore.state.lists = _.sortBy(boardsStore.state.lists, 'position'); // Save the labels - gl.boardService.generateDefaultLists() + gl.boardService + .generateDefaultLists() .then(res => res.data) - .then((data) => { - data.forEach((listObj) => { + .then(data => { + data.forEach(listObj => { const list = boardsStore.findList('title', listObj.title); list.id = listObj.id; list.label.id = listObj.label.id; - list.getIssues() - .catch(() => { - // TODO: handle request error - }); + list.getIssues().catch(() => { + // TODO: handle request error + }); }); }) .catch(() => { @@ -57,7 +57,6 @@ export default { clearBlankState: boardsStore.removeBlankState.bind(boardsStore), }, }; - </script> <template> diff --git a/app/assets/javascripts/boards/components/board_card.vue b/app/assets/javascripts/boards/components/board_card.vue index 843498f0d06..2f31316aa76 100644 --- a/app/assets/javascripts/boards/components/board_card.vue +++ b/app/assets/javascripts/boards/components/board_card.vue @@ -1,77 +1,77 @@ <script> - /* eslint-disable vue/require-default-prop */ - import IssueCardInner from './issue_card_inner.vue'; - import eventHub from '../eventhub'; - import boardsStore from '../stores/boards_store'; +/* eslint-disable vue/require-default-prop */ +import IssueCardInner from './issue_card_inner.vue'; +import eventHub from '../eventhub'; +import boardsStore from '../stores/boards_store'; - export default { - name: 'BoardsIssueCard', - components: { - IssueCardInner, +export default { + name: 'BoardsIssueCard', + components: { + IssueCardInner, + }, + props: { + list: { + type: Object, + default: () => ({}), }, - props: { - list: { - type: Object, - default: () => ({}), - }, - issue: { - type: Object, - default: () => ({}), - }, - issueLinkBase: { - type: String, - default: '', - }, - disabled: { - type: Boolean, - default: false, - }, - index: { - type: Number, - default: 0, - }, - rootPath: { - type: String, - default: '', - }, - groupId: { - type: Number, - }, + issue: { + type: Object, + default: () => ({}), }, - data() { - return { - showDetail: false, - detailIssue: boardsStore.detail, - }; + issueLinkBase: { + type: String, + default: '', }, - computed: { - issueDetailVisible() { - return this.detailIssue.issue && this.detailIssue.issue.id === this.issue.id; - }, + disabled: { + type: Boolean, + default: false, }, - methods: { - mouseDown() { - this.showDetail = true; - }, - mouseMove() { - this.showDetail = false; - }, - showIssue(e) { - if (e.target.classList.contains('js-no-trigger')) return; + index: { + type: Number, + default: 0, + }, + rootPath: { + type: String, + default: '', + }, + groupId: { + type: Number, + }, + }, + data() { + return { + showDetail: false, + detailIssue: boardsStore.detail, + }; + }, + computed: { + issueDetailVisible() { + return this.detailIssue.issue && this.detailIssue.issue.id === this.issue.id; + }, + }, + methods: { + mouseDown() { + this.showDetail = true; + }, + mouseMove() { + this.showDetail = false; + }, + showIssue(e) { + if (e.target.classList.contains('js-no-trigger')) return; - if (this.showDetail) { - this.showDetail = false; + if (this.showDetail) { + this.showDetail = false; - if (boardsStore.detail.issue && boardsStore.detail.issue.id === this.issue.id) { - eventHub.$emit('clearDetailIssue'); - } else { - eventHub.$emit('newDetailIssue', this.issue); - boardsStore.detail.list = this.list; - } + if (boardsStore.detail.issue && boardsStore.detail.issue.id === this.issue.id) { + eventHub.$emit('clearDetailIssue'); + } else { + eventHub.$emit('newDetailIssue', this.issue); + boardsStore.detail.list = this.list; } - }, + } }, - }; + }, +}; </script> <template> diff --git a/app/assets/javascripts/boards/components/board_new_issue.vue b/app/assets/javascripts/boards/components/board_new_issue.vue index ae2d1ee3c6e..ee3dc38bca6 100644 --- a/app/assets/javascripts/boards/components/board_new_issue.vue +++ b/app/assets/javascripts/boards/components/board_new_issue.vue @@ -62,7 +62,8 @@ export default { eventHub.$emit(`scroll-board-list-${this.list.id}`); this.cancel(); - return this.list.newIssue(issue) + return this.list + .newIssue(issue) .then(() => { // Need this because our jQuery very kindly disables buttons on ALL form submissions $(this.$refs.submitButton).enable(); diff --git a/app/assets/javascripts/boards/components/board_sidebar.js b/app/assets/javascripts/boards/components/board_sidebar.js index 62666954de0..e637e1f1223 100644 --- a/app/assets/javascripts/boards/components/board_sidebar.js +++ b/app/assets/javascripts/boards/components/board_sidebar.js @@ -38,7 +38,7 @@ export default Vue.extend({ }; }, computed: { - showSidebar () { + showSidebar() { return Object.keys(this.issue).length; }, milestoneTitle() { @@ -51,18 +51,20 @@ export default Vue.extend({ return this.issue.labels && this.issue.labels.length; }, labelDropdownTitle() { - return this.hasLabels ? sprintf(__('%{firstLabel} +%{labelCount} more'), { - firstLabel: this.issue.labels[0].title, - labelCount: this.issue.labels.length - 1 - }) : __('Label'); + return this.hasLabels + ? sprintf(__('%{firstLabel} +%{labelCount} more'), { + firstLabel: this.issue.labels[0].title, + labelCount: this.issue.labels.length - 1, + }) + : __('Label'); }, selectedLabels() { return this.hasLabels ? this.issue.labels.map(l => l.title).join(',') : ''; - } + }, }, watch: { detail: { - handler () { + handler() { if (this.issue.id !== this.detail.issue.id) { $('.block.assignee') .find('input:not(.js-vue)[name="issue[assignee_ids][]"]') @@ -71,17 +73,19 @@ export default Vue.extend({ }); $('.js-issue-board-sidebar', this.$el).each((i, el) => { - $(el).data('glDropdown').clearMenu(); + $(el) + .data('glDropdown') + .clearMenu(); }); } this.issue = this.detail.issue; this.list = this.detail.list; }, - deep: true + deep: true, }, }, - created () { + created() { // Get events from glDropdown eventHub.$on('sidebar.removeAssignee', this.removeAssignee); eventHub.$on('sidebar.addAssignee', this.addAssignee); @@ -94,7 +98,7 @@ export default Vue.extend({ eventHub.$off('sidebar.removeAllAssignees', this.removeAllAssignees); eventHub.$off('sidebar.saveAssignees', this.saveAssignees); }, - mounted () { + mounted() { new IssuableContext(this.currentUser); new MilestoneSelect(); new DueDateSelectors(); @@ -102,29 +106,30 @@ export default Vue.extend({ new Sidebar(); }, methods: { - closeSidebar () { + closeSidebar() { this.detail.issue = {}; }, - assignSelf () { + assignSelf() { // Notify gl dropdown that we are now assigning to current user this.$refs.assigneeBlock.dispatchEvent(new Event('assignYourself')); this.addAssignee(this.currentUser); this.saveAssignees(); }, - removeAssignee (a) { + removeAssignee(a) { boardsStore.detail.issue.removeAssignee(a); }, - addAssignee (a) { + addAssignee(a) { boardsStore.detail.issue.addAssignee(a); }, - removeAllAssignees () { + removeAllAssignees() { boardsStore.detail.issue.removeAllAssignees(); }, - saveAssignees () { + saveAssignees() { this.loadingAssignees = true; - boardsStore.detail.issue.update() + boardsStore.detail.issue + .update() .then(() => { this.loadingAssignees = false; }) diff --git a/app/assets/javascripts/boards/components/issue_card_inner.vue b/app/assets/javascripts/boards/components/issue_card_inner.vue index aa98f35786e..d956777a86b 100644 --- a/app/assets/javascripts/boards/components/issue_card_inner.vue +++ b/app/assets/javascripts/boards/components/issue_card_inner.vue @@ -1,142 +1,142 @@ <script> - import $ from 'jquery'; - import Icon from '~/vue_shared/components/icon.vue'; - import UserAvatarLink from '../../vue_shared/components/user_avatar/user_avatar_link.vue'; - import eventHub from '../eventhub'; - import tooltip from '../../vue_shared/directives/tooltip'; - import boardsStore from '../stores/boards_store'; +import $ from 'jquery'; +import Icon from '~/vue_shared/components/icon.vue'; +import UserAvatarLink from '../../vue_shared/components/user_avatar/user_avatar_link.vue'; +import eventHub from '../eventhub'; +import tooltip from '../../vue_shared/directives/tooltip'; +import boardsStore from '../stores/boards_store'; - export default { - components: { - UserAvatarLink, - Icon, - }, - directives: { - tooltip, - }, - props: { - issue: { - type: Object, - required: true, - }, - issueLinkBase: { - type: String, - required: true, - }, - list: { - type: Object, - required: false, - default: () => ({}), - }, - rootPath: { - type: String, - required: true, - }, - updateFilters: { - type: Boolean, - required: false, - default: false, - }, - groupId: { - type: Number, - required: false, - default: null, - }, - }, - data() { - return { - limitBeforeCounter: 3, - maxRender: 4, - maxCounter: 99, - }; +export default { + components: { + UserAvatarLink, + Icon, + }, + directives: { + tooltip, + }, + props: { + issue: { + type: Object, + required: true, }, - computed: { - numberOverLimit() { - return this.issue.assignees.length - this.limitBeforeCounter; - }, - assigneeCounterTooltip() { - return `${this.assigneeCounterLabel} more`; - }, - assigneeCounterLabel() { - if (this.numberOverLimit > this.maxCounter) { - return `${this.maxCounter}+`; - } - - return `+${this.numberOverLimit}`; - }, - shouldRenderCounter() { - if (this.issue.assignees.length <= this.maxRender) { - return false; - } + issueLinkBase: { + type: String, + required: true, + }, + list: { + type: Object, + required: false, + default: () => ({}), + }, + rootPath: { + type: String, + required: true, + }, + updateFilters: { + type: Boolean, + required: false, + default: false, + }, + groupId: { + type: Number, + required: false, + default: null, + }, + }, + data() { + return { + limitBeforeCounter: 3, + maxRender: 4, + maxCounter: 99, + }; + }, + computed: { + numberOverLimit() { + return this.issue.assignees.length - this.limitBeforeCounter; + }, + assigneeCounterTooltip() { + return `${this.assigneeCounterLabel} more`; + }, + assigneeCounterLabel() { + if (this.numberOverLimit > this.maxCounter) { + return `${this.maxCounter}+`; + } - return this.issue.assignees.length > this.numberOverLimit; - }, - issueId() { - if (this.issue.iid) { - return `#${this.issue.iid}`; - } + return `+${this.numberOverLimit}`; + }, + shouldRenderCounter() { + if (this.issue.assignees.length <= this.maxRender) { return false; - }, - showLabelFooter() { - return this.issue.labels.find(l => this.showLabel(l)) !== undefined; - }, - }, - methods: { - isIndexLessThanlimit(index) { - return index < this.limitBeforeCounter; - }, - shouldRenderAssignee(index) { - // Eg. maxRender is 4, - // Render up to all 4 assignees if there are only 4 assigness - // Otherwise render up to the limitBeforeCounter - if (this.issue.assignees.length <= this.maxRender) { - return index < this.maxRender; - } + } - return index < this.limitBeforeCounter; - }, - assigneeUrl(assignee) { - return `${this.rootPath}${assignee.username}`; - }, - assigneeUrlTitle(assignee) { - return `Assigned to ${assignee.name}`; - }, - avatarUrlTitle(assignee) { - return `Avatar for ${assignee.name}`; - }, - showLabel(label) { - if (!label.id) return false; - return true; - }, - filterByLabel(label, e) { - if (!this.updateFilters) return; + return this.issue.assignees.length > this.numberOverLimit; + }, + issueId() { + if (this.issue.iid) { + return `#${this.issue.iid}`; + } + return false; + }, + showLabelFooter() { + return this.issue.labels.find(l => this.showLabel(l)) !== undefined; + }, + }, + methods: { + isIndexLessThanlimit(index) { + return index < this.limitBeforeCounter; + }, + shouldRenderAssignee(index) { + // Eg. maxRender is 4, + // Render up to all 4 assignees if there are only 4 assigness + // Otherwise render up to the limitBeforeCounter + if (this.issue.assignees.length <= this.maxRender) { + return index < this.maxRender; + } + + return index < this.limitBeforeCounter; + }, + assigneeUrl(assignee) { + return `${this.rootPath}${assignee.username}`; + }, + assigneeUrlTitle(assignee) { + return `Assigned to ${assignee.name}`; + }, + avatarUrlTitle(assignee) { + return `Avatar for ${assignee.name}`; + }, + showLabel(label) { + if (!label.id) return false; + return true; + }, + filterByLabel(label, e) { + if (!this.updateFilters) return; - const filterPath = boardsStore.filter.path.split('&'); - const labelTitle = encodeURIComponent(label.title); - const param = `label_name[]=${labelTitle}`; - const labelIndex = filterPath.indexOf(param); - $(e.currentTarget).tooltip('hide'); + const filterPath = boardsStore.filter.path.split('&'); + const labelTitle = encodeURIComponent(label.title); + const param = `label_name[]=${labelTitle}`; + const labelIndex = filterPath.indexOf(param); + $(e.currentTarget).tooltip('hide'); - if (labelIndex === -1) { - filterPath.push(param); - } else { - filterPath.splice(labelIndex, 1); - } + if (labelIndex === -1) { + filterPath.push(param); + } else { + filterPath.splice(labelIndex, 1); + } - boardsStore.filter.path = filterPath.join('&'); + boardsStore.filter.path = filterPath.join('&'); - boardsStore.updateFiltersUrl(); + boardsStore.updateFiltersUrl(); - eventHub.$emit('updateTokens'); - }, - labelStyle(label) { - return { - backgroundColor: label.color, - color: label.textColor, - }; - }, - }, - }; + eventHub.$emit('updateTokens'); + }, + labelStyle(label) { + return { + backgroundColor: label.color, + color: label.textColor, + }; + }, + }, +}; </script> <template> <div> diff --git a/app/assets/javascripts/boards/components/modal/empty_state.vue b/app/assets/javascripts/boards/components/modal/empty_state.vue index dbd69f84526..795ba864545 100644 --- a/app/assets/javascripts/boards/components/modal/empty_state.vue +++ b/app/assets/javascripts/boards/components/modal/empty_state.vue @@ -20,7 +20,7 @@ export default { computed: { contents() { const obj = { - title: 'You haven\'t added any issues to your project yet', + title: "You haven't added any issues to your project yet", content: ` An issue can be a bug, a todo or a feature request that needs to be discussed in a project. Besides, issues are searchable and filterable. @@ -28,7 +28,7 @@ export default { }; if (this.activeTab === 'selected') { - obj.title = 'You haven\'t selected any issues yet'; + obj.title = "You haven't selected any issues yet"; obj.content = ` Go back to <strong>Open issues</strong> and select some issues to add to your board. diff --git a/app/assets/javascripts/boards/components/modal/footer.vue b/app/assets/javascripts/boards/components/modal/footer.vue index 268ca6bca13..d51597ed22d 100644 --- a/app/assets/javascripts/boards/components/modal/footer.vue +++ b/app/assets/javascripts/boards/components/modal/footer.vue @@ -42,19 +42,17 @@ export default { const req = this.buildUpdateRequest(list); // Post the data to the backend - gl.boardService - .bulkUpdate(issueIds, req) - .catch(() => { - Flash(__('Failed to update issues, please try again.')); + gl.boardService.bulkUpdate(issueIds, req).catch(() => { + Flash(__('Failed to update issues, please try again.')); - selectedIssues.forEach((issue) => { - list.removeIssue(issue); - list.issuesSize -= 1; - }); + selectedIssues.forEach(issue => { + list.removeIssue(issue); + list.issuesSize -= 1; }); + }); // Add the issues on the frontend - selectedIssues.forEach((issue) => { + selectedIssues.forEach(issue => { list.addIssue(issue); list.issuesSize += 1; }); diff --git a/app/assets/javascripts/boards/components/modal/header.vue b/app/assets/javascripts/boards/components/modal/header.vue index 979fb4d7199..fc6cefa89a9 100644 --- a/app/assets/javascripts/boards/components/modal/header.vue +++ b/app/assets/javascripts/boards/components/modal/header.vue @@ -1,52 +1,52 @@ <script> - import ModalFilters from './filters'; - import ModalTabs from './tabs.vue'; - import ModalStore from '../../stores/modal_store'; - import modalMixin from '../../mixins/modal_mixins'; +import ModalFilters from './filters'; +import ModalTabs from './tabs.vue'; +import ModalStore from '../../stores/modal_store'; +import modalMixin from '../../mixins/modal_mixins'; - export default { - components: { - ModalTabs, - ModalFilters, +export default { + components: { + ModalTabs, + ModalFilters, + }, + mixins: [modalMixin], + props: { + projectId: { + type: Number, + required: true, }, - mixins: [modalMixin], - props: { - projectId: { - type: Number, - required: true, - }, - milestonePath: { - type: String, - required: true, - }, - labelPath: { - type: String, - required: true, - }, + milestonePath: { + type: String, + required: true, }, - data() { - return ModalStore.store; + labelPath: { + type: String, + required: true, }, - computed: { - selectAllText() { - if (ModalStore.selectedCount() !== this.issues.length || this.issues.length === 0) { - return 'Select all'; - } + }, + data() { + return ModalStore.store; + }, + computed: { + selectAllText() { + if (ModalStore.selectedCount() !== this.issues.length || this.issues.length === 0) { + return 'Select all'; + } - return 'Deselect all'; - }, - showSearch() { - return this.activeTab === 'all' && !this.loading && this.issuesCount > 0; - }, + return 'Deselect all'; }, - methods: { - toggleAll() { - this.$refs.selectAllBtn.blur(); + showSearch() { + return this.activeTab === 'all' && !this.loading && this.issuesCount > 0; + }, + }, + methods: { + toggleAll() { + this.$refs.selectAllBtn.blur(); - ModalStore.toggleAll(); - }, + ModalStore.toggleAll(); }, - }; + }, +}; </script> <template> <div> diff --git a/app/assets/javascripts/boards/components/modal/index.vue b/app/assets/javascripts/boards/components/modal/index.vue index 0c4c709324d..40949cc0656 100644 --- a/app/assets/javascripts/boards/components/modal/index.vue +++ b/app/assets/javascripts/boards/components/modal/index.vue @@ -1,143 +1,144 @@ <script> - /* global ListIssue */ - import { urlParamsToObject } from '~/lib/utils/common_utils'; - import ModalHeader from './header.vue'; - import ModalList from './list.vue'; - import ModalFooter from './footer.vue'; - import EmptyState from './empty_state.vue'; - import ModalStore from '../../stores/modal_store'; +/* global ListIssue */ +import { urlParamsToObject } from '~/lib/utils/common_utils'; +import ModalHeader from './header.vue'; +import ModalList from './list.vue'; +import ModalFooter from './footer.vue'; +import EmptyState from './empty_state.vue'; +import ModalStore from '../../stores/modal_store'; - export default { - components: { - EmptyState, - ModalHeader, - ModalList, - ModalFooter, +export default { + components: { + EmptyState, + ModalHeader, + ModalList, + ModalFooter, + }, + props: { + newIssuePath: { + type: String, + required: true, }, - props: { - newIssuePath: { - type: String, - required: true, - }, - emptyStateSvg: { - type: String, - required: true, - }, - issueLinkBase: { - type: String, - required: true, - }, - rootPath: { - type: String, - required: true, - }, - projectId: { - type: Number, - required: true, - }, - milestonePath: { - type: String, - required: true, - }, - labelPath: { - type: String, - required: true, - }, + emptyStateSvg: { + type: String, + required: true, }, - data() { - return ModalStore.store; + issueLinkBase: { + type: String, + required: true, }, - computed: { - showList() { - if (this.activeTab === 'selected') { - return this.selectedIssues.length > 0; - } + rootPath: { + type: String, + required: true, + }, + projectId: { + type: Number, + required: true, + }, + milestonePath: { + type: String, + required: true, + }, + labelPath: { + type: String, + required: true, + }, + }, + data() { + return ModalStore.store; + }, + computed: { + showList() { + if (this.activeTab === 'selected') { + return this.selectedIssues.length > 0; + } - return this.issuesCount > 0; - }, - showEmptyState() { - if (!this.loading && this.issuesCount === 0) { - return true; - } + return this.issuesCount > 0; + }, + showEmptyState() { + if (!this.loading && this.issuesCount === 0) { + return true; + } - return this.activeTab === 'selected' && this.selectedIssues.length === 0; - }, + return this.activeTab === 'selected' && this.selectedIssues.length === 0; }, - watch: { - page() { - this.loadIssues(); - }, - showAddIssuesModal() { - if (this.showAddIssuesModal && !this.issues.length) { - this.loading = true; + }, + watch: { + page() { + this.loadIssues(); + }, + showAddIssuesModal() { + if (this.showAddIssuesModal && !this.issues.length) { + this.loading = true; + const loadingDone = () => { + this.loading = false; + }; + + this.loadIssues() + .then(loadingDone) + .catch(loadingDone); + } else if (!this.showAddIssuesModal) { + this.issues = []; + this.selectedIssues = []; + this.issuesCount = false; + } + }, + filter: { + handler() { + if (this.$el.tagName) { + this.page = 1; + this.filterLoading = true; const loadingDone = () => { - this.loading = false; + this.filterLoading = false; }; - this.loadIssues() + this.loadIssues(true) .then(loadingDone) .catch(loadingDone); - } else if (!this.showAddIssuesModal) { - this.issues = []; - this.selectedIssues = []; - this.issuesCount = false; } }, - filter: { - handler() { - if (this.$el.tagName) { - this.page = 1; - this.filterLoading = true; - const loadingDone = () => { - this.filterLoading = false; - }; - - this.loadIssues(true) - .then(loadingDone) - .catch(loadingDone); - } - }, - deep: true, - }, + deep: true, }, - created() { - this.page = 1; - }, - methods: { - loadIssues(clearIssues = false) { - if (!this.showAddIssuesModal) return false; + }, + created() { + this.page = 1; + }, + methods: { + loadIssues(clearIssues = false) { + if (!this.showAddIssuesModal) return false; - return gl.boardService.getBacklog({ + return gl.boardService + .getBacklog({ ...urlParamsToObject(this.filter.path), page: this.page, per: this.perPage, }) - .then(res => res.data) - .then(data => { - if (clearIssues) { - this.issues = []; - } + .then(res => res.data) + .then(data => { + if (clearIssues) { + this.issues = []; + } - data.issues.forEach(issueObj => { - const issue = new ListIssue(issueObj); - const foundSelectedIssue = ModalStore.findSelectedIssue(issue); - issue.selected = !!foundSelectedIssue; + data.issues.forEach(issueObj => { + const issue = new ListIssue(issueObj); + const foundSelectedIssue = ModalStore.findSelectedIssue(issue); + issue.selected = !!foundSelectedIssue; - this.issues.push(issue); - }); + this.issues.push(issue); + }); - this.loadingNewPage = false; + this.loadingNewPage = false; - if (!this.issuesCount) { - this.issuesCount = data.size; - } - }) - .catch(() => { - // TODO: handle request error - }); - }, + if (!this.issuesCount) { + this.issuesCount = data.size; + } + }) + .catch(() => { + // TODO: handle request error + }); }, - }; + }, +}; </script> <template> <div diff --git a/app/assets/javascripts/boards/components/modal/list.vue b/app/assets/javascripts/boards/components/modal/list.vue index c93fd9f415c..e11f398e70d 100644 --- a/app/assets/javascripts/boards/components/modal/list.vue +++ b/app/assets/javascripts/boards/components/modal/list.vue @@ -1,120 +1,120 @@ <script> - import Icon from '~/vue_shared/components/icon.vue'; - import bp from '../../../breakpoints'; - import ModalStore from '../../stores/modal_store'; - import IssueCardInner from '../issue_card_inner.vue'; +import Icon from '~/vue_shared/components/icon.vue'; +import bp from '../../../breakpoints'; +import ModalStore from '../../stores/modal_store'; +import IssueCardInner from '../issue_card_inner.vue'; - export default { - components: { - IssueCardInner, - Icon, +export default { + components: { + IssueCardInner, + Icon, + }, + props: { + issueLinkBase: { + type: String, + required: true, }, - props: { - issueLinkBase: { - type: String, - required: true, - }, - rootPath: { - type: String, - required: true, - }, - emptyStateSvg: { - type: String, - required: true, - }, + rootPath: { + type: String, + required: true, }, - data() { - return ModalStore.store; + emptyStateSvg: { + type: String, + required: true, }, - computed: { - loopIssues() { - if (this.activeTab === 'all') { - return this.issues; - } + }, + data() { + return ModalStore.store; + }, + computed: { + loopIssues() { + if (this.activeTab === 'all') { + return this.issues; + } - return this.selectedIssues; - }, - groupedIssues() { - const groups = []; - this.loopIssues.forEach((issue, i) => { - const index = i % this.columns; + return this.selectedIssues; + }, + groupedIssues() { + const groups = []; + this.loopIssues.forEach((issue, i) => { + const index = i % this.columns; - if (!groups[index]) { - groups.push([]); - } + if (!groups[index]) { + groups.push([]); + } - groups[index].push(issue); - }); + groups[index].push(issue); + }); - return groups; - }, + return groups; }, - watch: { - activeTab() { - if (this.activeTab === 'all') { - ModalStore.purgeUnselectedIssues(); - } - }, + }, + watch: { + activeTab() { + if (this.activeTab === 'all') { + ModalStore.purgeUnselectedIssues(); + } }, - mounted() { - this.scrollHandlerWrapper = this.scrollHandler.bind(this); - this.setColumnCountWrapper = this.setColumnCount.bind(this); - this.setColumnCount(); + }, + mounted() { + this.scrollHandlerWrapper = this.scrollHandler.bind(this); + this.setColumnCountWrapper = this.setColumnCount.bind(this); + this.setColumnCount(); - this.$refs.list.addEventListener('scroll', this.scrollHandlerWrapper); - window.addEventListener('resize', this.setColumnCountWrapper); + this.$refs.list.addEventListener('scroll', this.scrollHandlerWrapper); + window.addEventListener('resize', this.setColumnCountWrapper); + }, + beforeDestroy() { + this.$refs.list.removeEventListener('scroll', this.scrollHandlerWrapper); + window.removeEventListener('resize', this.setColumnCountWrapper); + }, + methods: { + scrollHandler() { + const currentPage = Math.floor(this.issues.length / this.perPage); + + if ( + this.scrollTop() > this.scrollHeight() - 100 && + !this.loadingNewPage && + currentPage === this.page + ) { + this.loadingNewPage = true; + this.page += 1; + } }, - beforeDestroy() { - this.$refs.list.removeEventListener('scroll', this.scrollHandlerWrapper); - window.removeEventListener('resize', this.setColumnCountWrapper); + toggleIssue(e, issue) { + if (e.target.tagName !== 'A') { + ModalStore.toggleIssue(issue); + } }, - methods: { - scrollHandler() { - const currentPage = Math.floor(this.issues.length / this.perPage); - - if ( - this.scrollTop() > this.scrollHeight() - 100 && - !this.loadingNewPage && - currentPage === this.page - ) { - this.loadingNewPage = true; - this.page += 1; - } - }, - toggleIssue(e, issue) { - if (e.target.tagName !== 'A') { - ModalStore.toggleIssue(issue); - } - }, - listHeight() { - return this.$refs.list.getBoundingClientRect().height; - }, - scrollHeight() { - return this.$refs.list.scrollHeight; - }, - scrollTop() { - return this.$refs.list.scrollTop + this.listHeight(); - }, - showIssue(issue) { - if (this.activeTab === 'all') return true; + listHeight() { + return this.$refs.list.getBoundingClientRect().height; + }, + scrollHeight() { + return this.$refs.list.scrollHeight; + }, + scrollTop() { + return this.$refs.list.scrollTop + this.listHeight(); + }, + showIssue(issue) { + if (this.activeTab === 'all') return true; - const index = ModalStore.selectedIssueIndex(issue); + const index = ModalStore.selectedIssueIndex(issue); - return index !== -1; - }, - setColumnCount() { - const breakpoint = bp.getBreakpointSize(); + return index !== -1; + }, + setColumnCount() { + const breakpoint = bp.getBreakpointSize(); - if (breakpoint === 'lg' || breakpoint === 'md') { - this.columns = 3; - } else if (breakpoint === 'sm') { - this.columns = 2; - } else { - this.columns = 1; - } - }, + if (breakpoint === 'lg' || breakpoint === 'md') { + this.columns = 3; + } else if (breakpoint === 'sm') { + this.columns = 2; + } else { + this.columns = 1; + } }, - }; + }, +}; </script> <template> <section diff --git a/app/assets/javascripts/boards/components/modal/tabs.vue b/app/assets/javascripts/boards/components/modal/tabs.vue index d926b080094..5d661590e8e 100644 --- a/app/assets/javascripts/boards/components/modal/tabs.vue +++ b/app/assets/javascripts/boards/components/modal/tabs.vue @@ -1,21 +1,21 @@ <script> - import ModalStore from '../../stores/modal_store'; - import modalMixin from '../../mixins/modal_mixins'; +import ModalStore from '../../stores/modal_store'; +import modalMixin from '../../mixins/modal_mixins'; - export default { - mixins: [modalMixin], - data() { - return ModalStore.store; +export default { + mixins: [modalMixin], + data() { + return ModalStore.store; + }, + computed: { + selectedCount() { + return ModalStore.selectedCount(); }, - computed: { - selectedCount() { - return ModalStore.selectedCount(); - }, - }, - destroyed() { - this.activeTab = 'all'; - }, - }; + }, + destroyed() { + this.activeTab = 'all'; + }, +}; </script> <template> <div class="top-area prepend-top-10 append-bottom-10"> diff --git a/app/assets/javascripts/boards/components/new_list_dropdown.js b/app/assets/javascripts/boards/components/new_list_dropdown.js index 2c2045f8901..f7016561f93 100644 --- a/app/assets/javascripts/boards/components/new_list_dropdown.js +++ b/app/assets/javascripts/boards/components/new_list_dropdown.js @@ -6,36 +6,41 @@ import _ from 'underscore'; import CreateLabelDropdown from '../../create_label'; import boardsStore from '../stores/boards_store'; -$(document).off('created.label').on('created.label', (e, label) => { - boardsStore.new({ - title: label.title, - position: boardsStore.state.lists.length - 2, - list_type: 'label', - label: { - id: label.id, +$(document) + .off('created.label') + .on('created.label', (e, label) => { + boardsStore.new({ title: label.title, - color: label.color, - }, + position: boardsStore.state.lists.length - 2, + list_type: 'label', + label: { + id: label.id, + title: label.title, + color: label.color, + }, + }); }); -}); export default function initNewListDropdown() { - $('.js-new-board-list').each(function () { + $('.js-new-board-list').each(function() { const $this = $(this); - new CreateLabelDropdown($this.closest('.dropdown').find('.dropdown-new-label'), $this.data('namespacePath'), $this.data('projectPath')); + new CreateLabelDropdown( + $this.closest('.dropdown').find('.dropdown-new-label'), + $this.data('namespacePath'), + $this.data('projectPath'), + ); $this.glDropdown({ data(term, callback) { - axios.get($this.attr('data-list-labels-path')) - .then(({ data }) => { - callback(data); - }); + axios.get($this.attr('data-list-labels-path')).then(({ data }) => { + callback(data); + }); }, - renderRow (label) { + renderRow(label) { const active = boardsStore.findList('title', label.title); const $li = $('<li />'); const $a = $('<a />', { - class: (active ? `is-active js-board-list-${active.id}` : ''), + class: active ? `is-active js-board-list-${active.id}` : '', text: label.title, href: '#', }); @@ -53,7 +58,7 @@ export default function initNewListDropdown() { selectable: true, multiSelect: true, containerSelector: '.js-tab-container-labels .dropdown-page-one .dropdown-content', - clicked (options) { + clicked(options) { const { e } = options; const label = options.selectedObj; e.preventDefault(); diff --git a/app/assets/javascripts/boards/components/project_select.vue b/app/assets/javascripts/boards/components/project_select.vue index 427a0868b0c..0f01a2a6c09 100644 --- a/app/assets/javascripts/boards/components/project_select.vue +++ b/app/assets/javascripts/boards/components/project_select.vue @@ -46,7 +46,7 @@ export default { selectable: true, data: (term, callback) => { this.loading = true; - return Api.groupProjects(this.groupId, term, {with_issues_enabled: true}, projects => { + return Api.groupProjects(this.groupId, term, { with_issues_enabled: true }, projects => { this.loading = false; callback(projects); }); @@ -54,7 +54,9 @@ export default { renderRow(project) { return ` <li> - <a href='#' class='dropdown-menu-link' data-project-id="${project.id}" data-project-name="${project.name}"> + <a href='#' class='dropdown-menu-link' data-project-id="${ + project.id + }" data-project-name="${project.name}"> ${_.escape(project.name)} </a> </li> diff --git a/app/assets/javascripts/boards/components/sidebar/remove_issue.vue b/app/assets/javascripts/boards/components/sidebar/remove_issue.vue index b8f2e324d43..d681e6a431c 100644 --- a/app/assets/javascripts/boards/components/sidebar/remove_issue.vue +++ b/app/assets/javascripts/boards/components/sidebar/remove_issue.vue @@ -1,79 +1,77 @@ <script> - import Vue from 'vue'; - import Flash from '../../../flash'; - import { __ } from '../../../locale'; - import boardsStore from '../../stores/boards_store'; +import Vue from 'vue'; +import Flash from '../../../flash'; +import { __ } from '../../../locale'; +import boardsStore from '../../stores/boards_store'; - export default Vue.extend({ - props: { - issue: { - type: Object, - required: true, - }, - list: { - type: Object, - required: true, - }, +export default Vue.extend({ + props: { + issue: { + type: Object, + required: true, }, - computed: { - updateUrl() { - return this.issue.path; - }, + list: { + type: Object, + required: true, }, - methods: { - removeIssue() { - const { issue } = this; - const lists = issue.getLists(); - const req = this.buildPatchRequest(issue, lists); - - const data = { - issue: this.seedPatchRequest(issue, req), - }; + }, + computed: { + updateUrl() { + return this.issue.path; + }, + }, + methods: { + removeIssue() { + const { issue } = this; + const lists = issue.getLists(); + const req = this.buildPatchRequest(issue, lists); - if (data.issue.label_ids.length === 0) { - data.issue.label_ids = ['']; - } + const data = { + issue: this.seedPatchRequest(issue, req), + }; - // Post the remove data - Vue.http.patch(this.updateUrl, data).catch(() => { - Flash(__('Failed to remove issue from board, please try again.')); + if (data.issue.label_ids.length === 0) { + data.issue.label_ids = ['']; + } - lists.forEach(list => { - list.addIssue(issue); - }); - }); + // Post the remove data + Vue.http.patch(this.updateUrl, data).catch(() => { + Flash(__('Failed to remove issue from board, please try again.')); - // Remove from the frontend store lists.forEach(list => { - list.removeIssue(issue); + list.addIssue(issue); }); + }); - boardsStore.detail.issue = {}; - }, - /** - * Build the default patch request. - */ - buildPatchRequest(issue, lists) { - const listLabelIds = lists.map(list => list.label.id); + // Remove from the frontend store + lists.forEach(list => { + list.removeIssue(issue); + }); - const labelIds = issue.labels - .map(label => label.id) - .filter(id => !listLabelIds.includes(id)); + boardsStore.detail.issue = {}; + }, + /** + * Build the default patch request. + */ + buildPatchRequest(issue, lists) { + const listLabelIds = lists.map(list => list.label.id); + + const labelIds = issue.labels.map(label => label.id).filter(id => !listLabelIds.includes(id)); - return { - label_ids: labelIds, - }; - }, - /** - * Seed the given patch request. - * - * (This is overridden in EE) - */ - seedPatchRequest(issue, req) { - return req; - }, + return { + label_ids: labelIds, + }; + }, + /** + * Seed the given patch request. + * + * (This is overridden in EE) + */ + seedPatchRequest(issue, req) { + return req; }, - }); + }, +}); </script> <template> <div diff --git a/app/assets/javascripts/boards/filtered_search_boards.js b/app/assets/javascripts/boards/filtered_search_boards.js index acf41e5689e..c14d69c5d18 100644 --- a/app/assets/javascripts/boards/filtered_search_boards.js +++ b/app/assets/javascripts/boards/filtered_search_boards.js @@ -32,7 +32,7 @@ export default class FilteredSearchBoards extends FilteredSearchManager { const tokens = FilteredSearchContainer.container.querySelectorAll('.js-visual-token'); // Remove all the tokens as they will be replaced by the search manager - [].forEach.call(tokens, (el) => { + [].forEach.call(tokens, el => { el.parentNode.removeChild(el); }); @@ -50,7 +50,10 @@ export default class FilteredSearchBoards extends FilteredSearchManager { canEdit(tokenName, tokenValue) { if (this.cantEdit.includes(tokenName)) return false; - return this.cantEditWithValue.findIndex(token => token.name === tokenName && - token.value === tokenValue) === -1; + return ( + this.cantEditWithValue.findIndex( + token => token.name === tokenName && token.value === tokenValue, + ) === -1 + ); } } diff --git a/app/assets/javascripts/boards/index.js b/app/assets/javascripts/boards/index.js index 91861f2f9ee..61a3072ac27 100644 --- a/app/assets/javascripts/boards/index.js +++ b/app/assets/javascripts/boards/index.js @@ -32,9 +32,9 @@ export default () => { const $boardApp = document.getElementById('board-app'); // check for browser back and trigger a hard reload to circumvent browser caching. - window.addEventListener('pageshow', (event) => { - const isNavTypeBackForward = window.performance && - window.performance.navigation.type === NavigationType.TYPE_BACK_FORWARD; + window.addEventListener('pageshow', event => { + const isNavTypeBackForward = + window.performance && window.performance.navigation.type === NavigationType.TYPE_BACK_FORWARD; if (event.persisted || isNavTypeBackForward) { window.location.reload(); diff --git a/app/assets/javascripts/boards/mixins/sortable_default_options.js b/app/assets/javascripts/boards/mixins/sortable_default_options.js index c9cde4effb9..983b28d2e67 100644 --- a/app/assets/javascripts/boards/mixins/sortable_default_options.js +++ b/app/assets/javascripts/boards/mixins/sortable_default_options.js @@ -4,7 +4,8 @@ import $ from 'jquery'; import sortableConfig from '../../sortable/sortable_config'; export function sortableStart() { - $('.has-tooltip').tooltip('hide') + $('.has-tooltip') + .tooltip('hide') .tooltip('disable'); document.body.classList.add('is-dragging'); } @@ -15,7 +16,8 @@ export function sortableEnd() { } export function getBoardSortableDefaultOptions(obj) { - const touchEnabled = ('ontouchstart' in window) || window.DocumentTouch && document instanceof DocumentTouch; + const touchEnabled = + 'ontouchstart' in window || (window.DocumentTouch && document instanceof DocumentTouch); const defaultSortOptions = Object.assign({}, sortableConfig, { filter: '.board-delete, .btn', @@ -26,6 +28,8 @@ export function getBoardSortableDefaultOptions(obj) { onEnd: sortableEnd, }); - Object.keys(obj).forEach((key) => { defaultSortOptions[key] = obj[key]; }); + Object.keys(obj).forEach(key => { + defaultSortOptions[key] = obj[key]; + }); return defaultSortOptions; } diff --git a/app/assets/javascripts/boards/models/issue.js b/app/assets/javascripts/boards/models/issue.js index 52d04389b88..669630edcab 100644 --- a/app/assets/javascripts/boards/models/issue.js +++ b/app/assets/javascripts/boards/models/issue.js @@ -9,7 +9,7 @@ import IssueProject from './project'; import boardsStore from '../stores/boards_store'; class ListIssue { - constructor (obj, defaultAvatar) { + constructor(obj, defaultAvatar) { this.id = obj.id; this.iid = obj.iid; this.title = obj.title; @@ -30,6 +30,7 @@ class ListIssue { this.toggleSubscriptionEndpoint = obj.toggle_subscription_endpoint; this.milestone_id = obj.milestone_id; this.project_id = obj.project_id; + this.assignableLabelsEndpoint = obj.assignable_labels_endpoint; if (obj.project) { this.project = new IssueProject(obj.project); @@ -39,54 +40,54 @@ class ListIssue { this.milestone = new ListMilestone(obj.milestone); } - obj.labels.forEach((label) => { + obj.labels.forEach(label => { this.labels.push(new ListLabel(label)); }); this.assignees = obj.assignees.map(a => new ListAssignee(a, defaultAvatar)); } - addLabel (label) { + addLabel(label) { if (!this.findLabel(label)) { this.labels.push(new ListLabel(label)); } } - findLabel (findLabel) { + findLabel(findLabel) { return this.labels.filter(label => label.title === findLabel.title)[0]; } - removeLabel (removeLabel) { + removeLabel(removeLabel) { if (removeLabel) { this.labels = this.labels.filter(label => removeLabel.title !== label.title); } } - removeLabels (labels) { + removeLabels(labels) { labels.forEach(this.removeLabel.bind(this)); } - addAssignee (assignee) { + addAssignee(assignee) { if (!this.findAssignee(assignee)) { this.assignees.push(new ListAssignee(assignee)); } } - findAssignee (findAssignee) { + findAssignee(findAssignee) { return this.assignees.filter(assignee => assignee.id === findAssignee.id)[0]; } - removeAssignee (removeAssignee) { + removeAssignee(removeAssignee) { if (removeAssignee) { this.assignees = this.assignees.filter(assignee => assignee.id !== removeAssignee.id); } } - removeAllAssignees () { + removeAllAssignees() { this.assignees = []; } - getLists () { + getLists() { return boardsStore.state.lists.filter(list => list.findIssue(this.id)); } @@ -102,14 +103,14 @@ class ListIssue { this.isLoading[key] = value; } - update () { + update() { const data = { issue: { milestone_id: this.milestone ? this.milestone.id : null, due_date: this.dueDate, - assignee_ids: this.assignees.length > 0 ? this.assignees.map((u) => u.id) : [0], - label_ids: this.labels.map((label) => label.id) - } + assignee_ids: this.assignees.length > 0 ? this.assignees.map(u => u.id) : [0], + label_ids: this.labels.map(label => label.id), + }, }; if (!data.issue.label_ids.length) { diff --git a/app/assets/javascripts/boards/models/list.js b/app/assets/javascripts/boards/models/list.js index 3161f1da8c9..dd3feedbc0e 100644 --- a/app/assets/javascripts/boards/models/list.js +++ b/app/assets/javascripts/boards/models/list.js @@ -234,11 +234,11 @@ class List { }); } - getTypeInfo (type) { + getTypeInfo(type) { return TYPES[type] || {}; } - onNewIssueResponse (issue, data) { + onNewIssueResponse(issue, data) { issue.id = data.id; issue.iid = data.iid; issue.project = data.project; diff --git a/app/assets/javascripts/boards/services/board_service.js b/app/assets/javascripts/boards/services/board_service.js index 029b0971f2c..3de6eb056c2 100644 --- a/app/assets/javascripts/boards/services/board_service.js +++ b/app/assets/javascripts/boards/services/board_service.js @@ -19,7 +19,9 @@ export default class BoardService { } static generateIssuePath(boardId, id) { - return `${gon.relative_url_root}/-/boards/${boardId ? `${boardId}` : ''}/issues${id ? `/${id}` : ''}`; + return `${gon.relative_url_root}/-/boards/${boardId ? `${boardId}` : ''}/issues${ + id ? `/${id}` : '' + }`; } all() { @@ -54,7 +56,9 @@ export default class BoardService { getIssuesForList(id, filter = {}) { const data = { id }; - Object.keys(filter).forEach((key) => { data[key] = filter[key]; }); + Object.keys(filter).forEach(key => { + data[key] = filter[key]; + }); return axios.get(mergeUrlParams(data, this.generateIssuesPath(id))); } @@ -75,7 +79,9 @@ export default class BoardService { } getBacklog(data) { - return axios.get(mergeUrlParams(data, `${gon.relative_url_root}/-/boards/${this.boardId}/issues.json`)); + return axios.get( + mergeUrlParams(data, `${gon.relative_url_root}/-/boards/${this.boardId}/issues.json`), + ); } bulkUpdate(issueIds, extraData = {}) { diff --git a/app/assets/javascripts/boards/stores/boards_store.js b/app/assets/javascripts/boards/stores/boards_store.js index 471955747fd..eefe14a1d79 100644 --- a/app/assets/javascripts/boards/stores/boards_store.js +++ b/app/assets/javascripts/boards/stores/boards_store.js @@ -20,20 +20,20 @@ const boardsStore = { issue: {}, list: {}, }, - create () { + create() { this.state.lists = []; this.filter.path = getUrlParamsArray().join('&'); this.detail = { issue: {}, }; }, - addList (listObj, defaultAvatar) { + addList(listObj, defaultAvatar) { const list = new List(listObj, defaultAvatar); this.state.lists.push(list); return list; }, - new (listObj) { + new(listObj) { const list = this.addList(listObj); const backlogList = this.findList('type', 'backlog', 'backlog'); @@ -50,44 +50,44 @@ const boardsStore = { }); this.removeBlankState(); }, - updateNewListDropdown (listId) { + updateNewListDropdown(listId) { $(`.js-board-list-${listId}`).removeClass('is-active'); }, - shouldAddBlankState () { + shouldAddBlankState() { // Decide whether to add the blank state - return !(this.state.lists.filter(list => list.type !== 'backlog' && list.type !== 'closed')[0]); + return !this.state.lists.filter(list => list.type !== 'backlog' && list.type !== 'closed')[0]; }, - addBlankState () { + addBlankState() { if (!this.shouldAddBlankState() || this.welcomeIsHidden() || this.disabled) return; this.addList({ id: 'blank', list_type: 'blank', title: 'Welcome to your Issue Board!', - position: 0 + position: 0, }); this.state.lists = _.sortBy(this.state.lists, 'position'); }, - removeBlankState () { + removeBlankState() { this.removeList('blank'); Cookies.set('issue_board_welcome_hidden', 'true', { expires: 365 * 10, - path: '' + path: '', }); }, - welcomeIsHidden () { + welcomeIsHidden() { return Cookies.get('issue_board_welcome_hidden') === 'true'; }, - removeList (id, type = 'blank') { + removeList(id, type = 'blank') { const list = this.findList('id', id, type); if (!list) return; this.state.lists = this.state.lists.filter(list => list.id !== id); }, - moveList (listFrom, orderLists) { + moveList(listFrom, orderLists) { orderLists.forEach((id, i) => { const list = this.findList('id', parseInt(id, 10)); @@ -95,22 +95,25 @@ const boardsStore = { }); listFrom.update(); }, - moveIssueToList (listFrom, listTo, issue, newIndex) { + moveIssueToList(listFrom, listTo, issue, newIndex) { const issueTo = listTo.findIssue(issue.id); const issueLists = issue.getLists(); const listLabels = issueLists.map(listIssue => listIssue.label); if (!issueTo) { // Check if target list assignee is already present in this issue - if ((listTo.type === 'assignee' && listFrom.type === 'assignee') && - issue.findAssignee(listTo.assignee)) { + if ( + listTo.type === 'assignee' && + listFrom.type === 'assignee' && + issue.findAssignee(listTo.assignee) + ) { const targetIssue = listTo.findIssue(issue.id); targetIssue.removeAssignee(listFrom.assignee); } else if (listTo.type === 'milestone') { const currentMilestone = issue.milestone; const currentLists = this.state.lists - .filter(list => (list.type === 'milestone' && list.id !== listTo.id)) - .filter(list => list.issues.some(listIssue => issue.id === listIssue.id)); + .filter(list => list.type === 'milestone' && list.id !== listTo.id) + .filter(list => list.issues.some(listIssue => issue.id === listIssue.id)); issue.removeMilestone(currentMilestone); issue.addMilestone(listTo.milestone); @@ -126,7 +129,7 @@ const boardsStore = { } if (listTo.type === 'closed' && listFrom.type !== 'backlog') { - issueLists.forEach((list) => { + issueLists.forEach(list => { list.removeIssue(issue); }); issue.removeLabels(listLabels); @@ -144,26 +147,28 @@ const boardsStore = { return ( (listTo.type !== 'label' && listFrom.type === 'assignee') || (listTo.type !== 'assignee' && listFrom.type === 'label') || - (listFrom.type === 'backlog') + listFrom.type === 'backlog' ); }, - moveIssueInList (list, issue, oldIndex, newIndex, idArray) { + moveIssueInList(list, issue, oldIndex, newIndex, idArray) { const beforeId = parseInt(idArray[newIndex - 1], 10) || null; const afterId = parseInt(idArray[newIndex + 1], 10) || null; list.moveIssue(issue, oldIndex, newIndex, beforeId, afterId); }, - findList (key, val, type = 'label') { - const filteredList = this.state.lists.filter((list) => { - const byType = type ? (list.type === type) || (list.type === 'assignee') || (list.type === 'milestone') : true; + findList(key, val, type = 'label') { + const filteredList = this.state.lists.filter(list => { + const byType = type + ? list.type === type || list.type === 'assignee' || list.type === 'milestone' + : true; return list[key] === val && byType; }); return filteredList[0]; }, - updateFiltersUrl () { + updateFiltersUrl() { window.history.pushState(null, null, `?${this.filter.path}`); - } + }, }; // hacks added in order to allow milestone_select to function properly diff --git a/app/assets/javascripts/boards/stores/modal_store.js b/app/assets/javascripts/boards/stores/modal_store.js index 0d9ac367a70..b7228bf7bf5 100644 --- a/app/assets/javascripts/boards/stores/modal_store.js +++ b/app/assets/javascripts/boards/stores/modal_store.js @@ -40,7 +40,7 @@ class ModalStore { toggleAll() { const select = this.selectedCount() !== this.store.issues.length; - this.store.issues.forEach((issue) => { + this.store.issues.forEach(issue => { const issueUpdate = issue; if (issueUpdate.selected !== select) { @@ -69,13 +69,14 @@ class ModalStore { removeSelectedIssue(issue, forcePurge = false) { if (this.store.activeTab === 'all' || forcePurge) { - this.store.selectedIssues = this.store.selectedIssues - .filter(fIssue => fIssue.id !== issue.id); + this.store.selectedIssues = this.store.selectedIssues.filter( + fIssue => fIssue.id !== issue.id, + ); } } purgeUnselectedIssues() { - this.store.selectedIssues.forEach((issue) => { + this.store.selectedIssues.forEach(issue => { if (!issue.selected) { this.removeSelectedIssue(issue, true); } @@ -87,8 +88,7 @@ class ModalStore { } findSelectedIssue(issue) { - return this.store.selectedIssues - .filter(filteredIssue => filteredIssue.id === issue.id)[0]; + return this.store.selectedIssues.filter(filteredIssue => filteredIssue.id === issue.id)[0]; } } diff --git a/app/assets/javascripts/clusters/components/applications.vue b/app/assets/javascripts/clusters/components/applications.vue index 6e7b5eb5526..6d7f45a35d8 100644 --- a/app/assets/javascripts/clusters/components/applications.vue +++ b/app/assets/javascripts/clusters/components/applications.vue @@ -1,6 +1,6 @@ <script> import _ from 'underscore'; -import helmInstallIllustration from '@gitlab-org/gitlab-svgs/dist/illustrations/kubernetes-installation.svg'; +import helmInstallIllustration from '@gitlab/svgs/dist/illustrations/kubernetes-installation.svg'; import elasticsearchLogo from 'images/cluster_app_logos/elasticsearch.png'; import gitlabLogo from 'images/cluster_app_logos/gitlab.png'; import helmLogo from 'images/cluster_app_logos/helm.png'; diff --git a/app/assets/javascripts/commit/pipelines/pipelines_table.vue b/app/assets/javascripts/commit/pipelines/pipelines_table.vue index a2aa3d197e3..82532539c9c 100644 --- a/app/assets/javascripts/commit/pipelines/pipelines_table.vue +++ b/app/assets/javascripts/commit/pipelines/pipelines_table.vue @@ -2,9 +2,15 @@ import PipelinesService from '../../pipelines/services/pipelines_service'; import PipelineStore from '../../pipelines/stores/pipelines_store'; import pipelinesMixin from '../../pipelines/mixins/pipelines'; +import TablePagination from '../../vue_shared/components/table_pagination.vue'; +import { getParameterByName } from '../../lib/utils/common_utils'; +import CIPaginationMixin from '../../vue_shared/mixins/ci_pagination_api_mixin'; export default { - mixins: [pipelinesMixin], + components: { + TablePagination, + }, + mixins: [pipelinesMixin, CIPaginationMixin], props: { endpoint: { type: String, @@ -35,6 +41,8 @@ export default { return { store, state: store.state, + page: getParameterByName('page') || '1', + requestData: {}, }; }, @@ -48,11 +56,14 @@ export default { }, created() { this.service = new PipelinesService(this.endpoint); + this.requestData = { page: this.page }; }, methods: { successCallback(resp) { // depending of the endpoint the response can either bring a `pipelines` key or not. const pipelines = resp.data.pipelines || resp.data; + + this.store.storePagination(resp.headers); this.setCommonData(pipelines); const updatePipelinesEvent = new CustomEvent('update-pipelines-count', { @@ -97,5 +108,11 @@ export default { :view-type="viewType" /> </div> + + <table-pagination + v-if="shouldRenderPagination" + :change="onChangePage" + :page-info="state.pageInfo" + /> </div> </template> diff --git a/app/assets/javascripts/commons/gitlab_ui.js b/app/assets/javascripts/commons/gitlab_ui.js index f60665577fe..6c18a0fd390 100644 --- a/app/assets/javascripts/commons/gitlab_ui.js +++ b/app/assets/javascripts/commons/gitlab_ui.js @@ -1,17 +1,4 @@ import Vue from 'vue'; -import { - GlPagination, - GlProgressBar, - GlModal, - GlLoadingIcon, - GlModalDirective, - GlTooltipDirective, -} from '@gitlab-org/gitlab-ui'; +import { GlLoadingIcon } from '@gitlab-org/gitlab-ui'; -Vue.component('gl-pagination', GlPagination); -Vue.component('gl-progress-bar', GlProgressBar); -Vue.component('gl-ui-modal', GlModal); Vue.component('gl-loading-icon', GlLoadingIcon); - -Vue.directive('gl-modal', GlModalDirective); -Vue.directive('gl-tooltip', GlTooltipDirective); diff --git a/app/assets/javascripts/diffs/components/diff_gutter_avatars.vue b/app/assets/javascripts/diffs/components/diff_gutter_avatars.vue index 1b59777f901..254bc235691 100644 --- a/app/assets/javascripts/diffs/components/diff_gutter_avatars.vue +++ b/app/assets/javascripts/diffs/components/diff_gutter_avatars.vue @@ -3,6 +3,7 @@ import { mapActions } from 'vuex'; import Icon from '~/vue_shared/components/icon.vue'; import { pluralize, truncate } from '~/lib/utils/text_utility'; import UserAvatarImage from '~/vue_shared/components/user_avatar/user_avatar_image.vue'; +import { GlTooltipDirective } from '@gitlab-org/gitlab-ui'; import { COUNT_OF_AVATARS_IN_GUTTER, LENGTH_OF_AVATAR_TOOLTIP } from '../constants'; export default { @@ -10,6 +11,9 @@ export default { Icon, UserAvatarImage, }, + directives: { + GlTooltip: GlTooltipDirective, + }, props: { discussions: { type: Array, diff --git a/app/assets/javascripts/diffs/components/diff_line_gutter_content.vue b/app/assets/javascripts/diffs/components/diff_line_gutter_content.vue index 6eff3013dcd..f4a9be19496 100644 --- a/app/assets/javascripts/diffs/components/diff_line_gutter_content.vue +++ b/app/assets/javascripts/diffs/components/diff_line_gutter_content.vue @@ -167,7 +167,7 @@ export default { <button v-if="shouldShowCommentButton" type="button" - class="add-diff-note js-add-diff-note-button" + class="add-diff-note js-add-diff-note-button qa-diff-comment" title="Add a comment to this line" @click="handleCommentButton" > diff --git a/app/assets/javascripts/diffs/components/inline_diff_table_row.vue b/app/assets/javascripts/diffs/components/inline_diff_table_row.vue index 62fa34e835a..542acd3d930 100644 --- a/app/assets/javascripts/diffs/components/inline_diff_table_row.vue +++ b/app/assets/javascripts/diffs/components/inline_diff_table_row.vue @@ -102,7 +102,7 @@ export default { :line-type="newLineType" :is-bottom="isBottom" :is-hover="isHover" - class="diff-line-num new_line" + class="diff-line-num new_line qa-new-diff-line" /> <td :class="line.type" diff --git a/app/assets/javascripts/diffs/components/tree_list.vue b/app/assets/javascripts/diffs/components/tree_list.vue index 96e7bd63183..91052b303a6 100644 --- a/app/assets/javascripts/diffs/components/tree_list.vue +++ b/app/assets/javascripts/diffs/components/tree_list.vue @@ -18,8 +18,8 @@ export default { }, data() { const treeListStored = localStorage.getItem(treeListStorageKey); - const renderTreeList = treeListStored !== null ? - convertPermissionToBoolean(treeListStored) : true; + const renderTreeList = + treeListStored !== null ? convertPermissionToBoolean(treeListStored) : true; return { search: '', diff --git a/app/assets/javascripts/dirty_submit/dirty_submit_form.js b/app/assets/javascripts/dirty_submit/dirty_submit_form.js index 5bea47f23c5..d8d0fa1fac4 100644 --- a/app/assets/javascripts/dirty_submit/dirty_submit_form.js +++ b/app/assets/javascripts/dirty_submit/dirty_submit_form.js @@ -31,7 +31,7 @@ class DirtySubmitForm { updateDirtyInput(event) { const input = event.target; - if (!input.dataset.dirtySubmitOriginalValue) return; + if (!input.dataset.isDirtySubmitInput) return; this.updateDirtyInputs(input); this.toggleSubmission(); @@ -65,6 +65,7 @@ class DirtySubmitForm { } static initInput(element) { + element.dataset.isDirtySubmitInput = true; element.dataset.dirtySubmitOriginalValue = DirtySubmitForm.inputCurrentValue(element); } diff --git a/app/assets/javascripts/environments/components/environment_actions.vue b/app/assets/javascripts/environments/components/environment_actions.vue index 2bc168a6b02..0a3ae384afa 100644 --- a/app/assets/javascripts/environments/components/environment_actions.vue +++ b/app/assets/javascripts/environments/components/environment_actions.vue @@ -1,4 +1,6 @@ <script> +import { s__, sprintf } from '~/locale'; +import { formatTime } from '~/lib/utils/datetime_utility'; import Icon from '~/vue_shared/components/icon.vue'; import eventHub from '../event_hub'; import tooltip from '../../vue_shared/directives/tooltip'; @@ -28,10 +30,24 @@ export default { }, }, methods: { - onClickAction(endpoint) { + onClickAction(action) { + if (action.scheduledAt) { + const confirmationMessage = sprintf( + s__( + "DelayedJobs|Are you sure you want to run %{jobName} immediately? Otherwise this job will run automatically after it's timer finishes.", + ), + { jobName: action.name }, + ); + // https://gitlab.com/gitlab-org/gitlab-ce/issues/52156 + // eslint-disable-next-line no-alert + if (!window.confirm(confirmationMessage)) { + return; + } + } + this.isLoading = true; - eventHub.$emit('postAction', { endpoint }); + eventHub.$emit('postAction', { endpoint: action.playPath }); }, isActionDisabled(action) { @@ -41,6 +57,11 @@ export default { return !action.playable; }, + + remainingTime(action) { + const remainingMilliseconds = new Date(action.scheduledAt).getTime() - Date.now(); + return formatTime(Math.max(0, remainingMilliseconds)); + }, }, }; </script> @@ -54,7 +75,7 @@ export default { :aria-label="title" :disabled="isLoading" type="button" - class="dropdown btn btn-default dropdown-new js-dropdown-play-icon-container" + class="dropdown btn btn-default dropdown-new js-environment-actions-dropdown" data-container="body" data-toggle="dropdown" > @@ -75,12 +96,19 @@ export default { :class="{ disabled: isActionDisabled(action) }" :disabled="isActionDisabled(action)" type="button" - class="js-manual-action-link no-btn btn" - @click="onClickAction(action.play_path)" + class="js-manual-action-link no-btn btn d-flex align-items-center" + @click="onClickAction(action)" > - <span> + <span class="flex-fill"> {{ action.name }} </span> + <span + v-if="action.scheduledAt" + class="text-secondary" + > + <icon name="clock" /> + {{ remainingTime(action) }} + </span> </button> </li> </ul> diff --git a/app/assets/javascripts/environments/components/environment_item.vue b/app/assets/javascripts/environments/components/environment_item.vue index bb9c139727e..41f59447905 100644 --- a/app/assets/javascripts/environments/components/environment_item.vue +++ b/app/assets/javascripts/environments/components/environment_item.vue @@ -13,9 +13,10 @@ import TerminalButtonComponent from './environment_terminal_button.vue'; import MonitoringButtonComponent from './environment_monitoring.vue'; import CommitComponent from '../../vue_shared/components/commit.vue'; import eventHub from '../event_hub'; +import { convertObjectPropsToCamelCase } from '~/lib/utils/common_utils'; /** - * Envrionment Item Component + * Environment Item Component * * Renders a table row for each environment. */ @@ -60,7 +61,7 @@ export default { computed: { /** - * Verifies if `last_deployment` key exists in the current Envrionment. + * Verifies if `last_deployment` key exists in the current Environment. * This key is required to render most of the html - this method works has * an helper. * @@ -74,21 +75,6 @@ export default { }, /** - * Verifies is the given environment has manual actions. - * Used to verify if we should render them or nor. - * - * @returns {Boolean|Undefined} - */ - hasManualActions() { - return ( - this.model && - this.model.last_deployment && - this.model.last_deployment.manual_actions && - this.model.last_deployment.manual_actions.length > 0 - ); - }, - - /** * Checkes whether the environment is protected. * (`is_protected` currently only set in EE) * @@ -154,23 +140,20 @@ export default { return ''; }, - /** - * Returns the manual actions with the name parsed. - * - * @returns {Array.<Object>|Undefined} - */ - manualActions() { - if (this.hasManualActions) { - return this.model.last_deployment.manual_actions.map(action => { - const parsedAction = { - name: humanize(action.name), - play_path: action.play_path, - playable: action.playable, - }; - return parsedAction; - }); + actions() { + if (!this.model || !this.model.last_deployment || !this.canCreateDeployment) { + return []; } - return []; + + const { manualActions, scheduledActions } = convertObjectPropsToCamelCase( + this.model.last_deployment, + { deep: true }, + ); + const combinedActions = (manualActions || []).concat(scheduledActions || []); + return combinedActions.map(action => ({ + ...action, + name: humanize(action.name), + })); }, /** @@ -443,7 +426,7 @@ export default { displayEnvironmentActions() { return ( - this.hasManualActions || + this.actions.length > 0 || this.externalURL || this.monitoringUrl || this.canStopEnvironment || @@ -619,8 +602,8 @@ export default { /> <actions-component - v-if="hasManualActions && canCreateDeployment" - :actions="manualActions" + v-if="actions.length > 0" + :actions="actions" /> <terminal-button-component diff --git a/app/assets/javascripts/environments/components/environments_app.vue b/app/assets/javascripts/environments/components/environments_app.vue index e2ecf426e64..557b2062c64 100644 --- a/app/assets/javascripts/environments/components/environments_app.vue +++ b/app/assets/javascripts/environments/components/environments_app.vue @@ -1,94 +1,92 @@ <script> - import Flash from '../../flash'; - import { s__ } from '../../locale'; - import emptyState from './empty_state.vue'; - import eventHub from '../event_hub'; - import environmentsMixin from '../mixins/environments_mixin'; - import CIPaginationMixin from '../../vue_shared/mixins/ci_pagination_api_mixin'; - import StopEnvironmentModal from './stop_environment_modal.vue'; +import Flash from '../../flash'; +import { s__ } from '../../locale'; +import emptyState from './empty_state.vue'; +import eventHub from '../event_hub'; +import environmentsMixin from '../mixins/environments_mixin'; +import CIPaginationMixin from '../../vue_shared/mixins/ci_pagination_api_mixin'; +import StopEnvironmentModal from './stop_environment_modal.vue'; - export default { - components: { - emptyState, - StopEnvironmentModal, - }, +export default { + components: { + emptyState, + StopEnvironmentModal, + }, - mixins: [ - CIPaginationMixin, - environmentsMixin, - ], + mixins: [CIPaginationMixin, environmentsMixin], - props: { - endpoint: { - type: String, - required: true, - }, - canCreateEnvironment: { - type: Boolean, - required: true, - }, - canCreateDeployment: { - type: Boolean, - required: true, - }, - canReadEnvironment: { - type: Boolean, - required: true, - }, - cssContainerClass: { - type: String, - required: true, - }, - newEnvironmentPath: { - type: String, - required: true, - }, - helpPagePath: { - type: String, - required: true, - }, + props: { + endpoint: { + type: String, + required: true, }, - - created() { - eventHub.$on('toggleFolder', this.toggleFolder); + canCreateEnvironment: { + type: Boolean, + required: true, }, - - beforeDestroy() { - eventHub.$off('toggleFolder'); + canCreateDeployment: { + type: Boolean, + required: true, + }, + canReadEnvironment: { + type: Boolean, + required: true, + }, + cssContainerClass: { + type: String, + required: true, + }, + newEnvironmentPath: { + type: String, + required: true, }, + helpPagePath: { + type: String, + required: true, + }, + }, + + created() { + eventHub.$on('toggleFolder', this.toggleFolder); + }, - methods: { - toggleFolder(folder) { - this.store.toggleFolder(folder); + beforeDestroy() { + eventHub.$off('toggleFolder'); + }, - if (!folder.isOpen) { - this.fetchChildEnvironments(folder, true); - } - }, + methods: { + toggleFolder(folder) { + this.store.toggleFolder(folder); - fetchChildEnvironments(folder, showLoader = false) { - this.store.updateEnvironmentProp(folder, 'isLoadingFolderContent', showLoader); + if (!folder.isOpen) { + this.fetchChildEnvironments(folder, true); + } + }, - this.service.getFolderContent(folder.folder_path) - .then(response => this.store.setfolderContent(folder, response.data.environments)) - .then(() => this.store.updateEnvironmentProp(folder, 'isLoadingFolderContent', false)) - .catch(() => { - Flash(s__('Environments|An error occurred while fetching the environments.')); - this.store.updateEnvironmentProp(folder, 'isLoadingFolderContent', false); - }); - }, + fetchChildEnvironments(folder, showLoader = false) { + this.store.updateEnvironmentProp(folder, 'isLoadingFolderContent', showLoader); + + this.service + .getFolderContent(folder.folder_path) + .then(response => this.store.setfolderContent(folder, response.data.environments)) + .then(() => this.store.updateEnvironmentProp(folder, 'isLoadingFolderContent', false)) + .catch(() => { + Flash(s__('Environments|An error occurred while fetching the environments.')); + this.store.updateEnvironmentProp(folder, 'isLoadingFolderContent', false); + }); + }, - successCallback(resp) { - this.saveData(resp); + successCallback(resp) { + this.saveData(resp); - // We need to verify if any folder is open to also update it - const openFolders = this.store.getOpenFolders(); - if (openFolders.length) { - openFolders.forEach(folder => this.fetchChildEnvironments(folder)); - } - }, + // We need to verify if any folder is open to also update it + const openFolders = this.store.getOpenFolders(); + if (openFolders.length) { + openFolders.forEach(folder => this.fetchChildEnvironments(folder)); + } }, - }; + }, +}; </script> <template> <div :class="cssContainerClass"> diff --git a/app/assets/javascripts/environments/stores/environments_store.js b/app/assets/javascripts/environments/stores/environments_store.js index 5ce9225a4bb..5808a2d4afa 100644 --- a/app/assets/javascripts/environments/stores/environments_store.js +++ b/app/assets/javascripts/environments/stores/environments_store.js @@ -34,14 +34,14 @@ export default class EnvironmentsStore { * @returns {Array} */ storeEnvironments(environments = []) { - const filteredEnvironments = environments.map((env) => { - const oldEnvironmentState = this.state.environments - .find((element) => { - if (env.latest) { - return element.id === env.latest.id; - } - return element.id === env.id; - }) || {}; + const filteredEnvironments = environments.map(env => { + const oldEnvironmentState = + this.state.environments.find(element => { + if (env.latest) { + return element.id === env.latest.id; + } + return element.id === env.id; + }) || {}; let filtered = {}; @@ -101,11 +101,11 @@ export default class EnvironmentsStore { } /** - * Toggles folder open property for the given folder. - * - * @param {Object} folder - * @return {Array} - */ + * Toggles folder open property for the given folder. + * + * @param {Object} folder + * @return {Array} + */ toggleFolder(folder) { return this.updateEnvironmentProp(folder, 'isOpen', !folder.isOpen); } @@ -119,7 +119,7 @@ export default class EnvironmentsStore { * @return {Object} */ setfolderContent(folder, environments) { - const updatedEnvironments = environments.map((env) => { + const updatedEnvironments = environments.map(env => { let updated = env; if (env.latest) { @@ -148,7 +148,7 @@ export default class EnvironmentsStore { updateEnvironmentProp(environment, prop, newValue) { const { environments } = this.state; - const updatedEnvironments = environments.map((env) => { + const updatedEnvironments = environments.map(env => { const updateEnv = Object.assign({}, env); if (env.id === environment.id) { updateEnv[prop] = newValue; diff --git a/app/assets/javascripts/filtered_search/dropdown_user.js b/app/assets/javascripts/filtered_search/dropdown_user.js index d36f38a70b5..d5027590bb7 100644 --- a/app/assets/javascripts/filtered_search/dropdown_user.js +++ b/app/assets/javascripts/filtered_search/dropdown_user.js @@ -39,8 +39,9 @@ export default class DropdownUser extends FilteredSearchDropdown { } itemClicked(e) { - super.itemClicked(e, - selected => selected.querySelector('.dropdown-light-content').innerText.trim()); + super.itemClicked(e, selected => + selected.querySelector('.dropdown-light-content').innerText.trim(), + ); } renderContent(forceShowList = false) { @@ -68,7 +69,7 @@ export default class DropdownUser extends FilteredSearchDropdown { // Removes the first character if it is a quotation so that we can search // with multiple words - if (value[0] === '"' || value[0] === '\'') { + if (value[0] === '"' || value[0] === "'") { value = value.slice(1); } diff --git a/app/assets/javascripts/filtered_search/filtered_search_dropdown.js b/app/assets/javascripts/filtered_search/filtered_search_dropdown.js index 4eb67ff7649..146d3ba963c 100644 --- a/app/assets/javascripts/filtered_search/filtered_search_dropdown.js +++ b/app/assets/javascripts/filtered_search/filtered_search_dropdown.js @@ -85,7 +85,7 @@ export default class FilteredSearchDropdown { } dispatchInputEvent() { - // Propogate input change to FilteredSearchDropdownManager + // Propagate input change to FilteredSearchDropdownManager // so that it can determine which dropdowns to open this.input.dispatchEvent( new CustomEvent('input', { diff --git a/app/assets/javascripts/filtered_search/filtered_search_dropdown_manager.js b/app/assets/javascripts/filtered_search/filtered_search_dropdown_manager.js index cd3d532c958..57ec6603d80 100644 --- a/app/assets/javascripts/filtered_search/filtered_search_dropdown_manager.js +++ b/app/assets/javascripts/filtered_search/filtered_search_dropdown_manager.js @@ -108,7 +108,7 @@ export default class FilteredSearchDropdownManager { }, }; - supportedTokens.forEach((type) => { + supportedTokens.forEach(type => { if (availableMappings[type]) { allowedMappings[type] = availableMappings[type]; } @@ -142,10 +142,7 @@ export default class FilteredSearchDropdownManager { } static addWordToInput(tokenName, tokenValue = '', clicked = false, options = {}) { - const { - uppercaseTokenName = false, - capitalizeTokenValue = false, - } = options; + const { uppercaseTokenName = false, capitalizeTokenValue = false } = options; const input = FilteredSearchContainer.container.querySelector('.filtered-search'); FilteredSearchVisualTokens.addFilterVisualToken(tokenName, tokenValue, { uppercaseTokenName, @@ -164,13 +161,16 @@ export default class FilteredSearchDropdownManager { updateDropdownOffset(key) { // Always align dropdown with the input field - let offset = this.filteredSearchInput.getBoundingClientRect().left - this.container.querySelector('.scroll-container').getBoundingClientRect().left; + let offset = + this.filteredSearchInput.getBoundingClientRect().left - + this.container.querySelector('.scroll-container').getBoundingClientRect().left; const maxInputWidth = 240; const currentDropdownWidth = this.mapping[key].element.clientWidth || maxInputWidth; // Make sure offset never exceeds the input container - const offsetMaxWidth = this.container.querySelector('.scroll-container').clientWidth - currentDropdownWidth; + const offsetMaxWidth = + this.container.querySelector('.scroll-container').clientWidth - currentDropdownWidth; if (offsetMaxWidth < offset) { offset = offsetMaxWidth; } @@ -196,8 +196,7 @@ export default class FilteredSearchDropdownManager { const glArguments = Object.assign({}, defaultArguments, extraArguments); // Passing glArguments to `new glClass(<arguments>)` - mappingKey.reference = - new (Function.prototype.bind.apply(glClass, [null, glArguments]))(); + mappingKey.reference = new (Function.prototype.bind.apply(glClass, [null, glArguments]))(); } if (firstLoad) { @@ -224,8 +223,8 @@ export default class FilteredSearchDropdownManager { } const match = this.filteredSearchTokenKeys.searchByKey(dropdownName.toLowerCase()); - const shouldOpenFilterDropdown = match && this.currentDropdown !== match.key - && this.mapping[match.key]; + const shouldOpenFilterDropdown = + match && this.currentDropdown !== match.key && this.mapping[match.key]; const shouldOpenHintDropdown = !match && this.currentDropdown !== 'hint'; if (shouldOpenFilterDropdown || shouldOpenHintDropdown) { @@ -236,8 +235,10 @@ export default class FilteredSearchDropdownManager { setDropdown() { const query = DropdownUtils.getSearchQuery(true); - const { lastToken, searchToken } = - this.tokenizer.processTokens(query, this.filteredSearchTokenKeys.getKeys()); + const { lastToken, searchToken } = this.tokenizer.processTokens( + query, + this.filteredSearchTokenKeys.getKeys(), + ); if (this.currentDropdown) { this.updateCurrentDropdownOffset(); diff --git a/app/assets/javascripts/filtered_search/filtered_search_manager.js b/app/assets/javascripts/filtered_search/filtered_search_manager.js index 54533ebb70d..4a2af02b40a 100644 --- a/app/assets/javascripts/filtered_search/filtered_search_manager.js +++ b/app/assets/javascripts/filtered_search/filtered_search_manager.js @@ -1,8 +1,5 @@ import _ from 'underscore'; -import { - getParameterByName, - getUrlParamsArray, -} from '~/lib/utils/common_utils'; +import { getParameterByName, getUrlParamsArray } from '~/lib/utils/common_utils'; import IssuableFilteredSearchTokenKeys from '~/filtered_search/issuable_filtered_search_token_keys'; import { visitUrl } from '../lib/utils/url_utility'; import Flash from '../flash'; @@ -48,24 +45,28 @@ export default class FilteredSearchManager { isLocalStorageAvailable: RecentSearchesService.isAvailable(), allowedKeys: this.filteredSearchTokenKeys.getKeys(), }); - this.searchHistoryDropdownElement = document.querySelector('.js-filtered-search-history-dropdown'); - const fullPath = this.searchHistoryDropdownElement ? - this.searchHistoryDropdownElement.dataset.fullPath : 'project'; + this.searchHistoryDropdownElement = document.querySelector( + '.js-filtered-search-history-dropdown', + ); + const fullPath = this.searchHistoryDropdownElement + ? this.searchHistoryDropdownElement.dataset.fullPath + : 'project'; const recentSearchesKey = `${fullPath}-${this.recentsStorageKeyNames[this.page]}`; this.recentSearchesService = new RecentSearchesService(recentSearchesKey); } setup() { // Fetch recent searches from localStorage - this.fetchingRecentSearchesPromise = this.recentSearchesService.fetch() - .catch((error) => { + this.fetchingRecentSearchesPromise = this.recentSearchesService + .fetch() + .catch(error => { if (error.name === 'RecentSearchesServiceError') return undefined; // eslint-disable-next-line no-new new Flash('An error occurred while parsing recent searches'); // Gracefully fail to empty array return []; }) - .then((searches) => { + .then(searches => { if (!searches) { return; } @@ -120,7 +121,7 @@ export default class FilteredSearchManager { if (this.stateFilters) { this.searchStateWrapper = this.searchState.bind(this); - this.applyToStateFilters((filterEl) => { + this.applyToStateFilters(filterEl => { filterEl.addEventListener('click', this.searchStateWrapper); }); } @@ -128,14 +129,14 @@ export default class FilteredSearchManager { unbindStateEvents() { if (this.stateFilters) { - this.applyToStateFilters((filterEl) => { + this.applyToStateFilters(filterEl => { filterEl.removeEventListener('click', this.searchStateWrapper); }); } } applyToStateFilters(callback) { - this.stateFilters.querySelectorAll('a[data-state]').forEach((filterEl) => { + this.stateFilters.querySelectorAll('a[data-state]').forEach(filterEl => { if (this.states.indexOf(filterEl.dataset.state) > -1) { callback(filterEl); } @@ -207,7 +208,7 @@ export default class FilteredSearchManager { let backspaceCount = 0; // closure for keeping track of the number of backspace keystrokes - return (e) => { + return e => { // 8 = Backspace Key // 46 = Delete Key if (e.keyCode === 8 || e.keyCode === 46) { @@ -274,8 +275,12 @@ export default class FilteredSearchManager { const isElementInDynamicFilterDropdown = e.target.closest('.filter-dropdown') !== null; const isElementInStaticFilterDropdown = e.target.closest('ul[data-dropdown]') !== null; - if (!isElementInFilteredSearch && !isElementInDynamicFilterDropdown && - !isElementInStaticFilterDropdown && inputContainer) { + if ( + !isElementInFilteredSearch && + !isElementInDynamicFilterDropdown && + !isElementInStaticFilterDropdown && + inputContainer + ) { inputContainer.classList.remove('focus'); } } @@ -368,7 +373,7 @@ export default class FilteredSearchManager { const removeElements = []; - [].forEach.call(this.tokensContainer.children, (t) => { + [].forEach.call(this.tokensContainer.children, t => { let canClearToken = t.classList.contains('js-visual-token'); if (canClearToken) { @@ -381,7 +386,7 @@ export default class FilteredSearchManager { } }); - removeElements.forEach((el) => { + removeElements.forEach(el => { el.parentElement.removeChild(el); }); @@ -397,13 +402,14 @@ export default class FilteredSearchManager { handleInputVisualToken() { const input = this.filteredSearchInput; - const { tokens, searchToken } - = this.tokenizer.processTokens(input.value, this.filteredSearchTokenKeys.getKeys()); - const { isLastVisualTokenValid } - = FilteredSearchVisualTokens.getLastVisualTokenBeforeInput(); + const { tokens, searchToken } = this.tokenizer.processTokens( + input.value, + this.filteredSearchTokenKeys.getKeys(), + ); + const { isLastVisualTokenValid } = FilteredSearchVisualTokens.getLastVisualTokenBeforeInput(); if (isLastVisualTokenValid) { - tokens.forEach((t) => { + tokens.forEach(t => { input.value = input.value.replace(`${t.key}:${t.symbol}${t.value}`, ''); FilteredSearchVisualTokens.addFilterVisualToken(t.key, `${t.symbol}${t.value}`, { uppercaseTokenName: this.filteredSearchTokenKeys.shouldUppercaseTokenName(t.key), @@ -453,15 +459,17 @@ export default class FilteredSearchManager { saveCurrentSearchQuery() { // Don't save before we have fetched the already saved searches - this.fetchingRecentSearchesPromise.then(() => { - const searchQuery = DropdownUtils.getSearchQuery(); - if (searchQuery.length > 0) { - const resultantSearches = this.recentSearchesStore.addRecentSearch(searchQuery); - this.recentSearchesService.save(resultantSearches); - } - }).catch(() => { - // https://gitlab.com/gitlab-org/gitlab-ce/issues/30821 - }); + this.fetchingRecentSearchesPromise + .then(() => { + const searchQuery = DropdownUtils.getSearchQuery(); + if (searchQuery.length > 0) { + const resultantSearches = this.recentSearchesStore.addRecentSearch(searchQuery); + this.recentSearchesService.save(resultantSearches); + } + }) + .catch(() => { + // https://gitlab.com/gitlab-org/gitlab-ce/issues/30821 + }); } // allows for modifying params array when a param can't be included in the URL (e.g. Service Desk) @@ -475,7 +483,7 @@ export default class FilteredSearchManager { const usernameParams = this.getUsernameParams(); let hasFilteredSearch = false; - params.forEach((p) => { + params.forEach(p => { const split = p.split('='); const keyParam = decodeURIComponent(split[0]); const value = split[1]; @@ -486,11 +494,9 @@ export default class FilteredSearchManager { if (condition) { hasFilteredSearch = true; const canEdit = this.canEdit && this.canEdit(condition.tokenKey); - FilteredSearchVisualTokens.addFilterVisualToken( - condition.tokenKey, - condition.value, - { canEdit }, - ); + FilteredSearchVisualTokens.addFilterVisualToken(condition.tokenKey, condition.value, { + canEdit, + }); } else { // Sanitize value since URL converts spaces into + // Replace before decode so that we know what was originally + versus the encoded + @@ -510,7 +516,7 @@ export default class FilteredSearchManager { if (sanitizedValue.indexOf(' ') !== -1) { // Prefer ", but use ' if required - quotationsToUse = sanitizedValue.indexOf('"') === -1 ? '"' : '\''; + quotationsToUse = sanitizedValue.indexOf('"') === -1 ? '"' : "'"; } hasFilteredSearch = true; @@ -531,7 +537,9 @@ export default class FilteredSearchManager { hasFilteredSearch = true; const tokenName = 'assignee'; const canEdit = this.canEdit && this.canEdit(tokenName); - FilteredSearchVisualTokens.addFilterVisualToken(tokenName, `@${usernameParams[id]}`, { canEdit }); + FilteredSearchVisualTokens.addFilterVisualToken(tokenName, `@${usernameParams[id]}`, { + canEdit, + }); } } else if (!match && keyParam === 'author_id') { const id = parseInt(value, 10); @@ -539,7 +547,9 @@ export default class FilteredSearchManager { hasFilteredSearch = true; const tokenName = 'author'; const canEdit = this.canEdit && this.canEdit(tokenName); - FilteredSearchVisualTokens.addFilterVisualToken(tokenName, `@${usernameParams[id]}`, { canEdit }); + FilteredSearchVisualTokens.addFilterVisualToken(tokenName, `@${usernameParams[id]}`, { + canEdit, + }); } } else if (!match && keyParam === 'search') { hasFilteredSearch = true; @@ -580,9 +590,11 @@ export default class FilteredSearchManager { const currentState = state || getParameterByName('state') || 'opened'; paths.push(`state=${currentState}`); - tokens.forEach((token) => { - const condition = this.filteredSearchTokenKeys - .searchByConditionKeyValue(token.key, token.value.toLowerCase()); + tokens.forEach(token => { + const condition = this.filteredSearchTokenKeys.searchByConditionKeyValue( + token.key, + token.value.toLowerCase(), + ); const tokenConfig = this.filteredSearchTokenKeys.searchByKey(token.key) || {}; const { param } = tokenConfig; @@ -601,8 +613,10 @@ export default class FilteredSearchManager { tokenValue = tokenValue.toLowerCase(); } - if ((tokenValue[0] === '\'' && tokenValue[tokenValue.length - 1] === '\'') || - (tokenValue[0] === '"' && tokenValue[tokenValue.length - 1] === '"')) { + if ( + (tokenValue[0] === "'" && tokenValue[tokenValue.length - 1] === "'") || + (tokenValue[0] === '"' && tokenValue[tokenValue.length - 1] === '"') + ) { tokenValue = tokenValue.slice(1, tokenValue.length - 1); } @@ -613,7 +627,10 @@ export default class FilteredSearchManager { }); if (searchToken) { - const sanitized = searchToken.split(' ').map(t => encodeURIComponent(t)).join('+'); + const sanitized = searchToken + .split(' ') + .map(t => encodeURIComponent(t)) + .join('+'); paths.push(`search=${sanitized}`); } @@ -630,7 +647,7 @@ export default class FilteredSearchManager { const usernamesById = {}; try { const attribute = this.filteredSearchInput.getAttribute('data-username-params'); - JSON.parse(attribute).forEach((user) => { + JSON.parse(attribute).forEach(user => { usernamesById[user.id] = user.username; }); } catch (e) { diff --git a/app/assets/javascripts/filtered_search/issuable_filtered_search_token_keys.js b/app/assets/javascripts/filtered_search/issuable_filtered_search_token_keys.js index e22f542b7bf..bb0ecb8efe7 100644 --- a/app/assets/javascripts/filtered_search/issuable_filtered_search_token_keys.js +++ b/app/assets/javascripts/filtered_search/issuable_filtered_search_token_keys.js @@ -68,12 +68,12 @@ export const conditions = [ value: 'any', }, { - url: 'milestone_title=No+Milestone', + url: 'milestone_title=None', tokenKey: 'milestone', value: 'none', }, { - url: 'milestone_title=Any+Milestone', + url: 'milestone_title=Any', tokenKey: 'milestone', value: 'any', }, @@ -92,6 +92,16 @@ export const conditions = [ tokenKey: 'label', value: 'none', }, + { + url: 'my_reaction_emoji=None', + tokenKey: 'my-reaction', + value: 'none', + }, + { + url: 'my_reaction_emoji=Any', + tokenKey: 'my-reaction', + value: 'any', + }, ]; const IssuableFilteredSearchTokenKeys = new FilteredSearchTokenKeys( diff --git a/app/assets/javascripts/flash.js b/app/assets/javascripts/flash.js index 749c09f897c..c2397842125 100644 --- a/app/assets/javascripts/flash.js +++ b/app/assets/javascripts/flash.js @@ -40,7 +40,9 @@ const createFlashEl = (message, type, isFixedLayout = false) => ` class="flash-${type}" > <div - class="flash-text ${isFixedLayout ? 'container-fluid container-limited limit-container-width' : ''}" + class="flash-text ${ + isFixedLayout ? 'container-fluid container-limited limit-container-width' : '' + }" > ${_.escape(message)} </div> @@ -78,7 +80,9 @@ const createFlash = function createFlash( if (!flashContainer) return null; - const isFixedLayout = navigation ? navigation.parentNode.classList.contains('container-limited') : true; + const isFixedLayout = navigation + ? navigation.parentNode.classList.contains('container-limited') + : true; flashContainer.innerHTML = createFlashEl(message, type, isFixedLayout); diff --git a/app/assets/javascripts/gfm_auto_complete.js b/app/assets/javascripts/gfm_auto_complete.js index 7dd0efd622d..00b3d283570 100644 --- a/app/assets/javascripts/gfm_auto_complete.js +++ b/app/assets/javascripts/gfm_auto_complete.js @@ -94,7 +94,7 @@ class GfmAutoComplete { ...this.getDefaultCallbacks(), beforeSave(commands) { if (GfmAutoComplete.isLoading(commands)) return commands; - return $.map(commands, (c) => { + return $.map(commands, c => { let search = c.name; if (c.aliases.length > 0) { search = `${search} ${c.aliases.join(' ')}`; @@ -167,7 +167,7 @@ class GfmAutoComplete { callbacks: { ...this.getDefaultCallbacks(), beforeSave(members) { - return $.map(members, (m) => { + return $.map(members, m => { let title = ''; if (m.username == null) { return m; @@ -178,7 +178,9 @@ class GfmAutoComplete { } const autoCompleteAvatar = m.avatar_url || m.username.charAt(0).toUpperCase(); - const imgAvatar = `<img src="${m.avatar_url}" alt="${m.username}" class="avatar avatar-inline center s26"/>`; + const imgAvatar = `<img src="${m.avatar_url}" alt="${ + m.username + }" class="avatar avatar-inline center s26"/>`; const txtAvatar = `<div class="avatar center avatar-inline s26">${autoCompleteAvatar}</div>`; return { @@ -211,7 +213,7 @@ class GfmAutoComplete { callbacks: { ...this.getDefaultCallbacks(), beforeSave(issues) { - return $.map(issues, (i) => { + return $.map(issues, i => { if (i.title == null) { return i; } @@ -244,7 +246,7 @@ class GfmAutoComplete { callbacks: { ...this.getDefaultCallbacks(), beforeSave(milestones) { - return $.map(milestones, (m) => { + return $.map(milestones, m => { if (m.title == null) { return m; } @@ -277,7 +279,7 @@ class GfmAutoComplete { callbacks: { ...this.getDefaultCallbacks(), beforeSave(merges) { - return $.map(merges, (m) => { + return $.map(merges, m => { if (m.title == null) { return m; } @@ -324,13 +326,20 @@ class GfmAutoComplete { }, matcher(flag, subtext) { const match = GfmAutoComplete.defaultMatcher(flag, subtext, this.app.controllers); - const subtextNodes = subtext.split(/\n+/g).pop().split(GfmAutoComplete.regexSubtext); + const subtextNodes = subtext + .split(/\n+/g) + .pop() + .split(GfmAutoComplete.regexSubtext); // Check if ~ is followed by '/label', '/relabel' or '/unlabel' commands. - command = subtextNodes.find((node) => { - if (node === LABEL_COMMAND.LABEL || - node === LABEL_COMMAND.RELABEL || - node === LABEL_COMMAND.UNLABEL) { return node; } + command = subtextNodes.find(node => { + if ( + node === LABEL_COMMAND.LABEL || + node === LABEL_COMMAND.RELABEL || + node === LABEL_COMMAND.UNLABEL + ) { + return node; + } return null; }); @@ -380,7 +389,7 @@ class GfmAutoComplete { callbacks: { ...this.getDefaultCallbacks(), beforeSave(snippets) { - return $.map(snippets, (m) => { + return $.map(snippets, m => { if (m.title == null) { return m; } @@ -458,13 +467,17 @@ class GfmAutoComplete { this.loadData($input, at, validEmojiNames); GfmAutoComplete.glEmojiTag = glEmojiTag; }) - .catch(() => { this.isLoadingData[at] = false; }); + .catch(() => { + this.isLoadingData[at] = false; + }); } else if (dataSource) { AjaxCache.retrieve(dataSource, true) - .then((data) => { + .then(data => { this.loadData($input, at, data); }) - .catch(() => { this.isLoadingData[at] = false; }); + .catch(() => { + this.isLoadingData[at] = false; + }); } else { this.isLoadingData[at] = false; } @@ -497,15 +510,16 @@ class GfmAutoComplete { } const loadingState = GfmAutoComplete.defaultLoadingData[0]; - return dataToInspect && - (dataToInspect === loadingState || dataToInspect.name === loadingState); + return dataToInspect && (dataToInspect === loadingState || dataToInspect.name === loadingState); } static defaultMatcher(flag, subtext, controllers) { // The below is taken from At.js source // Tweaked to commands to start without a space only if char before is a non-word character // https://github.com/ichord/At.js - const atSymbolsWithBar = Object.keys(controllers).join('|').replace(/[$]/, '\\$&'); + const atSymbolsWithBar = Object.keys(controllers) + .join('|') + .replace(/[$]/, '\\$&'); const atSymbolsWithoutBar = Object.keys(controllers).join(''); const targetSubtext = subtext.split(GfmAutoComplete.regexSubtext).pop(); const resultantFlag = flag.replace(/[-[\]/{}()*+?.\\^$|]/g, '\\$&'); @@ -513,7 +527,10 @@ class GfmAutoComplete { const accentAChar = decodeURI('%C3%80'); const accentYChar = decodeURI('%C3%BF'); - const regexp = new RegExp(`^(?:\\B|[^a-zA-Z0-9_\`${atSymbolsWithoutBar}]|\\s)${resultantFlag}(?!${atSymbolsWithBar})((?:[A-Za-z${accentAChar}-${accentYChar}0-9_'.+-]|[^\\x00-\\x7a])*)$`, 'gi'); + const regexp = new RegExp( + `^(?:\\B|[^a-zA-Z0-9_\`${atSymbolsWithoutBar}]|\\s)${resultantFlag}(?!${atSymbolsWithBar})((?:[A-Za-z${accentAChar}-${accentYChar}0-9_'.+-]|[^\\x00-\\x7a])*)$`, + 'gi', + ); return regexp.exec(targetSubtext); } @@ -552,8 +569,9 @@ GfmAutoComplete.Members = { template: '<li>${avatarTag} ${username} <small>${title}</small></li>', }; GfmAutoComplete.Labels = { - // eslint-disable-next-line no-template-curly-in-string - template: '<li><span class="dropdown-label-box" style="background: ${color}"></span> ${title}</li>', + template: + // eslint-disable-next-line no-template-curly-in-string + '<li><span class="dropdown-label-box" style="background: ${color}"></span> ${title}</li>', }; // Issues, MergeRequests and Snippets GfmAutoComplete.Issues = { @@ -567,7 +585,8 @@ GfmAutoComplete.Milestones = { template: '<li>${title}</li>', }; GfmAutoComplete.Loading = { - template: '<li style="pointer-events: none;"><i class="fa fa-spinner fa-spin"></i> Loading...</li>', + template: + '<li style="pointer-events: none;"><i class="fa fa-spinner fa-spin"></i> Loading...</li>', }; export default GfmAutoComplete; diff --git a/app/assets/javascripts/gl_field_errors.js b/app/assets/javascripts/gl_field_errors.js index 3764e7ab422..d5d5954ce6a 100644 --- a/app/assets/javascripts/gl_field_errors.js +++ b/app/assets/javascripts/gl_field_errors.js @@ -28,7 +28,7 @@ export default class GlFieldErrors { this.form.on('submit', GlFieldErrors.catchInvalidFormSubmit); } - /* Neccessary to prevent intercept and override invalid form submit + /* Necessary to prevent intercept and override invalid form submit * because Safari & iOS quietly allow form submission when form is invalid * and prevents disabling of invalid submit button by application.js */ diff --git a/app/assets/javascripts/ide/components/ide.vue b/app/assets/javascripts/ide/components/ide.vue index ad6151e3bf6..0a368f6558c 100644 --- a/app/assets/javascripts/ide/components/ide.vue +++ b/app/assets/javascripts/ide/components/ide.vue @@ -43,7 +43,7 @@ export default { 'currentProjectId', 'errorMessage', ]), - ...mapGetters(['activeFile', 'hasChanges', 'someUncommitedChanges', 'isCommitModeActive']), + ...mapGetters(['activeFile', 'hasChanges', 'someUncommittedChanges', 'isCommitModeActive']), }, mounted() { window.onbeforeunload = e => this.onBeforeUnload(e); @@ -63,7 +63,7 @@ export default { onBeforeUnload(e = {}) { const returnValue = __('Are you sure you want to lose unsaved changes?'); - if (!this.someUncommitedChanges) return undefined; + if (!this.someUncommittedChanges) return undefined; Object.assign(e, { returnValue, diff --git a/app/assets/javascripts/ide/components/ide_side_bar.vue b/app/assets/javascripts/ide/components/ide_side_bar.vue index d4c430cd2f3..364ab9426e0 100644 --- a/app/assets/javascripts/ide/components/ide_side_bar.vue +++ b/app/assets/javascripts/ide/components/ide_side_bar.vue @@ -25,11 +25,11 @@ export default { }, computed: { ...mapState(['loading', 'currentActivityView', 'changedFiles', 'stagedFiles', 'lastCommitMsg']), - ...mapGetters(['currentProject', 'someUncommitedChanges']), + ...mapGetters(['currentProject', 'someUncommittedChanges']), showSuccessMessage() { return ( this.currentActivityView === activityBarViews.edit && - (this.lastCommitMsg && !this.someUncommitedChanges) + (this.lastCommitMsg && !this.someUncommittedChanges) ); }, }, diff --git a/app/assets/javascripts/ide/components/repo_commit_section.vue b/app/assets/javascripts/ide/components/repo_commit_section.vue index d3b24c5b793..5e86876c1c1 100644 --- a/app/assets/javascripts/ide/components/repo_commit_section.vue +++ b/app/assets/javascripts/ide/components/repo_commit_section.vue @@ -27,10 +27,10 @@ export default { 'unusedSeal', ]), ...mapState('commit', ['commitMessage', 'submitCommitLoading']), - ...mapGetters(['lastOpenedFile', 'hasChanges', 'someUncommitedChanges', 'activeFile']), + ...mapGetters(['lastOpenedFile', 'hasChanges', 'someUncommittedChanges', 'activeFile']), ...mapGetters('commit', ['discardDraftButtonDisabled']), showStageUnstageArea() { - return !!(this.someUncommitedChanges || this.lastCommitMsg || !this.unusedSeal); + return !!(this.someUncommittedChanges || this.lastCommitMsg || !this.unusedSeal); }, activeFileKey() { return this.activeFile ? this.activeFile.key : null; diff --git a/app/assets/javascripts/ide/stores/getters.js b/app/assets/javascripts/ide/stores/getters.js index 709748fb530..8ad85074d6b 100644 --- a/app/assets/javascripts/ide/stores/getters.js +++ b/app/assets/javascripts/ide/stores/getters.js @@ -63,7 +63,7 @@ export const isEditModeActive = state => state.currentActivityView === activityB export const isCommitModeActive = state => state.currentActivityView === activityBarViews.commit; export const isReviewModeActive = state => state.currentActivityView === activityBarViews.review; -export const someUncommitedChanges = state => +export const someUncommittedChanges = state => !!(state.changedFiles.length || state.stagedFiles.length); export const getChangesInFolder = state => path => { diff --git a/app/assets/javascripts/jobs/components/artifacts_block.vue b/app/assets/javascripts/jobs/components/artifacts_block.vue index 17fd5321642..93c89411b4a 100644 --- a/app/assets/javascripts/jobs/components/artifacts_block.vue +++ b/app/assets/javascripts/jobs/components/artifacts_block.vue @@ -1,10 +1,12 @@ <script> +import { GlLink } from '@gitlab-org/gitlab-ui'; import TimeagoTooltip from '~/vue_shared/components/time_ago_tooltip.vue'; import timeagoMixin from '~/vue_shared/mixins/timeago'; export default { components: { TimeagoTooltip, + GlLink, }, mixins: [timeagoMixin], props: { @@ -53,16 +55,16 @@ export default { class="btn-group d-flex" role="group" > - <a + <gl-link v-if="artifact.keep_path" :href="artifact.keep_path" class="js-keep-artifacts btn btn-sm btn-default" data-method="post" > {{ s__('Job|Keep') }} - </a> + </gl-link> - <a + <gl-link v-if="artifact.download_path" :href="artifact.download_path" class="js-download-artifacts btn btn-sm btn-default" @@ -70,15 +72,15 @@ export default { rel="nofollow" > {{ s__('Job|Download') }} - </a> + </gl-link> - <a + <gl-link v-if="artifact.browse_path" :href="artifact.browse_path" class="js-browse-artifacts btn btn-sm btn-default" > {{ s__('Job|Browse') }} - </a> + </gl-link> </div> </div> </template> diff --git a/app/assets/javascripts/jobs/components/commit_block.vue b/app/assets/javascripts/jobs/components/commit_block.vue index 7d51f6afd10..06fe23fedce 100644 --- a/app/assets/javascripts/jobs/components/commit_block.vue +++ b/app/assets/javascripts/jobs/components/commit_block.vue @@ -1,9 +1,11 @@ <script> +import { GlLink } from '@gitlab-org/gitlab-ui'; import ClipboardButton from '~/vue_shared/components/clipboard_button.vue'; export default { components: { ClipboardButton, + GlLink, }, props: { commit: { @@ -31,10 +33,10 @@ export default { <p> {{ __('Commit') }} - <a + <gl-link :href="commit.commit_path" class="js-commit-sha commit-sha link-commit" - >{{ commit.short_id }}</a> + >{{ commit.short_id }}</gl-link> <clipboard-button :text="commit.short_id" @@ -42,11 +44,11 @@ export default { css-class="btn btn-clipboard btn-transparent" /> - <a + <gl-link v-if="mergeRequest" :href="mergeRequest.path" class="js-link-commit link-commit" - >!{{ mergeRequest.iid }}</a> + >!{{ mergeRequest.iid }}</gl-link> </p> <p class="build-light-text append-bottom-0"> diff --git a/app/assets/javascripts/jobs/components/empty_state.vue b/app/assets/javascripts/jobs/components/empty_state.vue index ee5ceb99b0a..be7425c2d25 100644 --- a/app/assets/javascripts/jobs/components/empty_state.vue +++ b/app/assets/javascripts/jobs/components/empty_state.vue @@ -1,5 +1,10 @@ <script> +import { GlLink } from '@gitlab-org/gitlab-ui'; + export default { + components: { + GlLink, + }, props: { illustrationPath: { type: String, @@ -62,13 +67,13 @@ export default { v-if="action" class="text-center" > - <a + <gl-link :href="action.path" :data-method="action.method" class="js-job-empty-state-action btn btn-primary" > {{ action.button_title }} - </a> + </gl-link> </div> </div> </div> diff --git a/app/assets/javascripts/jobs/components/erased_block.vue b/app/assets/javascripts/jobs/components/erased_block.vue index 5ffbfb6e19a..d80e905c68e 100644 --- a/app/assets/javascripts/jobs/components/erased_block.vue +++ b/app/assets/javascripts/jobs/components/erased_block.vue @@ -1,10 +1,12 @@ <script> import _ from 'underscore'; +import { GlLink } from '@gitlab-org/gitlab-ui'; import TimeagoTooltip from '~/vue_shared/components/time_ago_tooltip.vue'; export default { components: { TimeagoTooltip, + GlLink, }, props: { user: { @@ -29,9 +31,9 @@ export default { <div class="erased alert alert-warning"> <template v-if="isErasedByUser"> {{ s__("Job|Job has been erased by") }} - <a :href="user.web_url"> + <gl-link :href="user.web_url"> {{ user.username }} - </a> + </gl-link> </template> <template v-else> {{ s__("Job|Job has been erased") }} diff --git a/app/assets/javascripts/jobs/components/job_app.vue b/app/assets/javascripts/jobs/components/job_app.vue index ac19034f69d..6e95e3d16f8 100644 --- a/app/assets/javascripts/jobs/components/job_app.vue +++ b/app/assets/javascripts/jobs/components/job_app.vue @@ -1,164 +1,166 @@ <script> - import _ from 'underscore'; - import { mapGetters, mapState, mapActions } from 'vuex'; - import { isScrolledToBottom } from '~/lib/utils/scroll_utils'; - import bp from '~/breakpoints'; - import CiHeader from '~/vue_shared/components/header_ci_component.vue'; - import Callout from '~/vue_shared/components/callout.vue'; - import createStore from '../store'; - import EmptyState from './empty_state.vue'; - import EnvironmentsBlock from './environments_block.vue'; - import ErasedBlock from './erased_block.vue'; - import Log from './job_log.vue'; - import LogTopBar from './job_log_controllers.vue'; - import StuckBlock from './stuck_block.vue'; - import Sidebar from './sidebar.vue'; +import _ from 'underscore'; +import { mapGetters, mapState, mapActions } from 'vuex'; +import { GlLoadingIcon } from '@gitlab-org/gitlab-ui'; +import { isScrolledToBottom } from '~/lib/utils/scroll_utils'; +import bp from '~/breakpoints'; +import CiHeader from '~/vue_shared/components/header_ci_component.vue'; +import Callout from '~/vue_shared/components/callout.vue'; +import createStore from '../store'; +import EmptyState from './empty_state.vue'; +import EnvironmentsBlock from './environments_block.vue'; +import ErasedBlock from './erased_block.vue'; +import Log from './job_log.vue'; +import LogTopBar from './job_log_controllers.vue'; +import StuckBlock from './stuck_block.vue'; +import Sidebar from './sidebar.vue'; - export default { - name: 'JobPageApp', - store: createStore(), - components: { - CiHeader, - Callout, - EmptyState, - EnvironmentsBlock, - ErasedBlock, - Log, - LogTopBar, - StuckBlock, - Sidebar, +export default { + name: 'JobPageApp', + store: createStore(), + components: { + CiHeader, + Callout, + EmptyState, + EnvironmentsBlock, + ErasedBlock, + GlLoadingIcon, + Log, + LogTopBar, + StuckBlock, + Sidebar, + }, + props: { + runnerSettingsUrl: { + type: String, + required: false, + default: null, }, - props: { - runnerSettingsUrl: { - type: String, - required: false, - default: null, - }, - runnerHelpUrl: { - type: String, - required: false, - default: null, - }, - endpoint: { - type: String, - required: true, - }, - terminalPath: { - type: String, - required: false, - default: null, - }, - pagePath: { - type: String, - required: true, - }, - logState: { - type: String, - required: true, - }, + runnerHelpUrl: { + type: String, + required: false, + default: null, }, - computed: { - ...mapState([ - 'isLoading', - 'job', - 'isSidebarOpen', - 'trace', - 'isTraceComplete', - 'traceSize', - 'isTraceSizeVisible', - 'isScrollBottomDisabled', - 'isScrollTopDisabled', - 'isScrolledToBottomBeforeReceivingTrace', - 'hasError', - ]), - ...mapGetters([ - 'headerActions', - 'headerTime', - 'shouldRenderCalloutMessage', - 'shouldRenderTriggeredLabel', - 'hasEnvironment', - 'hasTrace', - 'emptyStateIllustration', - 'isScrollingDown', - 'emptyStateAction', - 'hasRunnersForProject', - ]), + endpoint: { + type: String, + required: true, + }, + terminalPath: { + type: String, + required: false, + default: null, + }, + pagePath: { + type: String, + required: true, + }, + logState: { + type: String, + required: true, + }, + }, + computed: { + ...mapState([ + 'isLoading', + 'job', + 'isSidebarOpen', + 'trace', + 'isTraceComplete', + 'traceSize', + 'isTraceSizeVisible', + 'isScrollBottomDisabled', + 'isScrollTopDisabled', + 'isScrolledToBottomBeforeReceivingTrace', + 'hasError', + ]), + ...mapGetters([ + 'headerActions', + 'headerTime', + 'shouldRenderCalloutMessage', + 'shouldRenderTriggeredLabel', + 'hasEnvironment', + 'hasTrace', + 'emptyStateIllustration', + 'isScrollingDown', + 'emptyStateAction', + 'hasRunnersForProject', + ]), - shouldRenderContent() { - return !this.isLoading && !this.hasError; - } + shouldRenderContent() { + return !this.isLoading && !this.hasError; }, - watch: { - // Once the job log is loaded, - // fetch the stages for the dropdown on the sidebar - job(newVal, oldVal) { - if (_.isEmpty(oldVal) && !_.isEmpty(newVal.pipeline)) { - this.fetchStages(); - } - }, + }, + watch: { + // Once the job log is loaded, + // fetch the stages for the dropdown on the sidebar + job(newVal, oldVal) { + if (_.isEmpty(oldVal) && !_.isEmpty(newVal.pipeline)) { + this.fetchStages(); + } }, - created() { - this.throttled = _.throttle(this.toggleScrollButtons, 100); + }, + created() { + this.throttled = _.throttle(this.toggleScrollButtons, 100); - this.setJobEndpoint(this.endpoint); - this.setTraceOptions({ - logState: this.logState, - pagePath: this.pagePath, - }); + this.setJobEndpoint(this.endpoint); + this.setTraceOptions({ + logState: this.logState, + pagePath: this.pagePath, + }); - this.fetchJob(); - this.fetchTrace(); + this.fetchJob(); + this.fetchTrace(); - window.addEventListener('resize', this.onResize); - window.addEventListener('scroll', this.updateScroll); - }, + window.addEventListener('resize', this.onResize); + window.addEventListener('scroll', this.updateScroll); + }, - mounted() { + mounted() { + this.updateSidebar(); + }, + + destroyed() { + window.removeEventListener('resize', this.onResize); + window.removeEventListener('scroll', this.updateScroll); + }, + + methods: { + ...mapActions([ + 'setJobEndpoint', + 'setTraceOptions', + 'fetchJob', + 'fetchStages', + 'hideSidebar', + 'showSidebar', + 'toggleSidebar', + 'fetchTrace', + 'scrollBottom', + 'scrollTop', + 'toggleScrollButtons', + 'toggleScrollAnimation', + ]), + onResize() { this.updateSidebar(); + this.updateScroll(); }, - - destroyed() { - window.removeEventListener('resize', this.onResize); - window.removeEventListener('scroll', this.updateScroll); + updateSidebar() { + if (bp.getBreakpointSize() === 'xs') { + this.hideSidebar(); + } else if (!this.isSidebarOpen) { + this.showSidebar(); + } }, + updateScroll() { + if (!isScrolledToBottom()) { + this.toggleScrollAnimation(false); + } else if (this.isScrollingDown) { + this.toggleScrollAnimation(true); + } - methods: { - ...mapActions([ - 'setJobEndpoint', - 'setTraceOptions', - 'fetchJob', - 'fetchStages', - 'hideSidebar', - 'showSidebar', - 'toggleSidebar', - 'fetchTrace', - 'scrollBottom', - 'scrollTop', - 'toggleScrollButtons', - 'toggleScrollAnimation', - ]), - onResize() { - this.updateSidebar(); - this.updateScroll(); - }, - updateSidebar() { - if (bp.getBreakpointSize() === 'xs') { - this.hideSidebar(); - } else if (!this.isSidebarOpen) { - this.showSidebar(); - } - }, - updateScroll() { - if (!isScrolledToBottom()) { - this.toggleScrollAnimation(false); - } else if (this.isScrollingDown) { - this.toggleScrollAnimation(true); - } - - this.throttled(); - }, + this.throttled(); }, - }; + }, +}; </script> <template> <div> diff --git a/app/assets/javascripts/jobs/components/job_container_item.vue b/app/assets/javascripts/jobs/components/job_container_item.vue index 6486b25c8a7..cdac8a391d1 100644 --- a/app/assets/javascripts/jobs/components/job_container_item.vue +++ b/app/assets/javascripts/jobs/components/job_container_item.vue @@ -1,15 +1,16 @@ <script> +import { GlTooltipDirective, GlLink } from '@gitlab-org/gitlab-ui'; import CiIcon from '~/vue_shared/components/ci_icon.vue'; import Icon from '~/vue_shared/components/icon.vue'; -import tooltip from '~/vue_shared/directives/tooltip'; export default { components: { CiIcon, Icon, + GlLink, }, directives: { - tooltip, + GlTooltip: GlTooltipDirective, }, props: { job: { @@ -37,11 +38,10 @@ export default { active: isActive }" > - <a - v-tooltip + <gl-link + v-gl-tooltip :href="job.status.details_path" :title="tooltipText" - data-container="body" data-boundary="viewport" class="js-job-link" > @@ -60,6 +60,6 @@ export default { name="retry" class="js-retry-icon" /> - </a> + </gl-link> </div> </template> diff --git a/app/assets/javascripts/jobs/components/job_log.vue b/app/assets/javascripts/jobs/components/job_log.vue index ffa6ada3e28..92e20e92d66 100644 --- a/app/assets/javascripts/jobs/components/job_log.vue +++ b/app/assets/javascripts/jobs/components/job_log.vue @@ -1,45 +1,45 @@ <script> - import { mapState, mapActions } from 'vuex'; +import { mapState, mapActions } from 'vuex'; - export default { - name: 'JobLog', - props: { - trace: { - type: String, - required: true, - }, - isComplete: { - type: Boolean, - required: true, - }, +export default { + name: 'JobLog', + props: { + trace: { + type: String, + required: true, }, - computed: { - ...mapState(['isScrolledToBottomBeforeReceivingTrace']), + isComplete: { + type: Boolean, + required: true, }, - updated() { - this.$nextTick(() => this.handleScrollDown()); + }, + computed: { + ...mapState(['isScrolledToBottomBeforeReceivingTrace']), + }, + updated() { + this.$nextTick(() => this.handleScrollDown()); + }, + mounted() { + this.$nextTick(() => this.handleScrollDown()); + }, + methods: { + ...mapActions(['scrollBottom']), + /** + * The job log is sent in HTML, which means we need to use `v-html` to render it + * Using the updated hook with $nextTick is not enough to wait for the DOM to be updated + * in this case because it runs before `v-html` has finished running, since there's no + * Vue binding. + * In order to scroll the page down after `v-html` has finished, we need to use setTimeout + */ + handleScrollDown() { + if (this.isScrolledToBottomBeforeReceivingTrace) { + setTimeout(() => { + this.scrollBottom(); + }, 0); + } }, - mounted() { - this.$nextTick(() => this.handleScrollDown()); - }, - methods: { - ...mapActions(['scrollBottom']), - /** - * The job log is sent in HTML, which means we need to use `v-html` to render it - * Using the updated hook with $nextTick is not enough to wait for the DOM to be updated - * in this case because it runs before `v-html` has finished running, since there's no - * Vue binding. - * In order to scroll the page down after `v-html` has finished, we need to use setTimeout - */ - handleScrollDown() { - if (this.isScrolledToBottomBeforeReceivingTrace) { - setTimeout(() => { - this.scrollBottom(); - }, 0); - } - }, - }, - }; + }, +}; </script> <template> <pre class="js-build-trace build-trace qa-build-trace"> diff --git a/app/assets/javascripts/jobs/components/job_log_controllers.vue b/app/assets/javascripts/jobs/components/job_log_controllers.vue index 94ab1b16c84..eeefa33264f 100644 --- a/app/assets/javascripts/jobs/components/job_log_controllers.vue +++ b/app/assets/javascripts/jobs/components/job_log_controllers.vue @@ -1,7 +1,7 @@ <script> +import { GlTooltipDirective, GlLink, GlButton } from '@gitlab-org/gitlab-ui'; import { polyfillSticky } from '~/lib/utils/sticky'; import Icon from '~/vue_shared/components/icon.vue'; -import tooltip from '~/vue_shared/directives/tooltip'; import { numberToHumanSize } from '~/lib/utils/number_utils'; import { sprintf } from '~/locale'; import scrollDown from '../svg/scroll_down.svg'; @@ -9,9 +9,11 @@ import scrollDown from '../svg/scroll_down.svg'; export default { components: { Icon, + GlLink, + GlButton, }, directives: { - tooltip, + GlTooltip: GlTooltipDirective, }, scrollDown, props: { @@ -73,76 +75,70 @@ export default { <template v-if="isTraceSizeVisible"> {{ jobLogSize }} - <a + <gl-link v-if="rawPath" :href="rawPath" class="js-raw-link raw-link" > {{ s__("Job|Complete Raw") }} - </a> + </gl-link> </template> </div> <!-- eo truncate information --> <div class="controllers float-right"> <!-- links --> - <a + <gl-link v-if="rawPath" - v-tooltip + v-gl-tooltip.body :title="s__('Job|Show complete raw')" :href="rawPath" class="js-raw-link-controller controllers-buttons" - data-container="body" > <icon name="doc-text" /> - </a> + </gl-link> - <a + <gl-link v-if="erasePath" - v-tooltip + v-gl-tooltip.body :title="s__('Job|Erase job log')" :href="erasePath" :data-confirm="__('Are you sure you want to erase this build?')" class="js-erase-link controllers-buttons" - data-container="body" data-method="post" > <icon name="remove" /> - </a> + </gl-link> <!-- eo links --> <!-- scroll buttons --> <div - v-tooltip + v-gl-tooltip :title="s__('Job|Scroll to top')" class="controllers-buttons" - data-container="body" > - <button + <gl-button :disabled="isScrollTopDisabled" type="button" class="js-scroll-top btn-scroll btn-transparent btn-blank" @click="handleScrollToTop" > - <icon name="scroll_up"/> - </button> + <icon name="scroll_up" /> + </gl-button> </div> <div - v-tooltip + v-gl-tooltip :title="s__('Job|Scroll to bottom')" class="controllers-buttons" - data-container="body" > - <button + <gl-button :disabled="isScrollBottomDisabled" - type="button" class="js-scroll-bottom btn-scroll btn-transparent btn-blank" :class="{ animate: isScrollingDown }" @click="handleScrollToBottom" v-html="$options.scrollDown" - > - </button> + /> </div> <!-- eo scroll buttons --> </div> diff --git a/app/assets/javascripts/jobs/components/sidebar_detail_row.vue b/app/assets/javascripts/jobs/components/sidebar_detail_row.vue index aeafe98a70b..cfedb38e17a 100644 --- a/app/assets/javascripts/jobs/components/sidebar_detail_row.vue +++ b/app/assets/javascripts/jobs/components/sidebar_detail_row.vue @@ -1,6 +1,11 @@ <script> +import { GlLink } from '@gitlab-org/gitlab-ui'; + export default { name: 'SidebarDetailRow', + components: { + GlLink, + }, props: { title: { type: String, @@ -41,7 +46,7 @@ export default { v-if="hasHelpURL" class="help-button float-right" > - <a + <gl-link :href="helpUrl" target="_blank" rel="noopener noreferrer nofollow" @@ -50,7 +55,7 @@ export default { class="fa fa-question-circle" aria-hidden="true" ></i> - </a> + </gl-link> </span> </p> </template> diff --git a/app/assets/javascripts/jobs/components/stuck_block.vue b/app/assets/javascripts/jobs/components/stuck_block.vue index 1d5789b175a..ca4bf471363 100644 --- a/app/assets/javascripts/jobs/components/stuck_block.vue +++ b/app/assets/javascripts/jobs/components/stuck_block.vue @@ -1,8 +1,12 @@ <script> +import { GlLink } from '@gitlab-org/gitlab-ui'; /** * Renders Stuck Runners block for job's view. */ export default { + components: { + GlLink, + }, props: { hasNoRunnersForProject: { type: Boolean, @@ -52,12 +56,12 @@ export default { </p> {{ __("Go to") }} - <a + <gl-link v-if="runnersPath" :href="runnersPath" class="js-runners-path" > {{ __("Runners page") }} - </a> + </gl-link> </div> </template> diff --git a/app/assets/javascripts/jobs/index.js b/app/assets/javascripts/jobs/index.js index ccd096a1da5..a32e945627c 100644 --- a/app/assets/javascripts/jobs/index.js +++ b/app/assets/javascripts/jobs/index.js @@ -23,4 +23,3 @@ export default () => { }, }); }; - diff --git a/app/assets/javascripts/jobs/store/getters.js b/app/assets/javascripts/jobs/store/getters.js index 4de01f8e532..d440b2c9ef1 100644 --- a/app/assets/javascripts/jobs/store/getters.js +++ b/app/assets/javascripts/jobs/store/getters.js @@ -35,16 +35,19 @@ export const hasEnvironment = state => !_.isEmpty(state.job.deployment_status); * Used to check if it should render the job log or the empty state * @returns {Boolean} */ -export const hasTrace = state => state.job.has_trace || (!_.isEmpty(state.job.status) && state.job.status.group === 'running'); +export const hasTrace = state => + state.job.has_trace || (!_.isEmpty(state.job.status) && state.job.status.group === 'running'); export const emptyStateIllustration = state => (state.job && state.job.status && state.job.status.illustration) || {}; -export const emptyStateAction = state => (state.job && state.job.status && state.job.status.action) || {}; +export const emptyStateAction = state => + (state.job && state.job.status && state.job.status.action) || {}; export const isScrollingDown = state => isScrolledToBottom() && !state.isTraceComplete; -export const hasRunnersForProject = state => state.job.runners.available && !state.job.runners.online; +export const hasRunnersForProject = state => + state.job.runners.available && !state.job.runners.online; // prevent babel-plugin-rewire from generating an invalid default during karma tests export default () => {}; diff --git a/app/assets/javascripts/labels_select.js b/app/assets/javascripts/labels_select.js index 3c38d998b6c..c0a76814102 100644 --- a/app/assets/javascripts/labels_select.js +++ b/app/assets/javascripts/labels_select.js @@ -25,16 +25,43 @@ export default class LabelsSelect { } $els.each(function(i, dropdown) { - var $block, $colorPreview, $dropdown, $form, $loading, $selectbox, $sidebarCollapsedValue, $value, abilityName, defaultLabel, enableLabelCreateButton, issueURLSplit, issueUpdateURL, labelUrl, namespacePath, projectPath, saveLabelData, selectedLabel, showAny, showNo, $sidebarLabelTooltip, initialSelected, $toggleText, fieldName, useId, propertyName, showMenuAbove, $container, $dropdownContainer; + var $block, + $colorPreview, + $dropdown, + $form, + $loading, + $selectbox, + $sidebarCollapsedValue, + $value, + abilityName, + defaultLabel, + enableLabelCreateButton, + issueURLSplit, + issueUpdateURL, + labelUrl, + namespacePath, + projectPath, + saveLabelData, + selectedLabel, + showAny, + showNo, + $sidebarLabelTooltip, + initialSelected, + $toggleText, + fieldName, + useId, + propertyName, + showMenuAbove, + $container, + $dropdownContainer; $dropdown = $(dropdown); $dropdownContainer = $dropdown.closest('.labels-filter'); $toggleText = $dropdown.find('.dropdown-toggle-text'); namespacePath = $dropdown.data('namespacePath'); projectPath = $dropdown.data('projectPath'); - labelUrl = $dropdown.data('labels'); issueUpdateURL = $dropdown.data('issueUpdate'); selectedLabel = $dropdown.data('selected'); - if ((selectedLabel != null) && !$dropdown.hasClass('js-multiselect')) { + if (selectedLabel != null && !$dropdown.hasClass('js-multiselect')) { selectedLabel = selectedLabel.split(','); } showNo = $dropdown.data('showNo'); @@ -50,26 +77,37 @@ export default class LabelsSelect { $value = $block.find('.value'); $loading = $block.find('.block-loading').fadeOut(); fieldName = $dropdown.data('fieldName'); - useId = $dropdown.is('.js-issuable-form-dropdown, .js-filter-bulk-update, .js-label-sidebar-dropdown'); + useId = $dropdown.is( + '.js-issuable-form-dropdown, .js-filter-bulk-update, .js-label-sidebar-dropdown', + ); propertyName = useId ? 'id' : 'title'; initialSelected = $selectbox .find('input[name="' + $dropdown.data('fieldName') + '"]') - .map(function () { + .map(function() { return this.value; - }).get(); + }) + .get(); const { handleClick } = options; $sidebarLabelTooltip.tooltip(); if ($dropdown.closest('.dropdown').find('.dropdown-new-label').length) { - new CreateLabelDropdown($dropdown.closest('.dropdown').find('.dropdown-new-label'), namespacePath, projectPath); + new CreateLabelDropdown( + $dropdown.closest('.dropdown').find('.dropdown-new-label'), + namespacePath, + projectPath, + ); } saveLabelData = function() { var data, selected; - selected = $dropdown.closest('.selectbox').find("input[name='" + fieldName + "']").map(function() { - return this.value; - }).get(); + selected = $dropdown + .closest('.selectbox') + .find("input[name='" + fieldName + "']") + .map(function() { + return this.value; + }) + .get(); if (_.isEqual(initialSelected, selected)) return; initialSelected = selected; @@ -82,7 +120,8 @@ export default class LabelsSelect { } $loading.removeClass('hidden').fadeIn(); $dropdown.trigger('loading.gl.dropdown'); - axios.put(issueUpdateURL, data) + axios + .put(issueUpdateURL, data) .then(({ data }) => { var labelCount, template, labelTooltipTitle, labelTitles, formattedLabels; $loading.fadeOut(); @@ -96,8 +135,7 @@ export default class LabelsSelect { issueUpdateURL, }); labelCount = data.labels.length; - } - else { + } else { template = '<span class="no-value">None</span>'; } $value.removeAttr('style').html(template); @@ -114,17 +152,14 @@ export default class LabelsSelect { } labelTooltipTitle = labelTitles.join(', '); - } - else { + } else { labelTooltipTitle = __('Labels'); } - $sidebarLabelTooltip - .attr('title', labelTooltipTitle) - .tooltip('_fixTitle'); + $sidebarLabelTooltip.attr('title', labelTooltipTitle).tooltip('_fixTitle'); $('.has-tooltip', $value).tooltip({ - container: 'body' + container: 'body', }); }) .catch(() => flash(__('Error saving label update.'))); @@ -132,34 +167,39 @@ export default class LabelsSelect { $dropdown.glDropdown({ showMenuAbove: showMenuAbove, data: function(term, callback) { - axios.get(labelUrl) - .then((res) => { - let data = _.chain(res.data).groupBy(function(label) { - return label.title; - }).map(function(label) { - var color; - color = _.map(label, function(dup) { - return dup.color; - }); - return { - id: label[0].id, - title: label[0].title, - color: color, - duplicate: color.length > 1 - }; - }).value(); + labelUrl = $dropdown.attr('data-labels'); + axios + .get(labelUrl) + .then(res => { + let data = _.chain(res.data) + .groupBy(function(label) { + return label.title; + }) + .map(function(label) { + var color; + color = _.map(label, function(dup) { + return dup.color; + }); + return { + id: label[0].id, + title: label[0].title, + color: color, + duplicate: color.length > 1, + }; + }) + .value(); if ($dropdown.hasClass('js-extra-options')) { var extraData = []; if (showNo) { extraData.unshift({ id: 0, - title: 'No Label' + title: 'No Label', }); } if (showAny) { extraData.unshift({ isAny: true, - title: 'Any Label' + title: 'Any Label', }); } if (extraData.length) { @@ -176,11 +216,22 @@ export default class LabelsSelect { .catch(() => flash(__('Error fetching labels.'))); }, renderRow: function(label, instance) { - var $a, $li, color, colorEl, indeterminate, removesAll, selectedClass, spacing, i, marked, dropdownName, dropdownValue; + var $a, + $li, + color, + colorEl, + indeterminate, + removesAll, + selectedClass, + spacing, + i, + marked, + dropdownName, + dropdownValue; $li = $('<li>'); $a = $('<a href="#">'); selectedClass = []; - removesAll = label.id <= 0 || (label.id == null); + removesAll = label.id <= 0 || label.id == null; if ($dropdown.hasClass('js-filter-bulk-update')) { indeterminate = $dropdown.data('indeterminate') || []; marked = $dropdown.data('marked') || []; @@ -200,9 +251,19 @@ export default class LabelsSelect { } else { if (this.id(label)) { dropdownName = $dropdown.data('fieldName'); - dropdownValue = this.id(label).toString().replace(/'/g, '\\\''); - - if ($form.find("input[type='hidden'][name='" + dropdownName + "'][value='" + dropdownValue + "']").length) { + dropdownValue = this.id(label) + .toString() + .replace(/'/g, "\\'"); + + if ( + $form.find( + "input[type='hidden'][name='" + + dropdownName + + "'][value='" + + dropdownValue + + "']", + ).length + ) { selectedClass.push('is-active'); } } @@ -213,16 +274,14 @@ export default class LabelsSelect { } if (label.duplicate) { color = DropdownUtils.duplicateLabelColor(label.color); - } - else { + } else { if (label.color != null) { [color] = label.color; } } if (color) { colorEl = "<span class='dropdown-label-box' style='background: " + color + "'></span>"; - } - else { + } else { colorEl = ''; } // We need to identify which items are actually labels @@ -235,7 +294,7 @@ export default class LabelsSelect { return $li.html($a).prop('outerHTML'); }, search: { - fields: ['title'] + fields: ['title'], }, selectable: true, filterable: true, @@ -255,25 +314,21 @@ export default class LabelsSelect { if (selected && selected.id === 0) { this.selected = []; return 'No Label'; - } - else if (isSelected) { + } else if (isSelected) { this.selected.push(title); - } - else if (!isSelected && title) { + } else if (!isSelected && title) { var index = this.selected.indexOf(title); this.selected.splice(index, 1); } if (selectedLabels.length === 1) { return selectedLabels; - } - else if (selectedLabels.length) { + } else if (selectedLabels.length) { return sprintf(__('%{firstLabel} +%{labelCount} more'), { firstLabel: selectedLabels[0], - labelCount: selectedLabels.length - 1 + labelCount: selectedLabels.length - 1, }); - } - else { + } else { return defaultLabel; } }, @@ -285,10 +340,9 @@ export default class LabelsSelect { return label.id; } - if ($dropdown.hasClass("js-filter-submit") && (label.isAny == null)) { + if ($dropdown.hasClass('js-filter-submit') && label.isAny == null) { return label.title; - } - else { + } else { return label.id; } }, @@ -310,13 +364,13 @@ export default class LabelsSelect { } if ($dropdown.hasClass('js-multiselect')) { if ($dropdown.hasClass('js-filter-submit') && (isIssueIndex || isMRIndex)) { - selectedLabels = $dropdown.closest('form').find("input:hidden[name='" + ($dropdown.data('fieldName')) + "']"); + selectedLabels = $dropdown + .closest('form') + .find("input:hidden[name='" + $dropdown.data('fieldName') + "']"); Issuable.filterResults($dropdown.closest('form')); - } - else if ($dropdown.hasClass('js-filter-submit')) { + } else if ($dropdown.hasClass('js-filter-submit')) { $dropdown.closest('form').submit(); - } - else { + } else { if (!$dropdown.hasClass('js-filter-bulk-update')) { saveLabelData(); } @@ -325,7 +379,7 @@ export default class LabelsSelect { }, multiSelect: $dropdown.hasClass('js-multiselect'), vue: $dropdown.hasClass('js-issue-board-sidebar'), - clicked: function (clickEvent) { + clicked: function(clickEvent) { const { $el, e, isMarking } = clickEvent; const label = clickEvent.selectedObj; @@ -339,7 +393,8 @@ export default class LabelsSelect { isMRIndex = page === 'projects:merge_requests:index'; if ($dropdown.parent().find('.is-active:not(.dropdown-clear-active)').length) { - $dropdown.parent() + $dropdown + .parent() .find('.dropdown-clear-active') .removeClass('is-active'); } @@ -367,28 +422,26 @@ export default class LabelsSelect { e.preventDefault(); return; - } - else if ($dropdown.hasClass('js-filter-submit') && (isIssueIndex || isMRIndex)) { + } else if ($dropdown.hasClass('js-filter-submit') && (isIssueIndex || isMRIndex)) { if (!$dropdown.hasClass('js-multiselect')) { selectedLabel = label.title; return Issuable.filterResults($dropdown.closest('form')); } - } - else if ($dropdown.hasClass('js-filter-submit')) { + } else if ($dropdown.hasClass('js-filter-submit')) { return $dropdown.closest('form').submit(); - } - else if ($dropdown.hasClass('js-issue-board-sidebar')) { + } else if ($dropdown.hasClass('js-issue-board-sidebar')) { if ($el.hasClass('is-active')) { - boardsStore.detail.issue.labels.push(new ListLabel({ - id: label.id, - title: label.title, - color: label.color[0], - textColor: '#fff' - })); - } - else { + boardsStore.detail.issue.labels.push( + new ListLabel({ + id: label.id, + title: label.title, + color: label.color[0], + textColor: '#fff', + }), + ); + } else { var { labels } = boardsStore.detail.issue; - labels = labels.filter(function (selectedLabel) { + labels = labels.filter(function(selectedLabel) { return selectedLabel.id !== label.id; }); boardsStore.detail.issue.labels = labels; @@ -396,19 +449,16 @@ export default class LabelsSelect { $loading.fadeIn(); - boardsStore.detail.issue.update($dropdown.attr('data-issue-update')) + boardsStore.detail.issue + .update($dropdown.attr('data-issue-update')) .then(fadeOutLoader) .catch(fadeOutLoader); - } - else if (handleClick) { + } else if (handleClick) { e.preventDefault(); handleClick(label); - } - else { + } else { if ($dropdown.hasClass('js-multiselect')) { - - } - else { + } else { return saveLabelData(); } } @@ -436,15 +486,17 @@ export default class LabelsSelect { // so best approach is to use traditional way of // concatenation // see: http://2ality.com/2016/05/template-literal-whitespace.html#joining-arrays - const tpl = _.template([ - '<% _.each(labels, function(label){ %>', - '<a href="<%- issueUpdateURL.slice(0, issueUpdateURL.lastIndexOf("/")) %>?label_name[]=<%- encodeURIComponent(label.title) %>">', - '<span class="badge label has-tooltip color-label" title="<%- label.description %>" style="background-color: <%- label.color %>; color: <%- label.text_color %>;">', - '<%- label.title %>', - '</span>', - '</a>', - '<% }); %>', - ].join('')); + const tpl = _.template( + [ + '<% _.each(labels, function(label){ %>', + '<a href="<%- issueUpdateURL.slice(0, issueUpdateURL.lastIndexOf("/")) %>?label_name[]=<%- encodeURIComponent(label.title) %>">', + '<span class="badge label has-tooltip color-label" title="<%- label.description %>" style="background-color: <%- label.color %>; color: <%- label.text_color %>;">', + '<%- label.title %>', + '</span>', + '</a>', + '<% }); %>', + ].join(''), + ); return tpl(tplData); } diff --git a/app/assets/javascripts/lib/utils/ace_utils.js b/app/assets/javascripts/lib/utils/ace_utils.js index efc4b2a8d94..ee71ae0e61a 100644 --- a/app/assets/javascripts/lib/utils/ace_utils.js +++ b/app/assets/javascripts/lib/utils/ace_utils.js @@ -1,6 +1,6 @@ /* global ace */ export default function getModeByFileExtension(path) { - const modelist = ace.require("ace/ext/modelist"); + const modelist = ace.require('ace/ext/modelist'); return modelist.getModeForPath(path).mode; -}; +} diff --git a/app/assets/javascripts/lib/utils/number_utils.js b/app/assets/javascripts/lib/utils/number_utils.js index afbab59055b..2ccc51c35f7 100644 --- a/app/assets/javascripts/lib/utils/number_utils.js +++ b/app/assets/javascripts/lib/utils/number_utils.js @@ -7,7 +7,7 @@ import { BYTES_IN_KIB } from './constants'; * * * Show 3 digits to the right * * For 2 digits to the left of the decimal point and X digits to the right of it * * * Show 2 digits to the right -*/ + */ export function formatRelevantDigits(number) { let digitsLeft = ''; let relevantDigits = 0; diff --git a/app/assets/javascripts/members.js b/app/assets/javascripts/members.js index 7d0c701fd70..bd263c75a3d 100644 --- a/app/assets/javascripts/members.js +++ b/app/assets/javascripts/members.js @@ -7,8 +7,12 @@ export default class Members { } addListeners() { - $('.js-member-update-control').off('change').on('change', this.formSubmit.bind(this)); - $('.js-edit-member-form').off('ajax:success').on('ajax:success', this.formSuccess.bind(this)); + $('.js-member-update-control') + .off('change') + .on('change', this.formSubmit.bind(this)); + $('.js-edit-member-form') + .off('ajax:success') + .on('ajax:success', this.formSuccess.bind(this)); gl.utils.disableButtonIfEmptyField('#user_ids', 'input[name=commit]', 'change'); } @@ -28,7 +32,7 @@ export default class Members { toggleLabel(selected, $el) { return $el.text(); }, - clicked: (options) => { + clicked: options => { this.formSubmit(null, options.$el); }, }); diff --git a/app/assets/javascripts/milestone_select.js b/app/assets/javascripts/milestone_select.js index 42fb5c7177a..d32f39881dd 100644 --- a/app/assets/javascripts/milestone_select.js +++ b/app/assets/javascripts/milestone_select.js @@ -9,7 +9,10 @@ import '~/gl_dropdown'; import axios from './lib/utils/axios_utils'; import { timeFor } from './lib/utils/datetime_utility'; import ModalStore from './boards/stores/modal_store'; -import boardsStore, { boardStoreIssueSet, boardStoreIssueDelete } from './boards/stores/boards_store'; +import boardsStore, { + boardStoreIssueSet, + boardStoreIssueDelete, +} from './boards/stores/boards_store'; export default class MilestoneSelect { constructor(currentProject, els, options = {}) { diff --git a/app/assets/javascripts/notes/components/comment_form.vue b/app/assets/javascripts/notes/components/comment_form.vue index b980e43b898..554db102027 100644 --- a/app/assets/javascripts/notes/components/comment_form.vue +++ b/app/assets/javascripts/notes/components/comment_form.vue @@ -390,7 +390,7 @@ append-right-10 comment-type-dropdown js-comment-type-dropdown droplab-dropdown" :disabled="isSubmitButtonDisabled" name="button" type="button" - class="btn comment-btn note-type-toggle js-note-new-discussion dropdown-toggle" + class="btn comment-btn note-type-toggle js-note-new-discussion dropdown-toggle qa-note-dropdown" data-display="static" data-toggle="dropdown" aria-label="Open comment type dropdown"> @@ -422,7 +422,7 @@ append-right-10 comment-type-dropdown js-comment-type-dropdown droplab-dropdown" <li :class="{ 'droplab-item-selected': noteType === 'discussion' }"> <button type="button" - class="btn btn-transparent" + class="btn btn-transparent qa-discussion-option" @click.prevent="setNoteType('discussion')"> <i aria-hidden="true" diff --git a/app/assets/javascripts/notes/components/discussion_filter.vue b/app/assets/javascripts/notes/components/discussion_filter.vue index 6e8f43048d1..affa2d1b574 100644 --- a/app/assets/javascripts/notes/components/discussion_filter.vue +++ b/app/assets/javascripts/notes/components/discussion_filter.vue @@ -1,7 +1,8 @@ <script> import $ from 'jquery'; -import Icon from '~/vue_shared/components/icon.vue'; import { mapGetters, mapActions } from 'vuex'; +import Icon from '~/vue_shared/components/icon.vue'; +import { DISCUSSION_FILTERS_DEFAULT_VALUE, HISTORY_ONLY_FILTER_VALUE } from '../constants'; export default { components: { @@ -12,14 +13,17 @@ export default { type: Array, required: true, }, - defaultValue: { + selectedValue: { type: Number, default: null, required: false, }, }, data() { - return { currentValue: this.defaultValue }; + return { + currentValue: this.selectedValue, + defaultValue: DISCUSSION_FILTERS_DEFAULT_VALUE, + }; }, computed: { ...mapGetters(['getNotesDataByProp']), @@ -28,8 +32,11 @@ export default { return this.filters.find(filter => filter.value === this.currentValue); }, }, + mounted() { + this.toggleCommentsForm(); + }, methods: { - ...mapActions(['filterDiscussion']), + ...mapActions(['filterDiscussion', 'setCommentsDisabled']), selectFilter(value) { const filter = parseInt(value, 10); @@ -39,6 +46,10 @@ export default { if (filter === this.currentValue) return; this.currentValue = filter; this.filterDiscussion({ path: this.getNotesDataByProp('discussionsPath'), filter }); + this.toggleCommentsForm(); + }, + toggleCommentsForm() { + this.setCommentsDisabled(this.currentValue === HISTORY_ONLY_FILTER_VALUE); }, }, }; @@ -73,6 +84,10 @@ export default { > {{ filter.title }} </button> + <div + v-if="filter.value === defaultValue" + class="dropdown-divider" + ></div> </li> </ul> </div> diff --git a/app/assets/javascripts/notes/components/note_awards_list.vue b/app/assets/javascripts/notes/components/note_awards_list.vue index e707f44bf5a..df7ab4502a6 100644 --- a/app/assets/javascripts/notes/components/note_awards_list.vue +++ b/app/assets/javascripts/notes/components/note_awards_list.vue @@ -110,7 +110,7 @@ export default { // Get the remaining list to use in `and x more` text. const remainingAwardList = awardList.slice(TOOLTIP_NAME_COUNT, awardList.length); - // Add myself to the begining of the list so title will start with You. + // Add myself to the beginning of the list so title will start with You. if (hasReactionByCurrentUser) { namesToShow.unshift('You'); } diff --git a/app/assets/javascripts/notes/components/note_form.vue b/app/assets/javascripts/notes/components/note_form.vue index 38c43e5fe08..31ee8fed984 100644 --- a/app/assets/javascripts/notes/components/note_form.vue +++ b/app/assets/javascripts/notes/components/note_form.vue @@ -187,7 +187,7 @@ export default { :data-supports-quick-actions="!isEditing" name="note[note]" class="note-textarea js-gfm-input js-note-text -js-autosize markdown-area js-vue-issue-note-form js-vue-textarea" +js-autosize markdown-area js-vue-issue-note-form js-vue-textarea qa-reply-input" aria-label="Description" placeholder="Write a comment or drag your files here…" @keydown.meta.enter="handleUpdate()" diff --git a/app/assets/javascripts/notes/components/noteable_discussion.vue b/app/assets/javascripts/notes/components/noteable_discussion.vue index c5fdfa1d47c..6293dd5b7e1 100644 --- a/app/assets/javascripts/notes/components/noteable_discussion.vue +++ b/app/assets/javascripts/notes/components/noteable_discussion.vue @@ -369,7 +369,7 @@ Please check your network connection and try again.`; role="group"> <button type="button" - class="js-vue-discussion-reply btn btn-text-field mr-2" + class="js-vue-discussion-reply btn btn-text-field mr-2 qa-discussion-reply" title="Add a reply" @click="showReplyForm">Reply...</button> </div> diff --git a/app/assets/javascripts/notes/components/notes_app.vue b/app/assets/javascripts/notes/components/notes_app.vue index b0faa443a18..ed5ac112dc0 100644 --- a/app/assets/javascripts/notes/components/notes_app.vue +++ b/app/assets/javascripts/notes/components/notes_app.vue @@ -54,7 +54,14 @@ export default { }; }, computed: { - ...mapGetters(['isNotesFetched', 'discussions', 'getNotesDataByProp', 'discussionCount', 'isLoading']), + ...mapGetters([ + 'isNotesFetched', + 'discussions', + 'getNotesDataByProp', + 'discussionCount', + 'isLoading', + 'commentsDisabled', + ]), noteableType() { return this.noteableData.noteableType; }, @@ -200,6 +207,7 @@ export default { </ul> <comment-form + v-if="!commentsDisabled" :noteable-type="noteableType" :markdown-version="markdownVersion" /> diff --git a/app/assets/javascripts/notes/constants.js b/app/assets/javascripts/notes/constants.js index 2c3e07c0506..3147dc64c27 100644 --- a/app/assets/javascripts/notes/constants.js +++ b/app/assets/javascripts/notes/constants.js @@ -15,6 +15,8 @@ export const MERGE_REQUEST_NOTEABLE_TYPE = 'MergeRequest'; export const UNRESOLVE_NOTE_METHOD_NAME = 'delete'; export const RESOLVE_NOTE_METHOD_NAME = 'post'; export const DESCRIPTION_TYPE = 'changed the description'; +export const HISTORY_ONLY_FILTER_VALUE = 2; +export const DISCUSSION_FILTERS_DEFAULT_VALUE = 0; export const NOTEABLE_TYPE_MAPPING = { Issue: ISSUE_NOTEABLE_TYPE, diff --git a/app/assets/javascripts/notes/discussion_filters.js b/app/assets/javascripts/notes/discussion_filters.js index 012ffc4093e..5c5f38a3fb0 100644 --- a/app/assets/javascripts/notes/discussion_filters.js +++ b/app/assets/javascripts/notes/discussion_filters.js @@ -1,15 +1,17 @@ import Vue from 'vue'; import DiscussionFilter from './components/discussion_filter.vue'; -export default (store) => { +export default store => { const discussionFilterEl = document.getElementById('js-vue-discussion-filter'); if (discussionFilterEl) { const { defaultFilter, notesFilters } = discussionFilterEl.dataset; - const defaultValue = defaultFilter ? parseInt(defaultFilter, 10) : null; + const selectedValue = defaultFilter ? parseInt(defaultFilter, 10) : null; const filterValues = notesFilters ? JSON.parse(notesFilters) : {}; - const filters = Object.keys(filterValues).map(entry => - ({ title: entry, value: filterValues[entry] })); + const filters = Object.keys(filterValues).map(entry => ({ + title: entry, + value: filterValues[entry], + })); return new Vue({ el: discussionFilterEl, @@ -22,7 +24,7 @@ export default (store) => { return createElement('discussion-filter', { props: { filters, - defaultValue, + selectedValue, }, }); }, diff --git a/app/assets/javascripts/notes/stores/actions.js b/app/assets/javascripts/notes/stores/actions.js index b5dd49bc6c9..88739ffb083 100644 --- a/app/assets/javascripts/notes/stores/actions.js +++ b/app/assets/javascripts/notes/stores/actions.js @@ -364,5 +364,9 @@ export const filterDiscussion = ({ dispatch }, { path, filter }) => { }); }; +export const setCommentsDisabled = ({ commit }, data) => { + commit(types.DISABLE_COMMENTS, data); +}; + // prevent babel-plugin-rewire from generating an invalid default during karma tests export default () => {}; diff --git a/app/assets/javascripts/notes/stores/collapse_utils.js b/app/assets/javascripts/notes/stores/collapse_utils.js index 4532226aa07..bee6d4f0329 100644 --- a/app/assets/javascripts/notes/stores/collapse_utils.js +++ b/app/assets/javascripts/notes/stores/collapse_utils.js @@ -70,7 +70,7 @@ export const collapseSystemNotes = notes => { } else if (lastDescriptionSystemNote) { const timeDifferenceMinutes = getTimeDifferenceMinutes(lastDescriptionSystemNote, note); - // are they less than 10 minutes appart? + // are they less than 10 minutes apart? if (timeDifferenceMinutes > 10) { // reset counter descriptionChangedTimes = 1; diff --git a/app/assets/javascripts/notes/stores/getters.js b/app/assets/javascripts/notes/stores/getters.js index e4f36154fcd..8df95c279eb 100644 --- a/app/assets/javascripts/notes/stores/getters.js +++ b/app/assets/javascripts/notes/stores/getters.js @@ -192,5 +192,7 @@ export const firstUnresolvedDiscussionId = (state, getters) => diffOrder => { return getters.unresolvedDiscussionsIdsByDate[0]; }; +export const commentsDisabled = state => state.commentsDisabled; + // prevent babel-plugin-rewire from generating an invalid default during karma tests export default () => {}; diff --git a/app/assets/javascripts/notes/stores/modules/index.js b/app/assets/javascripts/notes/stores/modules/index.js index 400142668ea..8aea269ea7d 100644 --- a/app/assets/javascripts/notes/stores/modules/index.js +++ b/app/assets/javascripts/notes/stores/modules/index.js @@ -21,6 +21,7 @@ export default () => ({ noteableData: { current_user: {}, }, + commentsDisabled: false, }, actions, getters, diff --git a/app/assets/javascripts/notes/stores/mutation_types.js b/app/assets/javascripts/notes/stores/mutation_types.js index 2fa53aef1d4..dfbf3b7b34b 100644 --- a/app/assets/javascripts/notes/stores/mutation_types.js +++ b/app/assets/javascripts/notes/stores/mutation_types.js @@ -15,6 +15,7 @@ export const UPDATE_DISCUSSION = 'UPDATE_DISCUSSION'; export const SET_DISCUSSION_DIFF_LINES = 'SET_DISCUSSION_DIFF_LINES'; export const SET_NOTES_FETCHED_STATE = 'SET_NOTES_FETCHED_STATE'; export const SET_NOTES_LOADING_STATE = 'SET_NOTES_LOADING_STATE'; +export const DISABLE_COMMENTS = 'DISABLE_COMMENTS'; // DISCUSSION export const COLLAPSE_DISCUSSION = 'COLLAPSE_DISCUSSION'; diff --git a/app/assets/javascripts/notes/stores/mutations.js b/app/assets/javascripts/notes/stores/mutations.js index 65085452139..c8d9e196103 100644 --- a/app/assets/javascripts/notes/stores/mutations.js +++ b/app/assets/javascripts/notes/stores/mutations.js @@ -225,4 +225,8 @@ export default { discussion.truncated_diff_lines = diffLines; }, + + [types.DISABLE_COMMENTS](state, value) { + state.commentsDisabled = value; + }, }; diff --git a/app/assets/javascripts/pages/milestones/shared/components/promote_milestone_modal.vue b/app/assets/javascripts/pages/milestones/shared/components/promote_milestone_modal.vue index 2c683a39f42..9d19e4a095d 100644 --- a/app/assets/javascripts/pages/milestones/shared/components/promote_milestone_modal.vue +++ b/app/assets/javascripts/pages/milestones/shared/components/promote_milestone_modal.vue @@ -1,54 +1,66 @@ <script> - import axios from '~/lib/utils/axios_utils'; - import createFlash from '~/flash'; - import GlModal from '~/vue_shared/components/gl_modal.vue'; - import { s__, sprintf } from '~/locale'; - import { visitUrl } from '~/lib/utils/url_utility'; - import eventHub from '../event_hub'; +import axios from '~/lib/utils/axios_utils'; +import createFlash from '~/flash'; +import GlModal from '~/vue_shared/components/gl_modal.vue'; +import { s__, sprintf } from '~/locale'; +import { visitUrl } from '~/lib/utils/url_utility'; +import eventHub from '../event_hub'; - export default { - components: { - GlModal, +export default { + components: { + GlModal, + }, + props: { + milestoneTitle: { + type: String, + required: true, }, - props: { - milestoneTitle: { - type: String, - required: true, - }, - url: { - type: String, - required: true, - }, - groupName: { - type: String, - required: true, - }, + url: { + type: String, + required: true, }, - computed: { - title() { - return sprintf(s__('Milestones|Promote %{milestoneTitle} to group milestone?'), { milestoneTitle: this.milestoneTitle }); - }, - text() { - return sprintf(s__(`Milestones|Promoting %{milestoneTitle} will make it available for all projects inside %{groupName}. + groupName: { + type: String, + required: true, + }, + }, + computed: { + title() { + return sprintf(s__('Milestones|Promote %{milestoneTitle} to group milestone?'), { + milestoneTitle: this.milestoneTitle, + }); + }, + text() { + return sprintf( + s__(`Milestones|Promoting %{milestoneTitle} will make it available for all projects inside %{groupName}. Existing project milestones with the same title will be merged. - This action cannot be reversed.`), { milestoneTitle: this.milestoneTitle, groupName: this.groupName }); - }, + This action cannot be reversed.`), + { milestoneTitle: this.milestoneTitle, groupName: this.groupName }, + ); }, - methods: { - onSubmit() { - eventHub.$emit('promoteMilestoneModal.requestStarted', this.url); - return axios.post(this.url, { params: { format: 'json' } }) - .then((response) => { - eventHub.$emit('promoteMilestoneModal.requestFinished', { milestoneUrl: this.url, successful: true }); - visitUrl(response.data.url); - }) - .catch((error) => { - eventHub.$emit('promoteMilestoneModal.requestFinished', { milestoneUrl: this.url, successful: false }); - createFlash(error); + }, + methods: { + onSubmit() { + eventHub.$emit('promoteMilestoneModal.requestStarted', this.url); + return axios + .post(this.url, { params: { format: 'json' } }) + .then(response => { + eventHub.$emit('promoteMilestoneModal.requestFinished', { + milestoneUrl: this.url, + successful: true, }); - }, + visitUrl(response.data.url); + }) + .catch(error => { + eventHub.$emit('promoteMilestoneModal.requestFinished', { + milestoneUrl: this.url, + successful: false, + }); + createFlash(error); + }); }, - }; + }, +}; </script> <template> <gl-modal @@ -65,4 +77,3 @@ {{ text }} </gl-modal> </template> - diff --git a/app/assets/javascripts/pages/projects/clusters/gcp/new/index.js b/app/assets/javascripts/pages/projects/clusters/gcp/new/index.js deleted file mode 100644 index d4f34e32a48..00000000000 --- a/app/assets/javascripts/pages/projects/clusters/gcp/new/index.js +++ /dev/null @@ -1,5 +0,0 @@ -import initGkeDropdowns from '~/projects/gke_cluster_dropdowns'; - -document.addEventListener('DOMContentLoaded', () => { - initGkeDropdowns(); -}); diff --git a/app/assets/javascripts/pages/projects/jobs/index/index.js b/app/assets/javascripts/pages/projects/jobs/index/index.js new file mode 100644 index 00000000000..1b57c67f16b --- /dev/null +++ b/app/assets/javascripts/pages/projects/jobs/index/index.js @@ -0,0 +1,16 @@ +import Vue from 'vue'; +import GlCountdown from '~/vue_shared/components/gl_countdown.vue'; + +document.addEventListener('DOMContentLoaded', () => { + const remainingTimeElements = document.querySelectorAll('.js-remaining-time'); + remainingTimeElements.forEach( + el => + new Vue({ + ...GlCountdown, + el, + propsData: { + endDateString: el.dateTime, + }, + }), + ); +}); diff --git a/app/assets/javascripts/pages/projects/project.js b/app/assets/javascripts/pages/projects/project.js index 52d66beefc9..a6bee49a6b1 100644 --- a/app/assets/javascripts/pages/projects/project.js +++ b/app/assets/javascripts/pages/projects/project.js @@ -64,7 +64,9 @@ export default class Project { const projectId = $(this).data('project-id'); const cookieKey = `hide_auto_devops_implicitly_enabled_banner_${projectId}`; Cookies.set(cookieKey, 'false'); - $(this).parents('.auto-devops-implicitly-enabled-banner').remove(); + $(this) + .parents('.auto-devops-implicitly-enabled-banner') + .remove(); return e.preventDefault(); }); Project.projectSelectDropdown(); diff --git a/app/assets/javascripts/pages/projects/shared/permissions/components/settings_panel.vue b/app/assets/javascripts/pages/projects/shared/permissions/components/settings_panel.vue index a16f7e6b77c..c0ec7a5dc94 100644 --- a/app/assets/javascripts/pages/projects/shared/permissions/components/settings_panel.vue +++ b/app/assets/javascripts/pages/projects/shared/permissions/components/settings_panel.vue @@ -1,202 +1,200 @@ <script> - import projectFeatureSetting from './project_feature_setting.vue'; - import projectFeatureToggle from '../../../../../vue_shared/components/toggle_button.vue'; - import projectSettingRow from './project_setting_row.vue'; - import { visibilityOptions, visibilityLevelDescriptions } from '../constants'; - import { toggleHiddenClassBySelector } from '../external'; +import projectFeatureSetting from './project_feature_setting.vue'; +import projectFeatureToggle from '../../../../../vue_shared/components/toggle_button.vue'; +import projectSettingRow from './project_setting_row.vue'; +import { visibilityOptions, visibilityLevelDescriptions } from '../constants'; +import { toggleHiddenClassBySelector } from '../external'; - export default { - components: { - projectFeatureSetting, - projectFeatureToggle, - projectSettingRow, - }, +export default { + components: { + projectFeatureSetting, + projectFeatureToggle, + projectSettingRow, + }, - props: { - currentSettings: { - type: Object, - required: true, - }, - canChangeVisibilityLevel: { - type: Boolean, - required: false, - default: false, - }, - allowedVisibilityOptions: { - type: Array, - required: false, - default: () => [0, 10, 20], - }, - lfsAvailable: { - type: Boolean, - required: false, - default: false, - }, - registryAvailable: { - type: Boolean, - required: false, - default: false, - }, - visibilityHelpPath: { - type: String, - required: false, - default: '', - }, - lfsHelpPath: { - type: String, - required: false, - default: '', - }, - registryHelpPath: { - type: String, - required: false, - default: '', - }, - pagesAvailable: { - type: Boolean, - required: false, - default: false, - }, - pagesAccessControlEnabled: { - type: Boolean, - required: false, - default: false, - }, - pagesHelpPath: { - type: String, - required: false, - default: '', - }, + props: { + currentSettings: { + type: Object, + required: true, + }, + canChangeVisibilityLevel: { + type: Boolean, + required: false, + default: false, + }, + allowedVisibilityOptions: { + type: Array, + required: false, + default: () => [0, 10, 20], + }, + lfsAvailable: { + type: Boolean, + required: false, + default: false, + }, + registryAvailable: { + type: Boolean, + required: false, + default: false, + }, + visibilityHelpPath: { + type: String, + required: false, + default: '', }, + lfsHelpPath: { + type: String, + required: false, + default: '', + }, + registryHelpPath: { + type: String, + required: false, + default: '', + }, + pagesAvailable: { + type: Boolean, + required: false, + default: false, + }, + pagesAccessControlEnabled: { + type: Boolean, + required: false, + default: false, + }, + pagesHelpPath: { + type: String, + required: false, + default: '', + }, + }, - data() { - const defaults = { - visibilityOptions, - visibilityLevel: visibilityOptions.PUBLIC, - issuesAccessLevel: 20, - repositoryAccessLevel: 20, - mergeRequestsAccessLevel: 20, - buildsAccessLevel: 20, - wikiAccessLevel: 20, - snippetsAccessLevel: 20, - pagesAccessLevel: 20, - containerRegistryEnabled: true, - lfsEnabled: true, - requestAccessEnabled: true, - highlightChangesClass: false, - }; + data() { + const defaults = { + visibilityOptions, + visibilityLevel: visibilityOptions.PUBLIC, + issuesAccessLevel: 20, + repositoryAccessLevel: 20, + mergeRequestsAccessLevel: 20, + buildsAccessLevel: 20, + wikiAccessLevel: 20, + snippetsAccessLevel: 20, + pagesAccessLevel: 20, + containerRegistryEnabled: true, + lfsEnabled: true, + requestAccessEnabled: true, + highlightChangesClass: false, + }; - return { ...defaults, ...this.currentSettings }; - }, + return { ...defaults, ...this.currentSettings }; + }, - computed: { - featureAccessLevelOptions() { - const options = [ - [10, 'Only Project Members'], - ]; - if (this.visibilityLevel !== visibilityOptions.PRIVATE) { - options.push([20, 'Everyone With Access']); - } - return options; - }, + computed: { + featureAccessLevelOptions() { + const options = [[10, 'Only Project Members']]; + if (this.visibilityLevel !== visibilityOptions.PRIVATE) { + options.push([20, 'Everyone With Access']); + } + return options; + }, - repoFeatureAccessLevelOptions() { - return this.featureAccessLevelOptions.filter( - ([value]) => value <= this.repositoryAccessLevel, - ); - }, + repoFeatureAccessLevelOptions() { + return this.featureAccessLevelOptions.filter( + ([value]) => value <= this.repositoryAccessLevel, + ); + }, - pagesFeatureAccessLevelOptions() { - if (this.visibilityLevel !== visibilityOptions.PUBLIC) { - return this.featureAccessLevelOptions.concat([[30, 'Everyone']]); - } - return this.featureAccessLevelOptions; - }, + pagesFeatureAccessLevelOptions() { + if (this.visibilityLevel !== visibilityOptions.PUBLIC) { + return this.featureAccessLevelOptions.concat([[30, 'Everyone']]); + } + return this.featureAccessLevelOptions; + }, - repositoryEnabled() { - return this.repositoryAccessLevel > 0; - }, + repositoryEnabled() { + return this.repositoryAccessLevel > 0; + }, - visibilityLevelDescription() { - return visibilityLevelDescriptions[this.visibilityLevel]; - }, + visibilityLevelDescription() { + return visibilityLevelDescriptions[this.visibilityLevel]; }, + }, - watch: { - visibilityLevel(value, oldValue) { - if (value === visibilityOptions.PRIVATE) { - // when private, features are restricted to "only team members" - this.issuesAccessLevel = Math.min(10, this.issuesAccessLevel); - this.repositoryAccessLevel = Math.min(10, this.repositoryAccessLevel); - this.mergeRequestsAccessLevel = Math.min(10, this.mergeRequestsAccessLevel); - this.buildsAccessLevel = Math.min(10, this.buildsAccessLevel); - this.wikiAccessLevel = Math.min(10, this.wikiAccessLevel); - this.snippetsAccessLevel = Math.min(10, this.snippetsAccessLevel); - if (this.pagesAccessLevel === 20) { - // When from Internal->Private narrow access for only members - this.pagesAccessLevel = 10; - } - this.highlightChanges(); - } else if (oldValue === visibilityOptions.PRIVATE) { - // if changing away from private, make enabled features more permissive - if (this.issuesAccessLevel > 0) this.issuesAccessLevel = 20; - if (this.repositoryAccessLevel > 0) this.repositoryAccessLevel = 20; - if (this.mergeRequestsAccessLevel > 0) this.mergeRequestsAccessLevel = 20; - if (this.buildsAccessLevel > 0) this.buildsAccessLevel = 20; - if (this.wikiAccessLevel > 0) this.wikiAccessLevel = 20; - if (this.snippetsAccessLevel > 0) this.snippetsAccessLevel = 20; - if (this.pagesAccessLevel === 10) this.pagesAccessLevel = 20; - this.highlightChanges(); + watch: { + visibilityLevel(value, oldValue) { + if (value === visibilityOptions.PRIVATE) { + // when private, features are restricted to "only team members" + this.issuesAccessLevel = Math.min(10, this.issuesAccessLevel); + this.repositoryAccessLevel = Math.min(10, this.repositoryAccessLevel); + this.mergeRequestsAccessLevel = Math.min(10, this.mergeRequestsAccessLevel); + this.buildsAccessLevel = Math.min(10, this.buildsAccessLevel); + this.wikiAccessLevel = Math.min(10, this.wikiAccessLevel); + this.snippetsAccessLevel = Math.min(10, this.snippetsAccessLevel); + if (this.pagesAccessLevel === 20) { + // When from Internal->Private narrow access for only members + this.pagesAccessLevel = 10; } - }, + this.highlightChanges(); + } else if (oldValue === visibilityOptions.PRIVATE) { + // if changing away from private, make enabled features more permissive + if (this.issuesAccessLevel > 0) this.issuesAccessLevel = 20; + if (this.repositoryAccessLevel > 0) this.repositoryAccessLevel = 20; + if (this.mergeRequestsAccessLevel > 0) this.mergeRequestsAccessLevel = 20; + if (this.buildsAccessLevel > 0) this.buildsAccessLevel = 20; + if (this.wikiAccessLevel > 0) this.wikiAccessLevel = 20; + if (this.snippetsAccessLevel > 0) this.snippetsAccessLevel = 20; + if (this.pagesAccessLevel === 10) this.pagesAccessLevel = 20; + this.highlightChanges(); + } + }, - repositoryAccessLevel(value, oldValue) { - if (value < oldValue) { - // sub-features cannot have more premissive access level - this.mergeRequestsAccessLevel = Math.min(this.mergeRequestsAccessLevel, value); - this.buildsAccessLevel = Math.min(this.buildsAccessLevel, value); + repositoryAccessLevel(value, oldValue) { + if (value < oldValue) { + // sub-features cannot have more premissive access level + this.mergeRequestsAccessLevel = Math.min(this.mergeRequestsAccessLevel, value); + this.buildsAccessLevel = Math.min(this.buildsAccessLevel, value); - if (value === 0) { - this.containerRegistryEnabled = false; - this.lfsEnabled = false; - } - } else if (oldValue === 0) { - this.mergeRequestsAccessLevel = value; - this.buildsAccessLevel = value; - this.containerRegistryEnabled = true; - this.lfsEnabled = true; + if (value === 0) { + this.containerRegistryEnabled = false; + this.lfsEnabled = false; } - }, + } else if (oldValue === 0) { + this.mergeRequestsAccessLevel = value; + this.buildsAccessLevel = value; + this.containerRegistryEnabled = true; + this.lfsEnabled = true; + } + }, - issuesAccessLevel(value, oldValue) { - if (value === 0) toggleHiddenClassBySelector('.issues-feature', true); - else if (oldValue === 0) toggleHiddenClassBySelector('.issues-feature', false); - }, + issuesAccessLevel(value, oldValue) { + if (value === 0) toggleHiddenClassBySelector('.issues-feature', true); + else if (oldValue === 0) toggleHiddenClassBySelector('.issues-feature', false); + }, - mergeRequestsAccessLevel(value, oldValue) { - if (value === 0) toggleHiddenClassBySelector('.merge-requests-feature', true); - else if (oldValue === 0) toggleHiddenClassBySelector('.merge-requests-feature', false); - }, + mergeRequestsAccessLevel(value, oldValue) { + if (value === 0) toggleHiddenClassBySelector('.merge-requests-feature', true); + else if (oldValue === 0) toggleHiddenClassBySelector('.merge-requests-feature', false); + }, - buildsAccessLevel(value, oldValue) { - if (value === 0) toggleHiddenClassBySelector('.builds-feature', true); - else if (oldValue === 0) toggleHiddenClassBySelector('.builds-feature', false); - }, + buildsAccessLevel(value, oldValue) { + if (value === 0) toggleHiddenClassBySelector('.builds-feature', true); + else if (oldValue === 0) toggleHiddenClassBySelector('.builds-feature', false); }, + }, - methods: { - highlightChanges() { - this.highlightChangesClass = true; - this.$nextTick(() => { - this.highlightChangesClass = false; - }); - }, + methods: { + highlightChanges() { + this.highlightChangesClass = true; + this.$nextTick(() => { + this.highlightChangesClass = false; + }); + }, - visibilityAllowed(option) { - return this.allowedVisibilityOptions.includes(option); - }, + visibilityAllowed(option) { + return this.allowedVisibilityOptions.includes(option); }, - }; + }, +}; </script> <template> diff --git a/app/assets/javascripts/pages/projects/wikis/components/delete_wiki_modal.vue b/app/assets/javascripts/pages/projects/wikis/components/delete_wiki_modal.vue index 75cb6374ad5..f970a5ebb64 100644 --- a/app/assets/javascripts/pages/projects/wikis/components/delete_wiki_modal.vue +++ b/app/assets/javascripts/pages/projects/wikis/components/delete_wiki_modal.vue @@ -1,8 +1,15 @@ <script> import _ from 'underscore'; import { s__, sprintf } from '~/locale'; +import { GlModal, GlModalDirective } from '@gitlab-org/gitlab-ui'; export default { + components: { + GlModal, + }, + directives: { + 'gl-modal': GlModalDirective, + }, props: { deleteWikiUrl: { type: String, @@ -54,7 +61,7 @@ export default { > {{ __('Delete') }} </button> - <gl-ui-modal + <gl-modal :title="title" :ok-title="s__('WikiPageConfirmDelete|Delete page')" :modal-id="modalId" @@ -81,6 +88,6 @@ export default { name="authenticity_token" /> </form> - </gl-ui-modal> + </gl-modal> </div> </template> diff --git a/app/assets/javascripts/pipelines/components/pipelines.vue b/app/assets/javascripts/pipelines/components/pipelines.vue index ea526cf1309..fcd8a54c9c1 100644 --- a/app/assets/javascripts/pipelines/components/pipelines.vue +++ b/app/assets/javascripts/pipelines/components/pipelines.vue @@ -155,14 +155,6 @@ export default { ); }, - shouldRenderPagination() { - return ( - !this.isLoading && - this.state.pipelines.length && - this.state.pageInfo.total > this.state.pageInfo.perPage - ); - }, - emptyTabMessage() { const { scopes } = this.$options; const possibleScopes = [scopes.pending, scopes.running, scopes.finished]; @@ -232,36 +224,6 @@ export default { this.setCommonData(resp.data.pipelines); } }, - /** - * Handles URL and query parameter changes. - * When the user uses the pagination or the tabs, - * - update URL - * - Make API request to the server with new parameters - * - Update the polling function - * - Update the internal state - */ - updateContent(parameters) { - this.updateInternalState(parameters); - - // fetch new data - return this.service - .getPipelines(this.requestData) - .then(response => { - this.isLoading = false; - this.successCallback(response); - - // restart polling - this.poll.restart({ data: this.requestData }); - }) - .catch(() => { - this.isLoading = false; - this.errorCallback(); - - // restart polling - this.poll.restart({ data: this.requestData }); - }); - }, - handleResetRunnersCache(endpoint) { this.isResetCacheButtonLoading = true; diff --git a/app/assets/javascripts/pipelines/components/pipelines_actions.vue b/app/assets/javascripts/pipelines/components/pipelines_actions.vue index a7507fb3b6f..07a4af3e61e 100644 --- a/app/assets/javascripts/pipelines/components/pipelines_actions.vue +++ b/app/assets/javascripts/pipelines/components/pipelines_actions.vue @@ -29,7 +29,7 @@ export default { if (action.scheduled_at) { const confirmationMessage = sprintf( s__( - "DelayedJobs|Are you sure you want to run %{jobName} immediately? This job will run automatically after it's timer finishes.", + "DelayedJobs|Are you sure you want to run %{jobName} immediately? Otherwise this job will run automatically after it's timer finishes.", ), { jobName: action.name }, ); diff --git a/app/assets/javascripts/pipelines/mixins/pipelines.js b/app/assets/javascripts/pipelines/mixins/pipelines.js index 8929b397f6c..85781f548c6 100644 --- a/app/assets/javascripts/pipelines/mixins/pipelines.js +++ b/app/assets/javascripts/pipelines/mixins/pipelines.js @@ -23,6 +23,15 @@ export default { hasMadeRequest: false, }; }, + computed: { + shouldRenderPagination() { + return ( + !this.isLoading && + this.state.pipelines.length && + this.state.pageInfo.total > this.state.pageInfo.perPage + ); + }, + }, beforeMount() { this.poll = new Poll({ resource: this.service, @@ -65,6 +74,35 @@ export default { this.poll.stop(); }, methods: { + /** + * Handles URL and query parameter changes. + * When the user uses the pagination or the tabs, + * - update URL + * - Make API request to the server with new parameters + * - Update the polling function + * - Update the internal state + */ + updateContent(parameters) { + this.updateInternalState(parameters); + + // fetch new data + return this.service + .getPipelines(this.requestData) + .then(response => { + this.isLoading = false; + this.successCallback(response); + + // restart polling + this.poll.restart({ data: this.requestData }); + }) + .catch(() => { + this.isLoading = false; + this.errorCallback(); + + // restart polling + this.poll.restart({ data: this.requestData }); + }); + }, updateTable() { // Cancel ongoing request if (this.isMakingRequest) { diff --git a/app/assets/javascripts/projects/project_new.js b/app/assets/javascripts/projects/project_new.js index ebe18b47e4e..998554d1be5 100644 --- a/app/assets/javascripts/projects/project_new.js +++ b/app/assets/javascripts/projects/project_new.js @@ -4,8 +4,10 @@ import { slugifyWithHyphens } from '../lib/utils/text_utility'; let hasUserDefinedProjectPath = false; -const deriveProjectPathFromUrl = ($projectImportUrl) => { - const $currentProjectPath = $projectImportUrl.parents('.toggle-import-form').find('#project_path'); +const deriveProjectPathFromUrl = $projectImportUrl => { + const $currentProjectPath = $projectImportUrl + .parents('.toggle-import-form') + .find('#project_path'); if (hasUserDefinedProjectPath) { return; } @@ -52,9 +54,11 @@ const bindEvents = () => { return; } - $('.how_to_import_link').on('click', (e) => { + $('.how_to_import_link').on('click', e => { e.preventDefault(); - $(e.currentTarget).next('.modal').show(); + $(e.currentTarget) + .next('.modal') + .show(); }); $('.modal-header .close').on('click', () => { @@ -63,15 +67,21 @@ const bindEvents = () => { $('.btn_import_gitlab_project').on('click', () => { const importHref = $('a.btn_import_gitlab_project').attr('href'); - $('.btn_import_gitlab_project') - .attr('href', `${importHref}?namespace_id=${$('#project_namespace_id').val()}&name=${$projectName.val()}&path=${$projectPath.val()}`); + $('.btn_import_gitlab_project').attr( + 'href', + `${importHref}?namespace_id=${$( + '#project_namespace_id', + ).val()}&name=${$projectName.val()}&path=${$projectPath.val()}`, + ); }); if ($pushNewProjectTipTrigger) { $pushNewProjectTipTrigger .removeAttr('rel') .removeAttr('target') - .on('click', (e) => { e.preventDefault(); }) + .on('click', e => { + e.preventDefault(); + }) .popover({ title: $pushNewProjectTipTrigger.data('title'), placement: 'bottom', @@ -79,13 +89,15 @@ const bindEvents = () => { content: $('.push-new-project-tip-template').html(), }) .on('shown.bs.popover', () => { - $(document).on('click.popover touchstart.popover', (event) => { + $(document).on('click.popover touchstart.popover', event => { if ($(event.target).closest('.popover').length === 0) { $pushNewProjectTipTrigger.trigger('click'); } }); - const target = $(`#${$pushNewProjectTipTrigger.attr('aria-describedby')}`).find('.js-select-on-focus'); + const target = $(`#${$pushNewProjectTipTrigger.attr('aria-describedby')}`).find( + '.js-select-on-focus', + ); addSelectOnFocusBehaviour(target); target.focus(); @@ -117,16 +129,18 @@ const bindEvents = () => { const selectedTemplate = templates[value]; $selectedTemplateText.text(selectedTemplate.text); - $(selectedTemplate.icon).clone().addClass('d-block').appendTo($selectedIcon); + $(selectedTemplate.icon) + .clone() + .addClass('d-block') + .appendTo($selectedIcon); const $activeTabProjectName = $('.tab-pane.active #project_name'); const $activeTabProjectPath = $('.tab-pane.active #project_path'); $activeTabProjectName.focus(); - $activeTabProjectName - .keyup(() => { - onProjectNameChange($activeTabProjectName, $activeTabProjectPath); - hasUserDefinedProjectPath = $activeTabProjectPath.val().trim().length > 0; - }); + $activeTabProjectName.keyup(() => { + onProjectNameChange($activeTabProjectName, $activeTabProjectPath); + hasUserDefinedProjectPath = $activeTabProjectPath.val().trim().length > 0; + }); } $useTemplateBtn.on('change', chooseTemplate); diff --git a/app/assets/javascripts/reports/components/issues_list.vue b/app/assets/javascripts/reports/components/issues_list.vue index 3b425ee2fed..f4243522ef8 100644 --- a/app/assets/javascripts/reports/components/issues_list.vue +++ b/app/assets/javascripts/reports/components/issues_list.vue @@ -1,18 +1,31 @@ <script> -import IssuesBlock from '~/reports/components/report_issues.vue'; -import { STATUS_SUCCESS, STATUS_FAILED, STATUS_NEUTRAL } from '~/reports/constants'; +import ReportItem from '~/reports/components/report_item.vue'; +import { STATUS_FAILED, STATUS_NEUTRAL, STATUS_SUCCESS } from '~/reports/constants'; +import SmartVirtualList from '~/vue_shared/components/smart_virtual_list.vue'; + +const wrapIssueWithState = (status, isNew = false) => issue => ({ + status: issue.status || status, + isNew, + issue, +}); /** * Renders block of issues */ - export default { components: { - IssuesBlock, + SmartVirtualList, + ReportItem, }, - success: STATUS_SUCCESS, - failed: STATUS_FAILED, - neutral: STATUS_NEUTRAL, + // Typical height of a report item in px + typicalReportItemHeight: 32, + /* + The maximum amount of shown issues. This is calculated by + ( max-height of report-block-list / typicalReportItemHeight ) + some safety margin + We will use VirtualList if we have more items than this number. + For entries lower than this number, the virtual scroll list calculates the total height of the element wrongly. + */ + maxShownReportItems: 20, props: { newIssues: { type: Array, @@ -40,42 +53,34 @@ export default { default: '', }, }, + computed: { + issuesWithState() { + return [ + ...this.newIssues.map(wrapIssueWithState(STATUS_FAILED, true)), + ...this.unresolvedIssues.map(wrapIssueWithState(STATUS_FAILED)), + ...this.neutralIssues.map(wrapIssueWithState(STATUS_NEUTRAL)), + ...this.resolvedIssues.map(wrapIssueWithState(STATUS_SUCCESS)), + ]; + }, + }, }; </script> <template> - <div class="report-block-container"> - - <issues-block - v-if="newIssues.length" - :component="component" - :issues="newIssues" - class="js-mr-code-new-issues" - status="failed" - is-new - /> - - <issues-block - v-if="unresolvedIssues.length" - :component="component" - :issues="unresolvedIssues" - :status="$options.failed" - class="js-mr-code-new-issues" - /> - - <issues-block - v-if="neutralIssues.length" - :component="component" - :issues="neutralIssues" - :status="$options.neutral" - class="js-mr-code-non-issues" - /> - - <issues-block - v-if="resolvedIssues.length" + <smart-virtual-list + :length="issuesWithState.length" + :remain="$options.maxShownReportItems" + :size="$options.typicalReportItemHeight" + class="report-block-container" + wtag="ul" + wclass="report-block-list" + > + <report-item + v-for="(wrapped, index) in issuesWithState" + :key="index" + :issue="wrapped.issue" + :status="wrapped.status" :component="component" - :issues="resolvedIssues" - :status="$options.success" - class="js-mr-code-resolved-issues" + :is-new="wrapped.isNew" /> - </div> + </smart-virtual-list> </template> diff --git a/app/assets/javascripts/reports/components/report_issues.vue b/app/assets/javascripts/reports/components/report_item.vue index a2a03945ae3..01e6d357a21 100644 --- a/app/assets/javascripts/reports/components/report_issues.vue +++ b/app/assets/javascripts/reports/components/report_item.vue @@ -3,14 +3,14 @@ import IssueStatusIcon from '~/reports/components/issue_status_icon.vue'; import { components, componentNames } from '~/reports/components/issue_body'; export default { - name: 'ReportIssues', + name: 'ReportItem', components: { IssueStatusIcon, ...components, }, props: { - issues: { - type: Array, + issue: { + type: Object, required: true, }, component: { @@ -33,27 +33,21 @@ export default { }; </script> <template> - <div> - <ul class="report-block-list"> - <li - v-for="(issue, index) in issues" - :key="index" - :class="{ 'is-dismissed': issue.isDismissed }" - class="report-block-list-issue" - > - <issue-status-icon - :status="issue.status || status" - class="append-right-5" - /> + <li + :class="{ 'is-dismissed': issue.isDismissed }" + class="report-block-list-issue" + > + <issue-status-icon + :status="status" + class="append-right-5" + /> - <component - :is="component" - v-if="component" - :issue="issue" - :status="issue.status || status" - :is-new="isNew" - /> - </li> - </ul> - </div> + <component + :is="component" + v-if="component" + :issue="issue" + :status="status" + :is-new="isNew" + /> + </li> </template> diff --git a/app/assets/javascripts/right_sidebar.js b/app/assets/javascripts/right_sidebar.js index 6b3753f7966..225e21ad322 100644 --- a/app/assets/javascripts/right_sidebar.js +++ b/app/assets/javascripts/right_sidebar.js @@ -21,7 +21,7 @@ Sidebar.initialize = function(currentUser) { } }; -Sidebar.prototype.removeListeners = function () { +Sidebar.prototype.removeListeners = function() { this.sidebar.off('click', '.sidebar-collapsed-icon'); this.sidebar.off('hidden.gl.dropdown'); $('.dropdown').off('loading.gl.dropdown'); @@ -38,10 +38,12 @@ Sidebar.prototype.addEventListeners = function() { $('.dropdown').on('loaded.gl.dropdown', this.sidebarDropdownLoaded); $document.on('click', '.js-sidebar-toggle', this.sidebarToggleClicked); - return $(document).off('click', '.js-issuable-todo').on('click', '.js-issuable-todo', this.toggleTodo); + return $(document) + .off('click', '.js-issuable-todo') + .on('click', '.js-issuable-todo', this.toggleTodo); }; -Sidebar.prototype.sidebarToggleClicked = function (e, triggered) { +Sidebar.prototype.sidebarToggleClicked = function(e, triggered) { var $allGutterToggleIcons, $this, isExpanded, tooltipLabel; e.preventDefault(); $this = $(this); @@ -51,18 +53,26 @@ Sidebar.prototype.sidebarToggleClicked = function (e, triggered) { if (isExpanded) { $allGutterToggleIcons.removeClass('fa-angle-double-right').addClass('fa-angle-double-left'); - $('aside.right-sidebar').removeClass('right-sidebar-expanded').addClass('right-sidebar-collapsed'); - $('.layout-page').removeClass('right-sidebar-expanded').addClass('right-sidebar-collapsed'); + $('aside.right-sidebar') + .removeClass('right-sidebar-expanded') + .addClass('right-sidebar-collapsed'); + $('.layout-page') + .removeClass('right-sidebar-expanded') + .addClass('right-sidebar-collapsed'); } else { $allGutterToggleIcons.removeClass('fa-angle-double-left').addClass('fa-angle-double-right'); - $('aside.right-sidebar').removeClass('right-sidebar-collapsed').addClass('right-sidebar-expanded'); - $('.layout-page').removeClass('right-sidebar-collapsed').addClass('right-sidebar-expanded'); + $('aside.right-sidebar') + .removeClass('right-sidebar-collapsed') + .addClass('right-sidebar-expanded'); + $('.layout-page') + .removeClass('right-sidebar-collapsed') + .addClass('right-sidebar-expanded'); } $this.attr('data-original-title', tooltipLabel); if (!triggered) { - Cookies.set("collapsed_gutter", $('.right-sidebar').hasClass('right-sidebar-collapsed')); + Cookies.set('collapsed_gutter', $('.right-sidebar').hasClass('right-sidebar-collapsed')); } }; @@ -71,21 +81,27 @@ Sidebar.prototype.toggleTodo = function(e) { $this = $(e.currentTarget); ajaxType = $this.attr('data-delete-path') ? 'delete' : 'post'; if ($this.attr('data-delete-path')) { - url = "" + ($this.attr('data-delete-path')); + url = '' + $this.attr('data-delete-path'); } else { - url = "" + ($this.data('url')); + url = '' + $this.data('url'); } $this.tooltip('hide'); - $('.js-issuable-todo').disable().addClass('is-loading'); + $('.js-issuable-todo') + .disable() + .addClass('is-loading'); axios[ajaxType](url, { issuable_id: $this.data('issuableId'), issuable_type: $this.data('issuableType'), - }).then(({ data }) => { - this.todoUpdateDone(data); - }).catch(() => flash(`There was an error ${ajaxType === 'post' ? 'adding a' : 'deleting the'} todo.`)); + }) + .then(({ data }) => { + this.todoUpdateDone(data); + }) + .catch(() => + flash(`There was an error ${ajaxType === 'post' ? 'adding a' : 'deleting the'} todo.`), + ); }; Sidebar.prototype.todoUpdateDone = function(data) { @@ -99,7 +115,8 @@ Sidebar.prototype.todoUpdateDone = function(data) { const $el = $(el); const $elText = $el.find('.js-issuable-todo-inner'); - $el.removeClass('is-loading') + $el + .removeClass('is-loading') .enable() .attr('aria-label', $el.data(`${attrPrefix}Text`)) .attr('data-delete-path', deletePath) @@ -119,7 +136,9 @@ Sidebar.prototype.todoUpdateDone = function(data) { Sidebar.prototype.sidebarDropdownLoading = function(e) { var $loading, $sidebarCollapsedIcon, i, img; - $sidebarCollapsedIcon = $(this).closest('.block').find('.sidebar-collapsed-icon'); + $sidebarCollapsedIcon = $(this) + .closest('.block') + .find('.sidebar-collapsed-icon'); img = $sidebarCollapsedIcon.find('img'); i = $sidebarCollapsedIcon.find('i'); $loading = $('<i class="fa fa-spinner fa-spin"></i>'); @@ -134,7 +153,9 @@ Sidebar.prototype.sidebarDropdownLoading = function(e) { Sidebar.prototype.sidebarDropdownLoaded = function(e) { var $sidebarCollapsedIcon, i, img; - $sidebarCollapsedIcon = $(this).closest('.block').find('.sidebar-collapsed-icon'); + $sidebarCollapsedIcon = $(this) + .closest('.block') + .find('.sidebar-collapsed-icon'); img = $sidebarCollapsedIcon.find('img'); $sidebarCollapsedIcon.find('i.fa-spin').remove(); i = $sidebarCollapsedIcon.find('i'); @@ -220,7 +241,7 @@ Sidebar.prototype.isOpen = function() { }; Sidebar.prototype.getBlock = function(name) { - return this.sidebar.find(".block." + name); + return this.sidebar.find('.block.' + name); }; export default Sidebar; diff --git a/app/assets/javascripts/search_autocomplete.js b/app/assets/javascripts/search_autocomplete.js index ad4f5320ff8..17def77b2d7 100644 --- a/app/assets/javascripts/search_autocomplete.js +++ b/app/assets/javascripts/search_autocomplete.js @@ -234,7 +234,9 @@ export class SearchAutocomplete { icon, text: term, template, - url: `${gon.relative_url_root}/search?search=${term}&project_id=${this.projectInputEl.val()}&group_id=${this.groupInputEl.val()}`, + url: `${ + gon.relative_url_root + }/search?search=${term}&project_id=${this.projectInputEl.val()}&group_id=${this.groupInputEl.val()}`, }); } } diff --git a/app/assets/javascripts/set_status_modal/set_status_modal_wrapper.vue b/app/assets/javascripts/set_status_modal/set_status_modal_wrapper.vue index 43f0b6651b9..4d461baf74d 100644 --- a/app/assets/javascripts/set_status_modal/set_status_modal_wrapper.vue +++ b/app/assets/javascripts/set_status_modal/set_status_modal_wrapper.vue @@ -5,6 +5,7 @@ import Icon from '~/vue_shared/components/icon.vue'; import GfmAutoComplete from '~/gfm_auto_complete'; import { __, s__ } from '~/locale'; import Api from '~/api'; +import { GlModal, GlTooltipDirective } from '@gitlab-org/gitlab-ui'; import eventHub from './event_hub'; import EmojiMenuInModal from './emoji_menu_in_modal'; @@ -13,6 +14,10 @@ const emojiMenuClass = 'js-modal-status-emoji-menu'; export default { components: { Icon, + GlModal, + }, + directives: { + GlTooltip: GlTooltipDirective, }, props: { currentEmoji: { @@ -152,7 +157,7 @@ export default { </script> <template> - <gl-ui-modal + <gl-modal :title="s__('SetStatusModal|Set a status')" :modal-id="modalId" :ok-title="s__('SetStatusModal|Set status')" @@ -237,5 +242,5 @@ export default { </div> </div> </div> - </gl-ui-modal> + </gl-modal> </template> diff --git a/app/assets/javascripts/sidebar/components/assignees/assignees.vue b/app/assets/javascripts/sidebar/components/assignees/assignees.vue index dd155c133ce..f1ea6aacdb2 100644 --- a/app/assets/javascripts/sidebar/components/assignees/assignees.vue +++ b/app/assets/javascripts/sidebar/components/assignees/assignees.vue @@ -74,8 +74,8 @@ export default { } if (!this.users.length) { - const emptyTooltipLabel = this.issuableType === 'issue' ? - __('Assignee(s)') : __('Assignee'); + const emptyTooltipLabel = + this.issuableType === 'issue' ? __('Assignee(s)') : __('Assignee'); names.push(emptyTooltipLabel); } @@ -248,4 +248,3 @@ export default { </div> </div> </template> - diff --git a/app/assets/javascripts/sidebar/components/subscriptions/subscriptions.vue b/app/assets/javascripts/sidebar/components/subscriptions/subscriptions.vue index 448c8fc3602..b6151aa6c64 100644 --- a/app/assets/javascripts/sidebar/components/subscriptions/subscriptions.vue +++ b/app/assets/javascripts/sidebar/components/subscriptions/subscriptions.vue @@ -1,74 +1,74 @@ <script> - import { __ } from '~/locale'; - import icon from '~/vue_shared/components/icon.vue'; - import toggleButton from '~/vue_shared/components/toggle_button.vue'; - import tooltip from '~/vue_shared/directives/tooltip'; - import eventHub from '../../event_hub'; +import { __ } from '~/locale'; +import icon from '~/vue_shared/components/icon.vue'; +import toggleButton from '~/vue_shared/components/toggle_button.vue'; +import tooltip from '~/vue_shared/directives/tooltip'; +import eventHub from '../../event_hub'; - const ICON_ON = 'notifications'; - const ICON_OFF = 'notifications-off'; - const LABEL_ON = __('Notifications on'); - const LABEL_OFF = __('Notifications off'); +const ICON_ON = 'notifications'; +const ICON_OFF = 'notifications-off'; +const LABEL_ON = __('Notifications on'); +const LABEL_OFF = __('Notifications off'); - export default { - directives: { - tooltip, +export default { + directives: { + tooltip, + }, + components: { + icon, + toggleButton, + }, + props: { + loading: { + type: Boolean, + required: false, + default: false, }, - components: { - icon, - toggleButton, + subscribed: { + type: Boolean, + required: false, + default: null, }, - props: { - loading: { - type: Boolean, - required: false, - default: false, - }, - subscribed: { - type: Boolean, - required: false, - default: null, - }, - id: { - type: Number, - required: false, - default: null, - }, + id: { + type: Number, + required: false, + default: null, }, - computed: { - showLoadingState() { - return this.subscribed === null; - }, - notificationIcon() { - return this.subscribed ? ICON_ON : ICON_OFF; - }, - notificationTooltip() { - return this.subscribed ? LABEL_ON : LABEL_OFF; - }, + }, + computed: { + showLoadingState() { + return this.subscribed === null; }, - methods: { - /** - * We need to emit this event on both component & eventHub - * for 2 dependencies; - * - * 1. eventHub: This component is used in Issue Boards sidebar - * where component template is part of HAML - * and event listeners are tied to app's eventHub. - * 2. Component: This compone is also used in Epics in EE - * where listeners are tied to component event. - */ - toggleSubscription() { - // App's eventHub event emission. - eventHub.$emit('toggleSubscription', this.id); + notificationIcon() { + return this.subscribed ? ICON_ON : ICON_OFF; + }, + notificationTooltip() { + return this.subscribed ? LABEL_ON : LABEL_OFF; + }, + }, + methods: { + /** + * We need to emit this event on both component & eventHub + * for 2 dependencies; + * + * 1. eventHub: This component is used in Issue Boards sidebar + * where component template is part of HAML + * and event listeners are tied to app's eventHub. + * 2. Component: This compone is also used in Epics in EE + * where listeners are tied to component event. + */ + toggleSubscription() { + // App's eventHub event emission. + eventHub.$emit('toggleSubscription', this.id); - // Component event emission. - this.$emit('toggleSubscription', this.id); - }, - onClickCollapsedIcon() { - this.$emit('toggleSidebar'); - }, + // Component event emission. + this.$emit('toggleSubscription', this.id); + }, + onClickCollapsedIcon() { + this.$emit('toggleSidebar'); }, - }; + }, +}; </script> <template> diff --git a/app/assets/javascripts/sidebar/components/time_tracking/comparison_pane.vue b/app/assets/javascripts/sidebar/components/time_tracking/comparison_pane.vue index e74912d628f..b145e5dc5e2 100644 --- a/app/assets/javascripts/sidebar/components/time_tracking/comparison_pane.vue +++ b/app/assets/javascripts/sidebar/components/time_tracking/comparison_pane.vue @@ -1,9 +1,13 @@ <script> import { parseSeconds, stringifyTime } from '~/lib/utils/datetime_utility'; import tooltip from '../../../vue_shared/directives/tooltip'; +import { GlProgressBar } from '@gitlab-org/gitlab-ui'; export default { name: 'TimeTrackingComparisonPane', + components: { + GlProgressBar, + }, directives: { tooltip, }, diff --git a/app/assets/javascripts/sidebar/sidebar_mediator.js b/app/assets/javascripts/sidebar/sidebar_mediator.js index d9ca5e46770..3e040ec8428 100644 --- a/app/assets/javascripts/sidebar/sidebar_mediator.js +++ b/app/assets/javascripts/sidebar/sidebar_mediator.js @@ -39,9 +39,10 @@ export default class SidebarMediator { } fetch() { - return this.service.get() + return this.service + .get() .then(response => response.json()) - .then((data) => { + .then(data => { this.processFetchedData(data); }) .catch(() => new Flash('Error occurred when fetching sidebar data')); @@ -56,30 +57,33 @@ export default class SidebarMediator { toggleSubscription() { this.store.setFetchingState('subscriptions', true); - return this.service.toggleSubscription() + return this.service + .toggleSubscription() .then(() => { this.store.setSubscribedState(!this.store.subscribed); this.store.setFetchingState('subscriptions', false); }) - .catch((err) => { + .catch(err => { this.store.setFetchingState('subscriptions', false); throw err; }); } fetchAutocompleteProjects(searchTerm) { - return this.service.getProjectsAutocomplete(searchTerm) + return this.service + .getProjectsAutocomplete(searchTerm) .then(response => response.json()) - .then((data) => { + .then(data => { this.store.setAutocompleteProjects(data); return this.store.autocompleteProjects; }); } moveIssue() { - return this.service.moveIssue(this.store.moveToProjectId) + return this.service + .moveIssue(this.store.moveToProjectId) .then(response => response.json()) - .then((data) => { + .then(data => { if (window.location.pathname !== data.web_url) { visitUrl(data.web_url); } diff --git a/app/assets/javascripts/vue_merge_request_widget/components/deployment.vue b/app/assets/javascripts/vue_merge_request_widget/components/deployment.vue index 57c52a2016a..2a8380f5f2b 100644 --- a/app/assets/javascripts/vue_merge_request_widget/components/deployment.vue +++ b/app/assets/javascripts/vue_merge_request_widget/components/deployment.vue @@ -65,6 +65,14 @@ export default { deployedText() { return this.$options.deployedTextMap[this.deployment.status]; }, + isDeployInProgress() { + return this.deployment.status === 'running'; + }, + deployInProgressTooltip() { + return this.isDeployInProgress + ? __('Stopping this environment is currently not possible as a deployment is in progress') + : ''; + }, shouldRenderDropdown() { return ( this.enableCiEnvironmentsStatusChanges && @@ -183,15 +191,23 @@ export default { css-class="js-deploy-url js-deploy-url-feature-flag deploy-link btn btn-default btn-sm inlin" /> </template> - <loading-button + <span v-if="deployment.stop_url" - :loading="isStopping" - container-class="btn btn-default btn-sm inline prepend-left-4" - title="Stop environment" - @click="stopEnvironment" + v-tooltip + :title="deployInProgressTooltip" + class="d-inline-block" + tabindex="0" > - <icon name="stop" /> - </loading-button> + <loading-button + :loading="isStopping" + :disabled="isDeployInProgress" + :title="__('Stop environment')" + container-class="js-stop-env btn btn-default btn-sm inline prepend-left-4" + @click="stopEnvironment" + > + <icon name="stop" /> + </loading-button> + </span> </div> </div> </div> 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 bd1946f337e..53608838f2f 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 @@ -63,10 +63,16 @@ export default { return this.pipeline.commit && Object.keys(this.pipeline.commit).length > 0; }, errorText() { - return sprintf(__('Could not retrieve the pipeline status. For troubleshooting steps, read the %{linkStart}documentation.%{linkEnd}'), { - linkStart: `<a href="${this.troubleshootingDocsPath}">`, - linkEnd: '</a>', - }); + return sprintf( + __( + 'Could not retrieve the pipeline status. For troubleshooting steps, read the %{linkStart}documentation.%{linkEnd}', + ), + { + linkStart: `<a href="${this.troubleshootingDocsPath}">`, + linkEnd: '</a>', + }, + false, + ); }, }, }; diff --git a/app/assets/javascripts/vue_merge_request_widget/components/states/ready_to_merge.vue b/app/assets/javascripts/vue_merge_request_widget/components/states/ready_to_merge.vue index c8ad2aa30a6..e7baecbcde4 100644 --- a/app/assets/javascripts/vue_merge_request_widget/components/states/ready_to_merge.vue +++ b/app/assets/javascripts/vue_merge_request_widget/components/states/ready_to_merge.vue @@ -71,7 +71,12 @@ export default { return defaultClass; }, iconClass() { - if (this.status === 'failed' || !this.commitMessage.length || !this.mr.isMergeAllowed || this.mr.preventMerge) { + if ( + this.status === 'failed' || + !this.commitMessage.length || + !this.mr.isMergeAllowed || + this.mr.preventMerge + ) { return 'warning'; } return 'success'; @@ -90,10 +95,12 @@ export default { }, isMergeButtonDisabled() { const { commitMessage } = this; - return Boolean(!commitMessage.length - || !this.shouldShowMergeControls() - || this.isMakingRequest - || this.mr.preventMerge); + return Boolean( + !commitMessage.length || + !this.shouldShowMergeControls() || + this.isMakingRequest || + this.mr.preventMerge, + ); }, isRemoveSourceBranchButtonDisabled() { return this.isMergeButtonDisabled; @@ -140,9 +147,10 @@ export default { }; this.isMakingRequest = true; - this.service.merge(options) + this.service + .merge(options) .then(res => res.data) - .then((data) => { + .then(data => { const hasError = data.status === 'failed' || data.status === 'hook_validation_error'; if (data.status === 'merge_when_pipeline_succeeds') { @@ -167,9 +175,10 @@ export default { }); }, handleMergePolling(continuePolling, stopPolling) { - this.service.poll() + this.service + .poll() .then(res => res.data) - .then((data) => { + .then(data => { if (data.state === 'merged') { // If state is merged we should update the widget and stop the polling eventHub.$emit('MRWidgetUpdateRequested'); @@ -205,9 +214,10 @@ export default { }); }, handleRemoveBranchPolling(continuePolling, stopPolling) { - this.service.poll() + this.service + .poll() .then(res => res.data) - .then((data) => { + .then(data => { // If source branch exists then we should continue polling // because removing a source branch is a background task and takes time if (data.source_branch_exists) { diff --git a/app/assets/javascripts/vue_merge_request_widget/mr_widget_options.vue b/app/assets/javascripts/vue_merge_request_widget/mr_widget_options.vue index 33064f82ef7..063d1e15544 100644 --- a/app/assets/javascripts/vue_merge_request_widget/mr_widget_options.vue +++ b/app/assets/javascripts/vue_merge_request_widget/mr_widget_options.vue @@ -116,7 +116,7 @@ export default { // init polling this.initPostMergeDeploymentsPolling(); } - } + }, }, created() { this.initPolling(); @@ -213,17 +213,21 @@ export default { }) .catch(() => this.throwDeploymentsError()); }, - fetchPostMergeDeployments(){ + fetchPostMergeDeployments() { return this.fetchDeployments('merge_commit') .then(({ data }) => { if (data.length) { this.mr.postMergeDeployments = data; } }) - .catch(() => this.throwDeploymentsError()); + .catch(() => this.throwDeploymentsError()); }, throwDeploymentsError() { - createFlash(__('Something went wrong while fetching the environments for this merge request. Please try again.')); + createFlash( + __( + 'Something went wrong while fetching the environments for this merge request. Please try again.', + ), + ); }, fetchActionsContent() { this.service diff --git a/app/assets/javascripts/vue_merge_request_widget/services/mr_widget_service.js b/app/assets/javascripts/vue_merge_request_widget/services/mr_widget_service.js index bf5b85b2ae6..0bb70bfd658 100644 --- a/app/assets/javascripts/vue_merge_request_widget/services/mr_widget_service.js +++ b/app/assets/javascripts/vue_merge_request_widget/services/mr_widget_service.js @@ -24,8 +24,8 @@ export default class MRWidgetService { fetchDeployments(targetParam) { return axios.get(this.endpoints.ciEnvironmentsStatusPath, { params: { - environment_target: targetParam - } + environment_target: targetParam, + }, }); } diff --git a/app/assets/javascripts/vue_merge_request_widget/stores/mr_widget_store.js b/app/assets/javascripts/vue_merge_request_widget/stores/mr_widget_store.js index c17e9c9e4d6..5c9a7133a6e 100644 --- a/app/assets/javascripts/vue_merge_request_widget/stores/mr_widget_store.js +++ b/app/assets/javascripts/vue_merge_request_widget/stores/mr_widget_store.js @@ -18,8 +18,7 @@ export default class MergeRequestStore { this.squash = data.squash; this.squashBeforeMergeHelpPath = this.squashBeforeMergeHelpPath || data.squash_before_merge_help_path; - this.troubleshootingDocsPath = - this.troubleshootingDocsPath || data.troubleshooting_docs_path; + this.troubleshootingDocsPath = this.troubleshootingDocsPath || data.troubleshooting_docs_path; this.enableSquashBeforeMerge = this.enableSquashBeforeMerge || true; this.iid = data.iid; diff --git a/app/assets/javascripts/vue_shared/components/commit.vue b/app/assets/javascripts/vue_shared/components/commit.vue index 13bca99dcb3..151eee75d44 100644 --- a/app/assets/javascripts/vue_shared/components/commit.vue +++ b/app/assets/javascripts/vue_shared/components/commit.vue @@ -13,7 +13,7 @@ export default { }, props: { /** - * Indicates the existance of a tag. + * Indicates the existence of a tag. * Used to render the correct icon, if true will render `fa-tag` icon, * if false will render a svg sprite fork icon */ diff --git a/app/assets/javascripts/vue_shared/components/filtered_search_dropdown.vue b/app/assets/javascripts/vue_shared/components/filtered_search_dropdown.vue index 460fa6ad72e..388a2f4ca36 100644 --- a/app/assets/javascripts/vue_shared/components/filtered_search_dropdown.vue +++ b/app/assets/javascripts/vue_shared/components/filtered_search_dropdown.vue @@ -56,12 +56,14 @@ export default { filteredResults() { if (this.filter !== '') { return this.items.filter( - item => item[this.filterKey] && item[this.filterKey].toLowerCase().includes(this.filter.toLowerCase()), + item => + item[this.filterKey] && + item[this.filterKey].toLowerCase().includes(this.filter.toLowerCase()), ); } return this.items.slice(0, this.visibleItems); - } + }, }, mounted() { /** diff --git a/app/assets/javascripts/vue_shared/components/gl_countdown.vue b/app/assets/javascripts/vue_shared/components/gl_countdown.vue index 9327a2a4a6c..a35986b2d03 100644 --- a/app/assets/javascripts/vue_shared/components/gl_countdown.vue +++ b/app/assets/javascripts/vue_shared/components/gl_countdown.vue @@ -1,10 +1,14 @@ <script> import { calculateRemainingMilliseconds, formatTime } from '~/lib/utils/datetime_utility'; +import { GlTooltipDirective } from '@gitlab-org/gitlab-ui'; /** * Counts down to a given end date. */ export default { + directives: { + GlTooltip: GlTooltipDirective, + }, props: { endDateString: { type: String, diff --git a/app/assets/javascripts/vue_shared/components/icon.vue b/app/assets/javascripts/vue_shared/components/icon.vue index 26f9d5ddc91..cddebfae115 100644 --- a/app/assets/javascripts/vue_shared/components/icon.vue +++ b/app/assets/javascripts/vue_shared/components/icon.vue @@ -8,7 +8,7 @@ let iconValidator = () => true; */ if (process.env.NODE_ENV !== 'production') { // eslint-disable-next-line global-require - const data = require('@gitlab-org/gitlab-svgs/dist/icons.json'); + const data = require('@gitlab/svgs/dist/icons.json'); const { icons } = data; iconValidator = value => { if (icons.includes(value)) { diff --git a/app/assets/javascripts/vue_shared/components/markdown/header.vue b/app/assets/javascripts/vue_shared/components/markdown/header.vue index 3ddb39730c4..27e3f314dd3 100644 --- a/app/assets/javascripts/vue_shared/components/markdown/header.vue +++ b/app/assets/javascripts/vue_shared/components/markdown/header.vue @@ -1,17 +1,17 @@ <script> import $ from 'jquery'; -import Tooltip from '../../directives/tooltip'; +import { GlTooltipDirective } from '@gitlab-org/gitlab-ui'; import ToolbarButton from './toolbar_button.vue'; import Icon from '../icon.vue'; export default { - directives: { - Tooltip, - }, components: { ToolbarButton, Icon, }, + directives: { + GlTooltip: GlTooltipDirective, + }, props: { previewMarkdown: { type: Boolean, @@ -147,7 +147,7 @@ export default { icon="table" /> <button - v-tooltip + v-gl-tooltip aria-label="Go full screen" class="toolbar-btn toolbar-fullscreen-btn js-zen-enter" data-container="body" diff --git a/app/assets/javascripts/vue_shared/components/markdown/toolbar_button.vue b/app/assets/javascripts/vue_shared/components/markdown/toolbar_button.vue index 3e89e1c1e75..91d0bbfc21c 100644 --- a/app/assets/javascripts/vue_shared/components/markdown/toolbar_button.vue +++ b/app/assets/javascripts/vue_shared/components/markdown/toolbar_button.vue @@ -1,13 +1,13 @@ <script> -import tooltip from '../../directives/tooltip'; -import icon from '../icon.vue'; +import { GlTooltipDirective } from '@gitlab-org/gitlab-ui'; +import Icon from '../icon.vue'; export default { components: { - icon, + Icon, }, directives: { - tooltip, + GlTooltip: GlTooltipDirective, }, props: { buttonTitle: { @@ -43,7 +43,7 @@ export default { <template> <button - v-tooltip + v-gl-tooltip :data-md-tag="tag" :data-md-select="tagSelect" :data-md-block="tagBlock" diff --git a/app/assets/javascripts/vue_shared/components/pagination_links.vue b/app/assets/javascripts/vue_shared/components/pagination_links.vue index 1f2a679c145..89dcf049f6e 100644 --- a/app/assets/javascripts/vue_shared/components/pagination_links.vue +++ b/app/assets/javascripts/vue_shared/components/pagination_links.vue @@ -1,7 +1,11 @@ <script> +import { GlPagination } from '@gitlab-org/gitlab-ui'; import { s__ } from '../../locale'; export default { + components: { + GlPagination, + }, props: { change: { type: Function, diff --git a/app/assets/javascripts/vue_shared/components/sidebar/collapsed_calendar_icon.vue b/app/assets/javascripts/vue_shared/components/sidebar/collapsed_calendar_icon.vue index 11fac3bb12c..5841db52704 100644 --- a/app/assets/javascripts/vue_shared/components/sidebar/collapsed_calendar_icon.vue +++ b/app/assets/javascripts/vue_shared/components/sidebar/collapsed_calendar_icon.vue @@ -1,39 +1,39 @@ <script> - import tooltip from '~/vue_shared/directives/tooltip'; +import tooltip from '~/vue_shared/directives/tooltip'; - export default { - name: 'CollapsedCalendarIcon', - directives: { - tooltip, +export default { + name: 'CollapsedCalendarIcon', + directives: { + tooltip, + }, + props: { + containerClass: { + type: String, + required: false, + default: '', }, - props: { - containerClass: { - type: String, - required: false, - default: '', - }, - text: { - type: String, - required: false, - default: '', - }, - showIcon: { - type: Boolean, - required: false, - default: true, - }, - tooltipText: { - type: String, - required: false, - default: '', - }, + text: { + type: String, + required: false, + default: '', }, - methods: { - click() { - this.$emit('click'); - }, + showIcon: { + type: Boolean, + required: false, + default: true, }, - }; + tooltipText: { + type: String, + required: false, + default: '', + }, + }, + methods: { + click() { + this.$emit('click'); + }, + }, +}; </script> <template> diff --git a/app/assets/javascripts/vue_shared/components/sidebar/collapsed_grouped_date_picker.vue b/app/assets/javascripts/vue_shared/components/sidebar/collapsed_grouped_date_picker.vue index 6e7194ccc9e..174c29809ac 100644 --- a/app/assets/javascripts/vue_shared/components/sidebar/collapsed_grouped_date_picker.vue +++ b/app/assets/javascripts/vue_shared/components/sidebar/collapsed_grouped_date_picker.vue @@ -1,84 +1,79 @@ <script> - import { __ } from '~/locale'; - import timeagoMixin from '~/vue_shared/mixins/timeago'; - import { dateInWords, timeFor } from '~/lib/utils/datetime_utility'; - import collapsedCalendarIcon from './collapsed_calendar_icon.vue'; +import { __ } from '~/locale'; +import timeagoMixin from '~/vue_shared/mixins/timeago'; +import { dateInWords, timeFor } from '~/lib/utils/datetime_utility'; +import collapsedCalendarIcon from './collapsed_calendar_icon.vue'; - export default { - name: 'SidebarCollapsedGroupedDatePicker', - components: { - collapsedCalendarIcon, +export default { + name: 'SidebarCollapsedGroupedDatePicker', + components: { + collapsedCalendarIcon, + }, + mixins: [timeagoMixin], + props: { + collapsed: { + type: Boolean, + required: false, + default: true, }, - mixins: [ - timeagoMixin, - ], - props: { - collapsed: { - type: Boolean, - required: false, - default: true, - }, - minDate: { - type: Date, - required: false, - default: null, - }, - maxDate: { - type: Date, - required: false, - default: null, - }, - disableClickableIcons: { - type: Boolean, - required: false, - default: false, - }, + minDate: { + type: Date, + required: false, + default: null, }, - computed: { - hasMinAndMaxDates() { - return this.minDate && this.maxDate; - }, - hasNoMinAndMaxDates() { - return !this.minDate && !this.maxDate; - }, - showMinDateBlock() { - return this.minDate || this.hasNoMinAndMaxDates; - }, - showFromText() { - return !this.maxDate && this.minDate; - }, - iconClass() { - const disabledClass = this.disableClickableIcons ? 'disabled' : ''; - return `sidebar-collapsed-icon calendar-icon ${disabledClass}`; - }, + maxDate: { + type: Date, + required: false, + default: null, }, - methods: { - toggleSidebar() { - this.$emit('toggleCollapse'); - }, - dateText(dateType = 'min') { - const date = this[`${dateType}Date`]; - const dateWords = dateInWords(date, true); - const parsedDateWords = dateWords ? dateWords.replace(',', '') : dateWords; + disableClickableIcons: { + type: Boolean, + required: false, + default: false, + }, + }, + computed: { + hasMinAndMaxDates() { + return this.minDate && this.maxDate; + }, + hasNoMinAndMaxDates() { + return !this.minDate && !this.maxDate; + }, + showMinDateBlock() { + return this.minDate || this.hasNoMinAndMaxDates; + }, + showFromText() { + return !this.maxDate && this.minDate; + }, + iconClass() { + const disabledClass = this.disableClickableIcons ? 'disabled' : ''; + return `sidebar-collapsed-icon calendar-icon ${disabledClass}`; + }, + }, + methods: { + toggleSidebar() { + this.$emit('toggleCollapse'); + }, + dateText(dateType = 'min') { + const date = this[`${dateType}Date`]; + const dateWords = dateInWords(date, true); + const parsedDateWords = dateWords ? dateWords.replace(',', '') : dateWords; - return date ? parsedDateWords : __('None'); - }, - tooltipText(dateType = 'min') { - const defaultText = dateType === 'min' ? __('Start date') : __('Due date'); - const date = this[`${dateType}Date`]; - const timeAgo = dateType === 'min' ? this.timeFormated(date) : timeFor(date); - const dateText = date ? [ - this.dateText(dateType), - `(${timeAgo})`, - ].join(' ') : ''; + return date ? parsedDateWords : __('None'); + }, + tooltipText(dateType = 'min') { + const defaultText = dateType === 'min' ? __('Start date') : __('Due date'); + const date = this[`${dateType}Date`]; + const timeAgo = dateType === 'min' ? this.timeFormated(date) : timeFor(date); + const dateText = date ? [this.dateText(dateType), `(${timeAgo})`].join(' ') : ''; - if (date) { - return [defaultText, dateText].join('<br />'); - } - return __('Start and due date'); - }, + if (date) { + return [defaultText, dateText].join('<br />'); + } + return __('Start and due date'); }, - }; + }, +}; </script> <template> diff --git a/app/assets/javascripts/vue_shared/components/sidebar/labels_select/dropdown_value_collapsed.vue b/app/assets/javascripts/vue_shared/components/sidebar/labels_select/dropdown_value_collapsed.vue index af297f3c408..0d5fc07e6e3 100644 --- a/app/assets/javascripts/vue_shared/components/sidebar/labels_select/dropdown_value_collapsed.vue +++ b/app/assets/javascripts/vue_shared/components/sidebar/labels_select/dropdown_value_collapsed.vue @@ -14,7 +14,10 @@ export default { }, computed: { labelsList() { - const labelsString = this.labels.slice(0, 5).map(label => label.title).join(', '); + const labelsString = this.labels + .slice(0, 5) + .map(label => label.title) + .join(', '); if (this.labels.length > 5) { return sprintf(s__('LabelSelect|%{labelsString}, and %{remainingLabelCount} more'), { diff --git a/app/assets/javascripts/vue_shared/components/smart_virtual_list.vue b/app/assets/javascripts/vue_shared/components/smart_virtual_list.vue new file mode 100644 index 00000000000..63034a45f77 --- /dev/null +++ b/app/assets/javascripts/vue_shared/components/smart_virtual_list.vue @@ -0,0 +1,42 @@ +<script> +import VirtualList from 'vue-virtual-scroll-list'; + +export default { + name: 'SmartVirtualList', + components: { VirtualList }, + props: { + size: { type: Number, required: true }, + length: { type: Number, required: true }, + remain: { type: Number, required: true }, + rtag: { type: String, default: 'div' }, + wtag: { type: String, default: 'div' }, + wclass: { type: String, default: null }, + }, +}; +</script> +<template> + <virtual-list + v-if="length > remain" + v-bind="$attrs" + :size="remain" + :remain="remain" + :rtag="rtag" + :wtag="wtag" + :wclass="wclass" + class="js-virtual-list" + > + <slot></slot> + </virtual-list> + <component + :is="rtag" + v-else + class="js-plain-element" + > + <component + :is="wtag" + :class="wclass" + > + <slot></slot> + </component> + </component> +</template> diff --git a/app/assets/javascripts/vue_shared/mixins/ci_pagination_api_mixin.js b/app/assets/javascripts/vue_shared/mixins/ci_pagination_api_mixin.js index 67a1632269e..f9e3f3df0cc 100644 --- a/app/assets/javascripts/vue_shared/mixins/ci_pagination_api_mixin.js +++ b/app/assets/javascripts/vue_shared/mixins/ci_pagination_api_mixin.js @@ -14,7 +14,14 @@ export default { onChangePage(page) { /* URLS parameters are strings, we need to parse to match types */ - this.updateContent({ scope: this.scope, page: Number(page).toString() }); + const params = { + page: Number(page).toString(), + }; + + if (this.scope) { + params.scope = this.scope; + } + this.updateContent(params); }, updateInternalState(parameters) { diff --git a/app/assets/stylesheets/framework/blocks.scss b/app/assets/stylesheets/framework/blocks.scss index f26b1fddae5..43b7c26b272 100644 --- a/app/assets/stylesheets/framework/blocks.scss +++ b/app/assets/stylesheets/framework/blocks.scss @@ -348,6 +348,7 @@ @include media-breakpoint-down(xs) { width: 100%; + margin: $btn-side-margin 0; } } } diff --git a/app/assets/stylesheets/framework/contextual_sidebar.scss b/app/assets/stylesheets/framework/contextual_sidebar.scss index 2e7f25d975e..6f103e4e89a 100644 --- a/app/assets/stylesheets/framework/contextual_sidebar.scss +++ b/app/assets/stylesheets/framework/contextual_sidebar.scss @@ -322,15 +322,15 @@ width: $contextual-sidebar-width - 1px; transition: width $sidebar-transition-duration; position: fixed; + height: $toggle-sidebar-height; bottom: 0; - padding: $gl-padding; + padding: 0 $gl-padding; background-color: $gray-light; border: 0; border-top: 1px solid $border-color; color: $gl-text-color-secondary; display: flex; align-items: center; - line-height: 1; svg { margin-right: 8px; diff --git a/app/assets/stylesheets/framework/dropdowns.scss b/app/assets/stylesheets/framework/dropdowns.scss index cdfad30e7ca..dca89981d81 100644 --- a/app/assets/stylesheets/framework/dropdowns.scss +++ b/app/assets/stylesheets/framework/dropdowns.scss @@ -158,7 +158,7 @@ color: $gl-text-color; outline: 0; - // make sure the text color is not overriden + // make sure the text color is not overridden &.text-danger { color: $brand-danger; } @@ -184,7 +184,7 @@ text-align: left; width: 100%; - // make sure the text color is not overriden + // make sure the text color is not overridden &.text-danger { color: $brand-danger; } diff --git a/app/assets/stylesheets/framework/images.scss b/app/assets/stylesheets/framework/images.scss index 1e93bf2b751..a20920e2503 100644 --- a/app/assets/stylesheets/framework/images.scss +++ b/app/assets/stylesheets/framework/images.scss @@ -39,7 +39,7 @@ svg { fill: currentColor; - $svg-sizes: 8 10 12 16 18 24 32 48 72; + $svg-sizes: 8 10 12 14 16 18 24 32 48 72; @each $svg-size in $svg-sizes { &.s#{$svg-size} { @include svg-size(#{$svg-size}px); diff --git a/app/assets/stylesheets/framework/mixins.scss b/app/assets/stylesheets/framework/mixins.scss index c030d75f5a4..9837b1a6bd0 100644 --- a/app/assets/stylesheets/framework/mixins.scss +++ b/app/assets/stylesheets/framework/mixins.scss @@ -291,7 +291,7 @@ /* * Mixin that handles the position of the controls placed on the top bar */ -@mixin build-controllers($control-font-size, $flex-direction, $with-grow, $flex-grow-size) { +@mixin build-controllers($control-font-size, $flex-direction, $with-grow, $flex-grow-size, $svg-display: 'block', $svg-top: '2px') { display: flex; font-size: $control-font-size; justify-content: $flex-direction; @@ -304,8 +304,9 @@ svg { width: 15px; height: 15px; - display: block; + display: $svg-display; fill: $gl-text-color; + top: $svg-top; } .controllers-buttons { diff --git a/app/assets/stylesheets/framework/secondary_navigation_elements.scss b/app/assets/stylesheets/framework/secondary_navigation_elements.scss index f47dfe1b563..de9e7c37695 100644 --- a/app/assets/stylesheets/framework/secondary_navigation_elements.scss +++ b/app/assets/stylesheets/framework/secondary_navigation_elements.scss @@ -1,6 +1,6 @@ // For tabbed navigation links, scrolling tabs, etc. For all top/main navigation, // please check nav.scss -.nav-links { +.nav-links:not(.quick-links) { display: flex; padding: 0; margin: 0; @@ -106,7 +106,7 @@ display: inline-block; float: right; text-align: right; - padding: 11px 0; + padding: $gl-padding-8 0; margin-bottom: 0; > .btn, diff --git a/app/assets/stylesheets/framework/tables.scss b/app/assets/stylesheets/framework/tables.scss index 339388392df..6954e6599b1 100644 --- a/app/assets/stylesheets/framework/tables.scss +++ b/app/assets/stylesheets/framework/tables.scss @@ -147,3 +147,9 @@ table { } } } + +.top-area + .content-list { + th { + border-top: 0; + } +} diff --git a/app/assets/stylesheets/framework/variables.scss b/app/assets/stylesheets/framework/variables.scss index ad66a0365ed..bfcac3f1c3f 100644 --- a/app/assets/stylesheets/framework/variables.scss +++ b/app/assets/stylesheets/framework/variables.scss @@ -10,6 +10,7 @@ $sidebar-breakpoint: 1024px; $default-transition-duration: 0.15s; $contextual-sidebar-width: 220px; $contextual-sidebar-collapsed-width: 50px; +$toggle-sidebar-height: 48px; /* * Color schema @@ -268,6 +269,7 @@ $flash-height: 52px; $context-header-height: 60px; $breadcrumb-min-height: 48px; $project-title-row-height: 24px; +$gl-line-height: 16px; /* * Common component specific colors diff --git a/app/assets/stylesheets/pages/builds.scss b/app/assets/stylesheets/pages/builds.scss index 31b258e56dd..1449723de52 100644 --- a/app/assets/stylesheets/pages/builds.scss +++ b/app/assets/stylesheets/pages/builds.scss @@ -94,7 +94,7 @@ } .controllers { - @include build-controllers(15px, center, false, 0); + @include build-controllers(15px, center, false, 0, inline, 0); } } diff --git a/app/assets/stylesheets/pages/environments.scss b/app/assets/stylesheets/pages/environments.scss index 19a36061c45..347fcad771a 100644 --- a/app/assets/stylesheets/pages/environments.scss +++ b/app/assets/stylesheets/pages/environments.scss @@ -44,11 +44,6 @@ margin: 0; } - .icon-play { - height: 13px; - width: 12px; - } - .external-url, .dropdown-new { color: $gl-text-color-secondary; @@ -366,7 +361,7 @@ } .arrow-shadow { - content: ""; + content: ''; position: absolute; width: 7px; height: 7px; diff --git a/app/assets/stylesheets/pages/events.scss b/app/assets/stylesheets/pages/events.scss index a91d44805ee..618f23d81b1 100644 --- a/app/assets/stylesheets/pages/events.scss +++ b/app/assets/stylesheets/pages/events.scss @@ -4,41 +4,29 @@ */ .event-item { font-size: $gl-font-size; - padding: $gl-padding-top 0 $gl-padding-top 40px; + padding: $gl-padding 0 $gl-padding 56px; border-bottom: 1px solid $white-normal; - color: $gl-text-color; + color: $gl-text-color-secondary; position: relative; - - &.event-inline { - .system-note-image { - top: 20px; - } - - .user-avatar { - top: 14px; - } - - .event-title, - .event-item-timestamp { - line-height: 40px; - } - } - - a { - color: $gl-text-color; - } + line-height: $gl-line-height; .system-note-image { position: absolute; left: 0; - top: 14px; svg { - width: 20px; - height: 20px; fill: $gl-text-color-secondary; } + } + + .system-note-image-inline { + svg { + fill: $gl-text-color-secondary; + } + } + .system-note-image, + .system-note-image-inline { &.opened-icon, &.created-icon { svg { @@ -53,16 +41,35 @@ &.accepted-icon svg { fill: $blue-300; } + + &.commented-on-icon svg { + fill: $blue-600; + } + } + + .event-user-info { + margin-bottom: $gl-padding-8; + + .author_name { + a { + color: $gl-text-color; + font-weight: $gl-font-weight-bold; + } + } } .event-title { - @include str-truncated(calc(100% - 174px)); - font-weight: $gl-font-weight-bold; - color: $gl-text-color; + .event-type { + &::first-letter { + text-transform: capitalize; + } + } } .event-body { + margin-top: $gl-padding-8; margin-right: 174px; + color: $gl-text-color; .event-note { word-wrap: break-word; @@ -92,7 +99,7 @@ } .note-image-attach { - margin-top: 4px; + margin-top: $gl-padding-4; margin-left: 0; max-width: 200px; float: none; @@ -107,7 +114,6 @@ color: $gl-gray-500; float: left; font-size: $gl-font-size; - line-height: 16px; margin-right: 5px; } } @@ -127,7 +133,9 @@ } } - &:last-child { border: 0; } + &:last-child { + border: 0; + } .event_commits { li { @@ -154,7 +162,6 @@ .event-item-timestamp { float: right; - line-height: 22px; } } @@ -177,10 +184,8 @@ .event-item { padding-left: 0; - &.event-inline { - .event-title { - line-height: 20px; - } + .event-user-info { + margin-bottom: $gl-padding-4; } .event-title { @@ -194,7 +199,8 @@ } .event-body { - margin: 0; + margin-top: $gl-padding-4; + margin-right: 0; padding-left: 0; } diff --git a/app/assets/stylesheets/pages/pipeline_schedules.scss b/app/assets/stylesheets/pages/pipeline_schedules.scss index 86e70955389..617b3db2fae 100644 --- a/app/assets/stylesheets/pages/pipeline_schedules.scss +++ b/app/assets/stylesheets/pages/pipeline_schedules.scss @@ -39,10 +39,6 @@ white-space: nowrap; overflow: hidden; text-overflow: ellipsis; - - svg { - vertical-align: middle; - } } .next-run-cell { @@ -52,6 +48,10 @@ a { color: $text-color; } + + svg { + vertical-align: middle; + } } .pipeline-schedules-user-callout { diff --git a/app/assets/stylesheets/pages/profile.scss b/app/assets/stylesheets/pages/profile.scss index f084adaf5d3..1d691d1d8b8 100644 --- a/app/assets/stylesheets/pages/profile.scss +++ b/app/assets/stylesheets/pages/profile.scss @@ -240,6 +240,12 @@ left: 0; } + .activities-block { + .event-item { + padding-left: 40px; + } + } + @include media-breakpoint-down(xs) { .cover-block { padding-top: 20px; @@ -267,6 +273,12 @@ margin-right: 0; } } + + .activities-block { + .event-item { + padding-left: 0; + } + } } } diff --git a/app/controllers/admin/appearances_controller.rb b/app/controllers/admin/appearances_controller.rb index fdd3b4126ff..e3226c86b0b 100644 --- a/app/controllers/admin/appearances_controller.rb +++ b/app/controllers/admin/appearances_controller.rb @@ -33,21 +33,21 @@ class Admin::AppearancesController < Admin::ApplicationController @appearance.save - redirect_to admin_appearances_path, notice: 'Logo was succesfully removed.' + redirect_to admin_appearances_path, notice: 'Logo was successfully removed.' end def header_logos @appearance.remove_header_logo! @appearance.save - redirect_to admin_appearances_path, notice: 'Header logo was succesfully removed.' + redirect_to admin_appearances_path, notice: 'Header logo was successfully removed.' end def favicon @appearance.remove_favicon! @appearance.save - redirect_to admin_appearances_path, notice: 'Favicon was succesfully removed.' + redirect_to admin_appearances_path, notice: 'Favicon was successfully removed.' end private diff --git a/app/controllers/clusters/applications_controller.rb b/app/controllers/clusters/applications_controller.rb new file mode 100644 index 00000000000..250f42f3096 --- /dev/null +++ b/app/controllers/clusters/applications_controller.rb @@ -0,0 +1,28 @@ +# frozen_string_literal: true + +class Clusters::ApplicationsController < Clusters::BaseController + before_action :cluster + before_action :authorize_create_cluster!, only: [:create] + + def create + Clusters::Applications::CreateService + .new(@cluster, current_user, create_cluster_application_params) + .execute(request) + + head :no_content + rescue Clusters::Applications::CreateService::InvalidApplicationError + render_404 + rescue StandardError + head :bad_request + end + + private + + def cluster + @cluster ||= clusterable.clusters.find(params[:id]) || render_404 + end + + def create_cluster_application_params + params.permit(:application, :hostname) + end +end diff --git a/app/controllers/clusters/base_controller.rb b/app/controllers/clusters/base_controller.rb new file mode 100644 index 00000000000..ef42f7c4074 --- /dev/null +++ b/app/controllers/clusters/base_controller.rb @@ -0,0 +1,37 @@ +# frozen_string_literal: true + +class Clusters::BaseController < ApplicationController + include RoutableActions + + skip_before_action :authenticate_user! + before_action :authorize_read_cluster! + + helper_method :clusterable + + private + + def cluster + @cluster ||= clusterable.clusters.find(params[:id]) + .present(current_user: current_user) + end + + def authorize_update_cluster! + access_denied! unless can?(current_user, :update_cluster, cluster) + end + + def authorize_admin_cluster! + access_denied! unless can?(current_user, :admin_cluster, cluster) + end + + def authorize_read_cluster! + access_denied! unless can?(current_user, :read_cluster, clusterable) + end + + def authorize_create_cluster! + access_denied! unless can?(current_user, :create_cluster, clusterable) + end + + def clusterable + raise NotImplementedError + end +end diff --git a/app/controllers/clusters/clusters_controller.rb b/app/controllers/clusters/clusters_controller.rb new file mode 100644 index 00000000000..f6f2060ebb5 --- /dev/null +++ b/app/controllers/clusters/clusters_controller.rb @@ -0,0 +1,218 @@ +# frozen_string_literal: true + +class Clusters::ClustersController < Clusters::BaseController + include RoutableActions + + before_action :cluster, except: [:index, :new, :create_gcp, :create_user] + before_action :generate_gcp_authorize_url, only: [:new] + before_action :validate_gcp_token, only: [:new] + before_action :gcp_cluster, only: [:new] + before_action :user_cluster, only: [:new] + before_action :authorize_create_cluster!, only: [:new] + before_action :authorize_update_cluster!, only: [:update] + before_action :authorize_admin_cluster!, only: [:destroy] + before_action :update_applications_status, only: [:cluster_status] + + helper_method :token_in_session + + STATUS_POLLING_INTERVAL = 10_000 + + def index + clusters = ClustersFinder.new(clusterable, current_user, :all).execute + @clusters = clusters.page(params[:page]).per(20) + end + + def new + end + + # Overridding ActionController::Metal#status is NOT a good idea + def cluster_status + respond_to do |format| + format.json do + Gitlab::PollingInterval.set_header(response, interval: STATUS_POLLING_INTERVAL) + + render json: ClusterSerializer + .new(current_user: @current_user) + .represent_status(@cluster) + end + end + end + + def show + end + + def update + Clusters::UpdateService + .new(current_user, update_params) + .execute(cluster) + + if cluster.valid? + respond_to do |format| + format.json do + head :no_content + end + format.html do + flash[:notice] = _('Kubernetes cluster was successfully updated.') + redirect_to cluster.show_path + end + end + else + respond_to do |format| + format.json { head :bad_request } + format.html { render :show } + end + end + end + + def destroy + if cluster.destroy + flash[:notice] = _('Kubernetes cluster integration was successfully removed.') + redirect_to clusterable.index_path, status: :found + else + flash[:notice] = _('Kubernetes cluster integration was not removed.') + render :show + end + end + + def create_gcp + @gcp_cluster = ::Clusters::CreateService + .new(current_user, create_gcp_cluster_params) + .execute(access_token: token_in_session) + .present(current_user: current_user) + + if @gcp_cluster.persisted? + redirect_to @gcp_cluster.show_path + else + generate_gcp_authorize_url + validate_gcp_token + user_cluster + + render :new, locals: { active_tab: 'gcp' } + end + end + + def create_user + @user_cluster = ::Clusters::CreateService + .new(current_user, create_user_cluster_params) + .execute(access_token: token_in_session) + .present(current_user: current_user) + + if @user_cluster.persisted? + redirect_to @user_cluster.show_path + else + generate_gcp_authorize_url + validate_gcp_token + gcp_cluster + + render :new, locals: { active_tab: 'user' } + end + end + + private + + def update_params + if cluster.managed? + params.require(:cluster).permit( + :enabled, + :environment_scope, + platform_kubernetes_attributes: [ + :namespace + ] + ) + else + params.require(:cluster).permit( + :enabled, + :name, + :environment_scope, + platform_kubernetes_attributes: [ + :api_url, + :token, + :ca_cert, + :namespace + ] + ) + end + end + + def create_gcp_cluster_params + params.require(:cluster).permit( + :enabled, + :name, + :environment_scope, + provider_gcp_attributes: [ + :gcp_project_id, + :zone, + :num_nodes, + :machine_type, + :legacy_abac + ]).merge( + provider_type: :gcp, + platform_type: :kubernetes, + clusterable: clusterable.subject + ) + end + + def create_user_cluster_params + params.require(:cluster).permit( + :enabled, + :name, + :environment_scope, + platform_kubernetes_attributes: [ + :namespace, + :api_url, + :token, + :ca_cert, + :authorization_type + ]).merge( + provider_type: :user, + platform_type: :kubernetes, + clusterable: clusterable.subject + ) + end + + def generate_gcp_authorize_url + state = generate_session_key_redirect(clusterable.new_path.to_s) + + @authorize_url = GoogleApi::CloudPlatform::Client.new( + nil, callback_google_api_auth_url, + state: state).authorize_url + rescue GoogleApi::Auth::ConfigMissingError + # no-op + end + + def gcp_cluster + @gcp_cluster = ::Clusters::Cluster.new.tap do |cluster| + cluster.build_provider_gcp + end + end + + def user_cluster + @user_cluster = ::Clusters::Cluster.new.tap do |cluster| + cluster.build_platform_kubernetes + end + end + + def validate_gcp_token + @valid_gcp_token = GoogleApi::CloudPlatform::Client.new(token_in_session, nil) + .validate_token(expires_at_in_session) + end + + def token_in_session + session[GoogleApi::CloudPlatform::Client.session_key_for_token] + end + + def expires_at_in_session + @expires_at_in_session ||= + session[GoogleApi::CloudPlatform::Client.session_key_for_expires_at] + end + + def generate_session_key_redirect(uri) + GoogleApi::CloudPlatform::Client.new_session_key_for_redirect_uri do |key| + session[key] = uri + end + end + + def update_applications_status + @cluster.applications.each(&:schedule_status_update) + end +end diff --git a/app/controllers/concerns/members_presentation.rb b/app/controllers/concerns/members_presentation.rb index c6c3598a976..0a9d3d86245 100644 --- a/app/controllers/concerns/members_presentation.rb +++ b/app/controllers/concerns/members_presentation.rb @@ -12,12 +12,7 @@ module MembersPresentation ).fabricate! end - # rubocop: disable CodeReuse/ActiveRecord def preload_associations(members) - ActiveRecord::Associations::Preloader.new.preload(members, :user) - ActiveRecord::Associations::Preloader.new.preload(members, :source) - ActiveRecord::Associations::Preloader.new.preload(members.map(&:user), :status) - ActiveRecord::Associations::Preloader.new.preload(members.map(&:user), :u2f_registrations) + MembersPreloader.new(members).preload_all end - # rubocop: enable CodeReuse/ActiveRecord end diff --git a/app/controllers/concerns/project_unauthorized.rb b/app/controllers/concerns/project_unauthorized.rb new file mode 100644 index 00000000000..f59440dbc59 --- /dev/null +++ b/app/controllers/concerns/project_unauthorized.rb @@ -0,0 +1,10 @@ +# frozen_string_literal: true + +module ProjectUnauthorized + extend ActiveSupport::Concern + + # EE would override this + def project_unauthorized_proc + # no-op + end +end diff --git a/app/controllers/concerns/routable_actions.rb b/app/controllers/concerns/routable_actions.rb index 88939b002b2..5624eb3aa45 100644 --- a/app/controllers/concerns/routable_actions.rb +++ b/app/controllers/concerns/routable_actions.rb @@ -3,23 +3,25 @@ module RoutableActions extend ActiveSupport::Concern - def find_routable!(routable_klass, requested_full_path, extra_authorization_proc: nil) + def find_routable!(routable_klass, requested_full_path, extra_authorization_proc: nil, not_found_or_authorized_proc: nil) routable = routable_klass.find_by_full_path(requested_full_path, follow_redirects: request.get?) if routable_authorized?(routable, extra_authorization_proc) ensure_canonical_path(routable, requested_full_path) routable else - handle_not_found_or_authorized(routable) + if not_found_or_authorized_proc + not_found_or_authorized_proc.call(routable) + end + + route_not_found unless performed? + nil end end - # This is overridden in gitlab-ee. - def handle_not_found_or_authorized(_routable) - route_not_found - end - def routable_authorized?(routable, extra_authorization_proc) + return false unless routable + action = :"read_#{routable.class.to_s.underscore}" return false unless can?(current_user, action, routable) diff --git a/app/controllers/import/gitea_controller.rb b/app/controllers/import/gitea_controller.rb index 382c684a408..f067ef625aa 100644 --- a/app/controllers/import/gitea_controller.rb +++ b/app/controllers/import/gitea_controller.rb @@ -23,7 +23,7 @@ class Import::GiteaController < Import::GithubController :"#{provider}_host_url" end - # Overriden methods + # Overridden methods def provider :gitea end diff --git a/app/controllers/import/github_controller.rb b/app/controllers/import/github_controller.rb index e3eec5a020d..58565aaf8c9 100644 --- a/app/controllers/import/github_controller.rb +++ b/app/controllers/import/github_controller.rb @@ -103,7 +103,7 @@ class Import::GithubController < Import::BaseController { github_access_token: session[access_token_key] } end - # The following methods are overriden in subclasses + # The following methods are overridden in subclasses def provider :github end diff --git a/app/controllers/oauth/authorizations_controller.rb b/app/controllers/oauth/authorizations_controller.rb index 894a6a431e3..705389749d8 100644 --- a/app/controllers/oauth/authorizations_controller.rb +++ b/app/controllers/oauth/authorizations_controller.rb @@ -3,7 +3,7 @@ class Oauth::AuthorizationsController < Doorkeeper::AuthorizationsController layout 'profile' - # Overriden from Doorkeeper::AuthorizationsController to + # Overridden from Doorkeeper::AuthorizationsController to # include the call to session.delete def new if pre_auth.authorizable? diff --git a/app/controllers/projects/application_controller.rb b/app/controllers/projects/application_controller.rb index a2bdcaefa9b..e0677ce3fbc 100644 --- a/app/controllers/projects/application_controller.rb +++ b/app/controllers/projects/application_controller.rb @@ -3,6 +3,7 @@ class Projects::ApplicationController < ApplicationController include CookiesHelper include RoutableActions + include ProjectUnauthorized include ChecksCollaboration skip_before_action :authenticate_user! @@ -21,7 +22,7 @@ class Projects::ApplicationController < ApplicationController path = File.join(params[:namespace_id], params[:project_id] || params[:id]) auth_proc = ->(project) { !project.pending_delete? } - @project = find_routable!(Project, path, extra_authorization_proc: auth_proc) + @project = find_routable!(Project, path, extra_authorization_proc: auth_proc, not_found_or_authorized_proc: project_unauthorized_proc) end def build_canonical_path(project) diff --git a/app/controllers/projects/clusters/applications_controller.rb b/app/controllers/projects/clusters/applications_controller.rb index bcea96bce94..c7b6218d007 100644 --- a/app/controllers/projects/clusters/applications_controller.rb +++ b/app/controllers/projects/clusters/applications_controller.rb @@ -1,29 +1,17 @@ # frozen_string_literal: true -class Projects::Clusters::ApplicationsController < Projects::ApplicationController - before_action :cluster - before_action :authorize_read_cluster! - before_action :authorize_create_cluster!, only: [:create] +class Projects::Clusters::ApplicationsController < Clusters::ApplicationsController + include ProjectUnauthorized - def create - Clusters::Applications::CreateService - .new(@cluster, current_user, create_cluster_application_params) - .execute(request) - - head :no_content - rescue Clusters::Applications::CreateService::InvalidApplicationError - render_404 - rescue StandardError - head :bad_request - end + prepend_before_action :project private - def cluster - @cluster ||= project.clusters.find(params[:id]) || render_404 + def clusterable + @clusterable ||= ClusterablePresenter.fabricate(project, current_user: current_user) end - def create_cluster_application_params - params.permit(:application, :hostname) + def project + @project ||= find_routable!(Project, File.join(params[:namespace_id], params[:project_id]), not_found_or_authorized_proc: project_unauthorized_proc) end end diff --git a/app/controllers/projects/clusters_controller.rb b/app/controllers/projects/clusters_controller.rb index 62adc66fb09..feda6deeaa6 100644 --- a/app/controllers/projects/clusters_controller.rb +++ b/app/controllers/projects/clusters_controller.rb @@ -1,224 +1,24 @@ # frozen_string_literal: true -class Projects::ClustersController < Projects::ApplicationController - before_action :cluster, except: [:index, :new, :create_gcp, :create_user] - before_action :authorize_read_cluster! - before_action :generate_gcp_authorize_url, only: [:new] - before_action :validate_gcp_token, only: [:new] - before_action :gcp_cluster, only: [:new] - before_action :user_cluster, only: [:new] - before_action :authorize_create_cluster!, only: [:new] - before_action :authorize_update_cluster!, only: [:update] - before_action :authorize_admin_cluster!, only: [:destroy] - before_action :update_applications_status, only: [:status] - helper_method :token_in_session +class Projects::ClustersController < Clusters::ClustersController + include ProjectUnauthorized - STATUS_POLLING_INTERVAL = 10_000 + prepend_before_action :project + before_action :repository - def index - clusters = ClustersFinder.new(project, current_user, :all).execute - @clusters = clusters.page(params[:page]).per(20) - end - - def new - end - - def status - respond_to do |format| - format.json do - Gitlab::PollingInterval.set_header(response, interval: STATUS_POLLING_INTERVAL) - - render json: ClusterSerializer - .new(project: @project, current_user: @current_user) - .represent_status(@cluster) - end - end - end - - def show - end - - def update - Clusters::UpdateService - .new(current_user, update_params) - .execute(cluster) - - if cluster.valid? - respond_to do |format| - format.json do - head :no_content - end - format.html do - flash[:notice] = _('Kubernetes cluster was successfully updated.') - redirect_to project_cluster_path(project, cluster) - end - end - else - respond_to do |format| - format.json { head :bad_request } - format.html { render :show } - end - end - end - - def destroy - if cluster.destroy - flash[:notice] = _('Kubernetes cluster integration was successfully removed.') - redirect_to project_clusters_path(project), status: :found - else - flash[:notice] = _('Kubernetes cluster integration was not removed.') - render :show - end - end - - def create_gcp - @gcp_cluster = ::Clusters::CreateService - .new(current_user, create_gcp_cluster_params) - .execute(project: project, access_token: token_in_session) - - if @gcp_cluster.persisted? - redirect_to project_cluster_path(project, @gcp_cluster) - else - generate_gcp_authorize_url - validate_gcp_token - user_cluster - - render :new, locals: { active_tab: 'gcp' } - end - end - - def create_user - @user_cluster = ::Clusters::CreateService - .new(current_user, create_user_cluster_params) - .execute(project: project, access_token: token_in_session) - - if @user_cluster.persisted? - redirect_to project_cluster_path(project, @user_cluster) - else - generate_gcp_authorize_url - validate_gcp_token - gcp_cluster - - render :new, locals: { active_tab: 'user' } - end - end + layout 'project' private - def cluster - @cluster ||= project.clusters.find(params[:id]) - .present(current_user: current_user) - end - - def update_params - if cluster.managed? - params.require(:cluster).permit( - :enabled, - :environment_scope, - platform_kubernetes_attributes: [ - :namespace - ] - ) - else - params.require(:cluster).permit( - :enabled, - :name, - :environment_scope, - platform_kubernetes_attributes: [ - :api_url, - :token, - :ca_cert, - :namespace - ] - ) - end - end - - def create_gcp_cluster_params - params.require(:cluster).permit( - :enabled, - :name, - :environment_scope, - provider_gcp_attributes: [ - :gcp_project_id, - :zone, - :num_nodes, - :machine_type, - :legacy_abac - ]).merge( - provider_type: :gcp, - platform_type: :kubernetes - ) - end - - def create_user_cluster_params - params.require(:cluster).permit( - :enabled, - :name, - :environment_scope, - platform_kubernetes_attributes: [ - :namespace, - :api_url, - :token, - :ca_cert, - :authorization_type - ]).merge( - provider_type: :user, - platform_type: :kubernetes - ) - end - - def generate_gcp_authorize_url - state = generate_session_key_redirect(new_project_cluster_path(@project).to_s) - - @authorize_url = GoogleApi::CloudPlatform::Client.new( - nil, callback_google_api_auth_url, - state: state).authorize_url - rescue GoogleApi::Auth::ConfigMissingError - # no-op - end - - def gcp_cluster - @gcp_cluster = ::Clusters::Cluster.new.tap do |cluster| - cluster.build_provider_gcp - end - end - - def user_cluster - @user_cluster = ::Clusters::Cluster.new.tap do |cluster| - cluster.build_platform_kubernetes - end - end - - def validate_gcp_token - @valid_gcp_token = GoogleApi::CloudPlatform::Client.new(token_in_session, nil) - .validate_token(expires_at_in_session) - end - - def token_in_session - session[GoogleApi::CloudPlatform::Client.session_key_for_token] - end - - def expires_at_in_session - @expires_at_in_session ||= - session[GoogleApi::CloudPlatform::Client.session_key_for_expires_at] - end - - def generate_session_key_redirect(uri) - GoogleApi::CloudPlatform::Client.new_session_key_for_redirect_uri do |key| - session[key] = uri - end - end - - def authorize_update_cluster! - access_denied! unless can?(current_user, :update_cluster, cluster) + def clusterable + @clusterable ||= ClusterablePresenter.fabricate(project, current_user: current_user) end - def authorize_admin_cluster! - access_denied! unless can?(current_user, :admin_cluster, cluster) + def project + @project ||= find_routable!(Project, File.join(params[:namespace_id], params[:project_id]), not_found_or_authorized_proc: project_unauthorized_proc) end - def update_applications_status - @cluster.applications.each(&:schedule_status_update) + def repository + @repository ||= project.repository end end diff --git a/app/controllers/projects/commit_controller.rb b/app/controllers/projects/commit_controller.rb index 00b63f55710..32fc5140366 100644 --- a/app/controllers/projects/commit_controller.rb +++ b/app/controllers/projects/commit_controller.rb @@ -43,7 +43,7 @@ class Projects::CommitController < Projects::ApplicationController # rubocop: disable CodeReuse/ActiveRecord def pipelines @pipelines = @commit.pipelines.order(id: :desc) - @pipelines = @pipelines.where(ref: params[:ref]) if params[:ref] + @pipelines = @pipelines.where(ref: params[:ref]).page(params[:page]).per(30) if params[:ref] respond_to do |format| format.html @@ -53,6 +53,7 @@ class Projects::CommitController < Projects::ApplicationController render json: { pipelines: PipelineSerializer .new(project: @project, current_user: @current_user) + .with_pagination(request, response) .represent(@pipelines), count: { all: @pipelines.count diff --git a/app/controllers/projects/merge_requests_controller.rb b/app/controllers/projects/merge_requests_controller.rb index 27b83da4f54..4bdb857b2d9 100644 --- a/app/controllers/projects/merge_requests_controller.rb +++ b/app/controllers/projects/merge_requests_controller.rb @@ -84,13 +84,14 @@ class Projects::MergeRequestsController < Projects::MergeRequests::ApplicationCo end def pipelines - @pipelines = @merge_request.all_pipelines + @pipelines = @merge_request.all_pipelines.page(params[:page]).per(30) Gitlab::PollingInterval.set_header(response, interval: 10_000) render json: { pipelines: PipelineSerializer .new(project: @project, current_user: @current_user) + .with_pagination(request, response) .represent(@pipelines), count: { all: @pipelines.count diff --git a/app/controllers/projects_controller.rb b/app/controllers/projects_controller.rb index ee438e160f2..7f4a9f5151b 100644 --- a/app/controllers/projects_controller.rb +++ b/app/controllers/projects_controller.rb @@ -276,7 +276,7 @@ class ProjectsController < Projects::ApplicationController # rubocop: enable CodeReuse/ActiveRecord # Render project landing depending of which features are available - # So if page is not availble in the list it renders the next page + # So if page is not available in the list it renders the next page # # pages list order: repository readme, wiki home, issues list, customize workflow def render_landing_page diff --git a/app/finders/autocomplete/users_finder.rb b/app/finders/autocomplete/users_finder.rb index e2283f3266e..45955783be9 100644 --- a/app/finders/autocomplete/users_finder.rb +++ b/app/finders/autocomplete/users_finder.rb @@ -72,7 +72,6 @@ module Autocomplete author_id.present? && current_user end - # rubocop: disable CodeReuse/ActiveRecord def find_users if project project.authorized_users.union_with_user(author_id) @@ -84,6 +83,5 @@ module Autocomplete User.none end end - # rubocop: enable CodeReuse/ActiveRecord end end diff --git a/app/finders/clusters_finder.rb b/app/finders/clusters_finder.rb index b40d6c41b71..0cce493b73e 100644 --- a/app/finders/clusters_finder.rb +++ b/app/finders/clusters_finder.rb @@ -1,20 +1,20 @@ # frozen_string_literal: true class ClustersFinder - def initialize(project, user, scope) - @project = project + def initialize(clusterable, user, scope) + @clusterable = clusterable @user = user @scope = scope || :active end def execute - clusters = project.clusters + clusters = clusterable.clusters filter_by_scope(clusters) end private - attr_reader :project, :user, :scope + attr_reader :clusterable, :user, :scope def filter_by_scope(clusters) case scope.to_sym diff --git a/app/finders/concerns/finder_with_cross_project_access.rb b/app/finders/concerns/finder_with_cross_project_access.rb index e038636f0c4..220f62bcc7f 100644 --- a/app/finders/concerns/finder_with_cross_project_access.rb +++ b/app/finders/concerns/finder_with_cross_project_access.rb @@ -16,7 +16,6 @@ module FinderWithCrossProjectAccess end override :execute - # rubocop: disable CodeReuse/ActiveRecord def execute(*args) check = Gitlab::CrossProjectAccess.find_check(self) original = super @@ -30,7 +29,6 @@ module FinderWithCrossProjectAccess original end end - # rubocop: enable CodeReuse/ActiveRecord # We can skip the cross project check for finding indivitual records. # this would be handled by the `can?(:read_*, result)` call in `FinderMethods` diff --git a/app/finders/group_descendants_finder.rb b/app/finders/group_descendants_finder.rb index 9d57d2d3bc9..c96979619fd 100644 --- a/app/finders/group_descendants_finder.rb +++ b/app/finders/group_descendants_finder.rb @@ -131,7 +131,6 @@ class GroupDescendantsFinder .with_selects_for_list(archived: params[:archived]) end - # rubocop: disable CodeReuse/ActiveRecord def subgroups return Group.none unless Group.supports_nested_groups? @@ -145,7 +144,6 @@ class GroupDescendantsFinder groups.with_selects_for_list(archived: params[:archived]).order_by(sort) end - # rubocop: enable CodeReuse/ActiveRecord # rubocop: disable CodeReuse/Finder def direct_child_projects diff --git a/app/finders/groups_finder.rb b/app/finders/groups_finder.rb index a35a3ed6142..ea954f98220 100644 --- a/app/finders/groups_finder.rb +++ b/app/finders/groups_finder.rb @@ -40,7 +40,6 @@ class GroupsFinder < UnionFinder attr_reader :current_user, :params - # rubocop: disable CodeReuse/ActiveRecord def all_groups return [owned_groups] if params[:owned] return [groups_with_min_access_level] if min_access_level? @@ -52,7 +51,6 @@ class GroupsFinder < UnionFinder groups << Group.none if groups.empty? groups end - # rubocop: enable CodeReuse/ActiveRecord def groups_for_ancestors current_user.authorized_groups @@ -82,11 +80,9 @@ class GroupsFinder < UnionFinder end # rubocop: enable CodeReuse/ActiveRecord - # rubocop: disable CodeReuse/ActiveRecord def owned_groups current_user&.owned_groups || Group.none end - # rubocop: enable CodeReuse/ActiveRecord def include_public_groups? current_user.nil? || all_available? diff --git a/app/finders/issuable_finder.rb b/app/finders/issuable_finder.rb index eb3d2498830..93bef592c65 100644 --- a/app/finders/issuable_finder.rb +++ b/app/finders/issuable_finder.rb @@ -192,11 +192,6 @@ class IssuableFinder params[:milestone_title].present? end - def filter_by_no_milestone? - milestones? && params[:milestone_title] == Milestone::None.title - end - - # rubocop: disable CodeReuse/ActiveRecord def milestones return @milestones if defined?(@milestones) @@ -217,7 +212,6 @@ class IssuableFinder Milestone.none end end - # rubocop: enable CodeReuse/ActiveRecord def labels? params[:label_name].present? @@ -227,7 +221,6 @@ class IssuableFinder labels? && params[:label_name].include?(Label::None.title) end - # rubocop: disable CodeReuse/ActiveRecord def labels return @labels if defined?(@labels) @@ -238,7 +231,6 @@ class IssuableFinder Label.none end end - # rubocop: enable CodeReuse/ActiveRecord def assignee_id? params[:assignee_id].present? @@ -248,15 +240,6 @@ class IssuableFinder params[:assignee_username].present? end - def filter_by_no_assignee? - # Assignee_id takes precedence over assignee_username - [NONE, FILTER_NONE].include?(params[:assignee_id].to_s.downcase) || params[:assignee_username].to_s == NONE - end - - def filter_by_any_assignee? - params[:assignee_id].to_s.downcase == FILTER_ANY - end - # rubocop: disable CodeReuse/ActiveRecord def assignee return @assignee if defined?(@assignee) @@ -422,6 +405,15 @@ class IssuableFinder end # rubocop: enable CodeReuse/ActiveRecord + def filter_by_no_assignee? + # Assignee_id takes precedence over assignee_username + [NONE, FILTER_NONE].include?(params[:assignee_id].to_s.downcase) || params[:assignee_username].to_s == NONE + end + + def filter_by_any_assignee? + params[:assignee_id].to_s.downcase == FILTER_ANY + end + # rubocop: disable CodeReuse/ActiveRecord def by_author(items) if author @@ -436,18 +428,6 @@ class IssuableFinder end # rubocop: enable CodeReuse/ActiveRecord - def filter_by_upcoming_milestone? - params[:milestone_title] == Milestone::Upcoming.name - end - - def filter_by_any_milestone? - params[:milestone_title] == Milestone::Any.title - end - - def filter_by_started_milestone? - params[:milestone_title] == Milestone::Started.name - end - # rubocop: disable CodeReuse/ActiveRecord def by_milestone(items) if milestones? @@ -469,6 +449,24 @@ class IssuableFinder end # rubocop: enable CodeReuse/ActiveRecord + def filter_by_no_milestone? + # Accepts `No Milestone` for compatibility + params[:milestone_title].to_s.downcase == FILTER_NONE || params[:milestone_title] == Milestone::None.title + end + + def filter_by_any_milestone? + # Accepts `Any Milestone` for compatibility + params[:milestone_title].to_s.downcase == FILTER_ANY || params[:milestone_title] == Milestone::Any.title + end + + def filter_by_upcoming_milestone? + params[:milestone_title] == Milestone::Upcoming.name + end + + def filter_by_started_milestone? + params[:milestone_title] == Milestone::Started.name + end + def by_label(items) return items unless labels? @@ -484,12 +482,27 @@ class IssuableFinder def by_my_reaction_emoji(items) if params[:my_reaction_emoji].present? && current_user - items = items.awarded(current_user, params[:my_reaction_emoji]) + items = + if filter_by_no_reaction? + items.not_awarded(current_user) + elsif filter_by_any_reaction? + items.awarded(current_user) + else + items.awarded(current_user, params[:my_reaction_emoji]) + end end items end + def filter_by_no_reaction? + params[:my_reaction_emoji].to_s.downcase == FILTER_NONE + end + + def filter_by_any_reaction? + params[:my_reaction_emoji].to_s.downcase == FILTER_ANY + end + def label_names if labels? params[:label_name].is_a?(String) ? params[:label_name].split(',') : params[:label_name] diff --git a/app/finders/issues_finder.rb b/app/finders/issues_finder.rb index cee57a83df4..45e494725d7 100644 --- a/app/finders/issues_finder.rb +++ b/app/finders/issues_finder.rb @@ -135,7 +135,6 @@ class IssuesFinder < IssuableFinder current_user.blank? end - # rubocop: disable CodeReuse/ActiveRecord def by_assignee(items) if filter_by_no_assignee? items.unassigned @@ -149,5 +148,4 @@ class IssuesFinder < IssuableFinder items end end - # rubocop: enable CodeReuse/ActiveRecord end diff --git a/app/finders/labels_finder.rb b/app/finders/labels_finder.rb index d000af21be3..e523942ea4c 100644 --- a/app/finders/labels_finder.rb +++ b/app/finders/labels_finder.rb @@ -12,7 +12,6 @@ class LabelsFinder < UnionFinder @params = params end - # rubocop: disable CodeReuse/ActiveRecord def execute(skip_authorization: false) @skip_authorization = skip_authorization items = find_union(label_ids, Label) || Label.none @@ -21,7 +20,6 @@ class LabelsFinder < UnionFinder items = by_search(items) sort(items) end - # rubocop: enable CodeReuse/ActiveRecord private diff --git a/app/finders/milestones_finder.rb b/app/finders/milestones_finder.rb index 47231ea80c7..9c477978f60 100644 --- a/app/finders/milestones_finder.rb +++ b/app/finders/milestones_finder.rb @@ -20,7 +20,6 @@ class MilestonesFinder @params = params end - # rubocop: disable CodeReuse/ActiveRecord def execute return Milestone.none if project_ids.empty? && group_ids.empty? @@ -31,7 +30,6 @@ class MilestonesFinder order(items) end - # rubocop: enable CodeReuse/ActiveRecord private diff --git a/app/finders/personal_access_tokens_finder.rb b/app/finders/personal_access_tokens_finder.rb index 81fd3b7a547..bd95dcd323f 100644 --- a/app/finders/personal_access_tokens_finder.rb +++ b/app/finders/personal_access_tokens_finder.rb @@ -3,7 +3,7 @@ class PersonalAccessTokensFinder attr_accessor :params - delegate :build, :find, :find_by, :find_by_token, to: :execute + delegate :build, :find, :find_by_id, :find_by_token, to: :execute def initialize(params = {}) @params = params diff --git a/app/finders/pipelines_finder.rb b/app/finders/pipelines_finder.rb index 3d0d3219a94..35d0e1acce5 100644 --- a/app/finders/pipelines_finder.rb +++ b/app/finders/pipelines_finder.rb @@ -12,7 +12,6 @@ class PipelinesFinder @params = params end - # rubocop: disable CodeReuse/ActiveRecord def execute unless Ability.allowed?(current_user, :read_pipeline, project) return Ci::Pipeline.none @@ -28,7 +27,6 @@ class PipelinesFinder items = by_yaml_errors(items) sort_items(items) end - # rubocop: enable CodeReuse/ActiveRecord private diff --git a/app/finders/projects_finder.rb b/app/finders/projects_finder.rb index 6ececcd4152..93d3c991846 100644 --- a/app/finders/projects_finder.rb +++ b/app/finders/projects_finder.rb @@ -88,7 +88,6 @@ class ProjectsFinder < UnionFinder # rubocop: enable CodeReuse/ActiveRecord # Builds a collection for an anonymous user. - # rubocop: disable CodeReuse/ActiveRecord def collection_without_user if private_only? || owned_projects? || min_access_level? Project.none @@ -96,7 +95,6 @@ class ProjectsFinder < UnionFinder Project.public_to_user end end - # rubocop: enable CodeReuse/ActiveRecord def owned_projects? params[:owned].present? diff --git a/app/finders/snippets_finder.rb b/app/finders/snippets_finder.rb index 3528e4228b2..f90971bb9f6 100644 --- a/app/finders/snippets_finder.rb +++ b/app/finders/snippets_finder.rb @@ -43,7 +43,6 @@ class SnippetsFinder < UnionFinder end end - # rubocop: disable CodeReuse/ActiveRecord def authorized_snippets_from_project if can?(current_user, :read_project_snippet, project) if project.team.member?(current_user) @@ -55,7 +54,6 @@ class SnippetsFinder < UnionFinder Snippet.none end end - # rubocop: enable CodeReuse/ActiveRecord # rubocop: disable CodeReuse/ActiveRecord def authorized_snippets diff --git a/app/helpers/application_settings_helper.rb b/app/helpers/application_settings_helper.rb index 0c9f69b6714..9a1c2a4c9e1 100644 --- a/app/helpers/application_settings_helper.rb +++ b/app/helpers/application_settings_helper.rb @@ -115,6 +115,7 @@ module ApplicationSettingsHelper :akismet_api_key, :akismet_enabled, :allow_local_requests_from_hooks_and_services, + :archive_builds_in_human_readable, :authorized_keys_enabled, :auto_devops_enabled, :auto_devops_domain, diff --git a/app/helpers/clusters_helper.rb b/app/helpers/clusters_helper.rb index 19eb763e1de..916dcb1a308 100644 --- a/app/helpers/clusters_helper.rb +++ b/app/helpers/clusters_helper.rb @@ -1,7 +1,8 @@ # frozen_string_literal: true module ClustersHelper - def has_multiple_clusters?(project) + # EE overrides this + def has_multiple_clusters? false end @@ -10,7 +11,7 @@ module ClustersHelper return unless show_gcp_signup_offer? content_tag :section, class: 'no-animate expanded' do - render 'projects/clusters/gcp_signup_offer_banner' + render 'clusters/clusters/gcp_signup_offer_banner' end end end diff --git a/app/helpers/events_helper.rb b/app/helpers/events_helper.rb index c94946a04e7..2adfc04deb8 100644 --- a/app/helpers/events_helper.rb +++ b/app/helpers/events_helper.rb @@ -163,14 +163,10 @@ module EventsHelper def event_note_title_html(event) if event.note_target - text = raw("#{event.note_target_type} ") + - if event.commit_note? - content_tag(:span, event.note_target_reference, class: 'commit-sha') - else - event.note_target_reference - end - - link_to(text, event_note_target_url(event), title: event.target_title, class: 'has-tooltip') + capture do + concat content_tag(:span, event.note_target_type, class: "event-target-type append-right-4") + concat link_to(event.note_target_reference, event_note_target_url(event), title: event.target_title, class: 'has-tooltip event-target-link append-right-4') + end else content_tag(:strong, '(deleted)') end @@ -183,17 +179,9 @@ module EventsHelper "--broken encoding" end - def event_row_class(event) - if event.body? - "event-block" - else - "event-inline" - end - end - - def icon_for_event(note) + def icon_for_event(note, size: 24) icon_name = ICON_NAMES_BY_EVENT_TYPE[note] - sprite_icon(icon_name) if icon_name + sprite_icon(icon_name, size: size) if icon_name end def icon_for_profile_event(event) @@ -203,8 +191,24 @@ module EventsHelper end else content_tag :div, class: 'system-note-image user-avatar' do - author_avatar(event, size: 32) + author_avatar(event, size: 40) + end + end + end + + def inline_event_icon(event) + unless current_path?('users#show') + content_tag :span, class: "system-note-image-inline d-none d-sm-flex append-right-4 #{event.action_name.parameterize}-icon align-self-center" do + icon_for_event(event.action_name, size: 14) end end end + + def event_user_info(event) + content_tag(:div, class: "event-user-info") do + concat content_tag(:span, link_to_author(event), class: "author_name") + concat " ".html_safe + concat content_tag(:span, event.author.to_reference, class: "username") + end + end end diff --git a/app/helpers/icons_helper.rb b/app/helpers/icons_helper.rb index 037004327b9..b0f63de2fb8 100644 --- a/app/helpers/icons_helper.rb +++ b/app/helpers/icons_helper.rb @@ -109,6 +109,8 @@ module IconsHelper def file_type_icon_class(type, mode, name) if type == 'folder' icon_class = 'folder' + elsif type == 'archive' + icon_class = 'archive' elsif mode == '120000' icon_class = 'share' else @@ -153,6 +155,6 @@ module IconsHelper private def known_sprites - @known_sprites ||= JSON.parse(File.read(Rails.root.join('node_modules/@gitlab-org/gitlab-svgs/dist/icons.json')))['icons'] + @known_sprites ||= JSON.parse(File.read(Rails.root.join('node_modules/@gitlab/svgs/dist/icons.json')))['icons'] end end diff --git a/app/helpers/labels_helper.rb b/app/helpers/labels_helper.rb index 76ed8efe2c6..39f661b5f0c 100644 --- a/app/helpers/labels_helper.rb +++ b/app/helpers/labels_helper.rb @@ -143,7 +143,7 @@ module LabelsHelper def labels_filter_path(options = {}) project = @target_project || @project - format = options.delete(:format) || :html + format = options.delete(:format) if project project_labels_path(project, format, options) diff --git a/app/helpers/page_layout_helper.rb b/app/helpers/page_layout_helper.rb index b33c074d1af..5038dcf9746 100644 --- a/app/helpers/page_layout_helper.rb +++ b/app/helpers/page_layout_helper.rb @@ -10,7 +10,7 @@ module PageLayoutHelper @breadcrumb_title = @page_title.last end - # Segments are seperated by middot + # Segments are separated by middot @page_title.join(" · ") end diff --git a/app/helpers/tree_helper.rb b/app/helpers/tree_helper.rb index 6d2da5699fb..78a11616d4c 100644 --- a/app/helpers/tree_helper.rb +++ b/app/helpers/tree_helper.rb @@ -31,11 +31,21 @@ module TreeHelper # mode - File unix mode # name - File name def tree_icon(type, mode, name) - icon("#{file_type_icon_class(type, mode, name)} fw") + icon([file_type_icon_class(type, mode, name), 'fw']) end - def tree_hex_class(content) - "file_#{hexdigest(content.name)}" + # Using Rails `*_path` methods can be slow, especially when generating + # many paths, as with a repository tree that has thousands of items. + def fast_project_blob_path(project, blob_path) + Addressable::URI.escape( + File.join(relative_url_root, project.path_with_namespace, 'blob', blob_path) + ) + end + + def fast_project_tree_path(project, tree_path) + Addressable::URI.escape( + File.join(relative_url_root, project.path_with_namespace, 'tree', tree_path) + ) end # Simple shortcut to File.join @@ -142,4 +152,8 @@ module TreeHelper def selected_branch @branch_name || tree_edit_branch end + + def relative_url_root + Gitlab.config.gitlab.relative_url_root.presence || '/' + end end diff --git a/app/helpers/user_callouts_helper.rb b/app/helpers/user_callouts_helper.rb index bae01d476df..4aba48061ba 100644 --- a/app/helpers/user_callouts_helper.rb +++ b/app/helpers/user_callouts_helper.rb @@ -3,7 +3,6 @@ module UserCalloutsHelper GKE_CLUSTER_INTEGRATION = 'gke_cluster_integration'.freeze GCP_SIGNUP_OFFER = 'gcp_signup_offer'.freeze - CLUSTER_SECURITY_WARNING = 'cluster_security_warning'.freeze def show_gke_cluster_integration_callout?(project) can?(current_user, :create_cluster, project) && @@ -14,10 +13,6 @@ module UserCalloutsHelper !user_dismissed?(GCP_SIGNUP_OFFER) end - def show_cluster_security_warning? - !user_dismissed?(CLUSTER_SECURITY_WARNING) - end - private def user_dismissed?(feature_name) diff --git a/app/mailers/emails/issues.rb b/app/mailers/emails/issues.rb index 602e5afe26b..93b51fb1774 100644 --- a/app/mailers/emails/issues.rb +++ b/app/mailers/emails/issues.rb @@ -45,6 +45,20 @@ module Emails mail_answer_thread(@issue, issue_thread_options(updated_by_user_id, recipient_id, reason)) end + def removed_milestone_issue_email(recipient_id, issue_id, updated_by_user_id, reason = nil) + setup_issue_mail(issue_id, recipient_id) + + mail_answer_thread(@issue, issue_thread_options(updated_by_user_id, recipient_id, reason)) + end + + def changed_milestone_issue_email(recipient_id, issue_id, milestone, updated_by_user_id, reason = nil) + setup_issue_mail(issue_id, recipient_id) + + @milestone = milestone + @milestone_url = milestone_url(@milestone) + mail_answer_thread(@issue, issue_thread_options(updated_by_user_id, recipient_id, reason)) + end + def issue_status_changed_email(recipient_id, issue_id, status, updated_by_user_id, reason = nil) setup_issue_mail(issue_id, recipient_id) diff --git a/app/mailers/emails/merge_requests.rb b/app/mailers/emails/merge_requests.rb index be085496731..6524d0c2087 100644 --- a/app/mailers/emails/merge_requests.rb +++ b/app/mailers/emails/merge_requests.rb @@ -40,6 +40,20 @@ module Emails mail_answer_thread(@merge_request, merge_request_thread_options(updated_by_user_id, recipient_id, reason)) end + def removed_milestone_merge_request_email(recipient_id, merge_request_id, updated_by_user_id, reason = nil) + setup_merge_request_mail(merge_request_id, recipient_id) + + mail_answer_thread(@merge_request, merge_request_thread_options(updated_by_user_id, recipient_id, reason)) + end + + def changed_milestone_merge_request_email(recipient_id, merge_request_id, milestone, updated_by_user_id, reason = nil) + setup_merge_request_mail(merge_request_id, recipient_id) + + @milestone = milestone + @milestone_url = milestone_url(@milestone) + mail_answer_thread(@merge_request, merge_request_thread_options(updated_by_user_id, recipient_id, reason)) + end + def closed_merge_request_email(recipient_id, merge_request_id, updated_by_user_id, reason = nil) setup_merge_request_mail(merge_request_id, recipient_id) diff --git a/app/mailers/previews/notify_preview.rb b/app/mailers/previews/notify_preview.rb index 2f5b5483e9d..e7e8d96eca4 100644 --- a/app/mailers/previews/notify_preview.rb +++ b/app/mailers/previews/notify_preview.rb @@ -68,6 +68,14 @@ class NotifyPreview < ActionMailer::Preview Notify.issue_status_changed_email(user.id, issue.id, 'closed', user.id).message end + def removed_milestone_issue_email + Notify.removed_milestone_issue_email(user.id, issue.id, user.id) + end + + def changed_milestone_issue_email + Notify.changed_milestone_issue_email(user.id, issue.id, milestone, user.id) + end + def closed_merge_request_email Notify.closed_merge_request_email(user.id, issue.id, user.id).message end @@ -80,6 +88,14 @@ class NotifyPreview < ActionMailer::Preview Notify.merged_merge_request_email(user.id, merge_request.id, user.id).message end + def removed_milestone_merge_request_email + Notify.removed_milestone_merge_request_email(user.id, merge_request.id, user.id) + end + + def changed_milestone_merge_request_email + Notify.changed_milestone_merge_request_email(user.id, merge_request.id, milestone, user.id) + end + def member_access_denied_email Notify.member_access_denied_email('project', project.id, user.id).message end @@ -143,6 +159,10 @@ class NotifyPreview < ActionMailer::Preview @merge_request ||= project.merge_requests.first end + def milestone + @milestone ||= issue.milestone + end + def pipeline @pipeline = Ci::Pipeline.last end diff --git a/app/models/application_setting.rb b/app/models/application_setting.rb index b66ec0ffab6..704310f53f0 100644 --- a/app/models/application_setting.rb +++ b/app/models/application_setting.rb @@ -5,6 +5,7 @@ class ApplicationSetting < ActiveRecord::Base include CacheMarkdownField include TokenAuthenticatable include IgnorableColumn + include ChronicDurationAttribute add_authentication_token_field :runners_registration_token add_authentication_token_field :health_check_access_token @@ -45,6 +46,8 @@ class ApplicationSetting < ActiveRecord::Base default_value_for :id, 1 + chronic_duration_attr_writer :archive_builds_in_human_readable, :archive_builds_in_seconds + validates :uuid, presence: true validates :session_expire_delay, @@ -184,6 +187,10 @@ class ApplicationSetting < ActiveRecord::Base validates :user_default_internal_regex, js_regex: true, allow_nil: true + validates :archive_builds_in_seconds, + allow_nil: true, + numericality: { only_integer: true, greater_than_or_equal_to: 1.day.seconds } + SUPPORTED_KEY_TYPES.each do |type| validates :"#{type}_key_restriction", presence: true, key_restriction: { type: type } end @@ -441,6 +448,10 @@ class ApplicationSetting < ActiveRecord::Base latest_terms end + def archive_builds_older_than + archive_builds_in_seconds.seconds.ago if archive_builds_in_seconds + end + private def ensure_uuid! diff --git a/app/models/ci/build.rb b/app/models/ci/build.rb index d73c02ba5d7..360c9924a7d 100644 --- a/app/models/ci/build.rb +++ b/app/models/ci/build.rb @@ -9,19 +9,18 @@ module Ci include Presentable include Importable include Gitlab::Utils::StrongMemoize + include Deployable belongs_to :project, inverse_of: :builds belongs_to :runner belongs_to :trigger_request belongs_to :erased_by, class_name: 'User' - has_many :deployments, as: :deployable - RUNNER_FEATURES = { upload_multiple_artifacts: -> (build) { build.publishes_artifacts_reports? } }.freeze - has_one :last_deployment, -> { order('deployments.id DESC') }, as: :deployable, class_name: 'Deployment' + has_one :deployment, as: :deployable, class_name: 'Deployment' has_many :trace_sections, class_name: 'Ci::BuildTraceSection' has_many :trace_chunks, class_name: 'Ci::BuildTraceChunk', foreign_key: :build_id @@ -195,6 +194,8 @@ module Ci end after_transition pending: :running do |build| + build.deployment&.run + build.run_after_commit do BuildHooksWorker.perform_async(id) end @@ -207,14 +208,18 @@ module Ci end after_transition any => [:success] do |build| + build.deployment&.succeed + build.run_after_commit do - BuildSuccessWorker.perform_async(id) PagesWorker.perform_async(:deploy, id) if build.pages_generator? end end before_transition any => [:failed] do |build| next unless build.project + + build.deployment&.drop + next if build.retries_max.zero? if build.retries_count < build.retries_max @@ -233,6 +238,10 @@ module Ci after_transition running: any do |build| Ci::BuildRunnerSession.where(build: build).delete_all end + + after_transition any => [:skipped, :canceled] do |build| + build.deployment&.cancel + end end def ensure_metadata @@ -245,22 +254,41 @@ module Ci .fabricate! end - def other_actions + def other_manual_actions pipeline.manual_actions.where.not(name: name) end + def other_scheduled_actions + pipeline.scheduled_actions.where.not(name: name) + end + def pages_generator? Gitlab.config.pages.enabled && self.name == 'pages' end + # degenerated build is one that cannot be run by Runner + def degenerated? + self.options.nil? + end + + def degenerate! + self.update!(options: nil, yaml_variables: nil, commands: nil) + end + + def archived? + return true if degenerated? + + archive_builds_older_than = Gitlab::CurrentSettings.current_application_settings.archive_builds_older_than + archive_builds_older_than.present? && created_at < archive_builds_older_than + end + def playable? - action? && (manual? || scheduled? || retryable?) + action? && !archived? && (manual? || scheduled? || retryable?) end def schedulable? - Feature.enabled?('ci_enable_scheduled_build', default_enabled: true) && - self.when == 'delayed' && options[:start_in].present? + self.when == 'delayed' && options[:start_in].present? end def options_scheduled_at @@ -284,7 +312,7 @@ module Ci end def retryable? - success? || failed? || canceled? + !archived? && (success? || failed? || canceled?) end def retries_count @@ -292,7 +320,7 @@ module Ci end def retries_max - self.options.fetch(:retry, 0).to_i + self.options.to_h.fetch(:retry, 0).to_i end def latest? @@ -323,8 +351,12 @@ module Ci self.options.fetch(:environment, {}).fetch(:action, 'start') if self.options end + def has_deployment? + !!self.deployment + end + def outdated_deployment? - success? && !last_deployment.try(:last?) + success? && !deployment.try(:last?) end def depends_on_builds @@ -339,6 +371,10 @@ module Ci user == current_user end + def on_stop + options&.dig(:environment, :on_stop) + end + # A slugified version of the build ref, suitable for inclusion in URLs and # domain names. Rules: # @@ -706,7 +742,7 @@ module Ci if success? return successful_deployment_status - elsif complete? && !success? + elsif failed? return :failed end @@ -723,13 +759,11 @@ module Ci end def successful_deployment_status - if success? && last_deployment&.last? - return :last - elsif success? && last_deployment.present? - return :out_of_date + if deployment&.last? + :last + else + :out_of_date end - - :creating end def each_report(report_types) diff --git a/app/models/ci/job_artifact.rb b/app/models/ci/job_artifact.rb index 34a889057ab..11c88200c37 100644 --- a/app/models/ci/job_artifact.rb +++ b/app/models/ci/job_artifact.rb @@ -15,7 +15,7 @@ module Ci metadata: nil, trace: nil, junit: 'junit.xml', - codequality: 'codequality.json', + codequality: 'gl-code-quality-report.json', sast: 'gl-sast-report.json', dependency_scanning: 'gl-dependency-scanning-report.json', container_scanning: 'gl-container-scanning-report.json', diff --git a/app/models/ci/pipeline.rb b/app/models/ci/pipeline.rb index aeee7f0a5d2..56010e899a4 100644 --- a/app/models/ci/pipeline.rb +++ b/app/models/ci/pipeline.rb @@ -181,22 +181,31 @@ module Ci # # ref - The name (or names) of the branch(es)/tag(s) to limit the list of # pipelines to. - def self.newest_first(ref = nil) + # limit - This limits a backlog search, default to 100. + def self.newest_first(ref: nil, limit: 100) relation = order(id: :desc) + relation = relation.where(ref: ref) if ref + + if limit + ids = relation.limit(limit).select(:id) + # MySQL does not support limit in subquery + ids = ids.pluck(:id) if Gitlab::Database.mysql? + relation = relation.where(id: ids) + end - ref ? relation.where(ref: ref) : relation + relation end def self.latest_status(ref = nil) - newest_first(ref).pluck(:status).first + newest_first(ref: ref).pluck(:status).first end def self.latest_successful_for(ref) - newest_first(ref).success.take + newest_first(ref: ref).success.take end def self.latest_successful_for_refs(refs) - relation = newest_first(refs).success + relation = newest_first(ref: refs).success relation.each_with_object({}) do |pipeline, hash| hash[pipeline.ref] ||= pipeline @@ -238,6 +247,10 @@ module Ci end end + def self.latest_successful_ids_per_project + success.group(:project_id).select('max(id) as id') + end + def self.truncate_sha(sha) sha[0...8] end diff --git a/app/models/clusters/cluster.rb b/app/models/clusters/cluster.rb index 222e4217e67..e80d35d0f3c 100644 --- a/app/models/clusters/cluster.rb +++ b/app/models/clusters/cluster.rb @@ -3,6 +3,7 @@ module Clusters class Cluster < ActiveRecord::Base include Presentable + include Gitlab::Utils::StrongMemoize self.table_name = 'clusters' @@ -19,13 +20,11 @@ module Clusters has_many :cluster_projects, class_name: 'Clusters::Project' has_many :projects, through: :cluster_projects, class_name: '::Project' + has_one :cluster_project, -> { order(id: :desc) }, class_name: 'Clusters::Project' has_many :cluster_groups, class_name: 'Clusters::Group' has_many :groups, through: :cluster_groups, class_name: '::Group' - has_one :cluster_group, -> { order(id: :desc) }, class_name: 'Clusters::Group' - has_one :group, through: :cluster_group, class_name: '::Group' - # we force autosave to happen when we save `Cluster` model has_one :provider_gcp, class_name: 'Clusters::Providers::Gcp', autosave: true @@ -118,16 +117,30 @@ module Clusters end def first_project - return @first_project if defined?(@first_project) - - @first_project = projects.first + strong_memoize(:first_project) do + projects.first + end end alias_method :project, :first_project + def first_group + strong_memoize(:first_group) do + groups.first + end + end + alias_method :group, :first_group + def kubeclient platform_kubernetes.kubeclient if kubernetes? end + def find_or_initialize_kubernetes_namespace(cluster_project) + kubernetes_namespaces.find_or_initialize_by( + project: cluster_project.project, + cluster_project: cluster_project + ) + end + private def restrict_modification diff --git a/app/models/clusters/kubernetes_namespace.rb b/app/models/clusters/kubernetes_namespace.rb index fb5f6b65d9d..ac7f9193b87 100644 --- a/app/models/clusters/kubernetes_namespace.rb +++ b/app/models/clusters/kubernetes_namespace.rb @@ -2,6 +2,8 @@ module Clusters class KubernetesNamespace < ActiveRecord::Base + include Gitlab::Kubernetes + self.table_name = 'clusters_kubernetes_namespaces' belongs_to :cluster_project, class_name: 'Clusters::Project' @@ -12,7 +14,8 @@ module Clusters validates :namespace, presence: true validates :namespace, uniqueness: { scope: :cluster_id } - before_validation :set_namespace_and_service_account_to_default, on: :create + delegate :ca_pem, to: :platform_kubernetes, allow_nil: true + delegate :api_url, to: :platform_kubernetes, allow_nil: true attr_encrypted :service_account_token, mode: :per_attribute_iv, @@ -23,14 +26,26 @@ module Clusters "#{namespace}-token" end - private + def configure_predefined_credentials + self.namespace = kubernetes_or_project_namespace + self.service_account_name = default_service_account_name + end + + def predefined_variables + config = YAML.dump(kubeconfig) - def set_namespace_and_service_account_to_default - self.namespace ||= default_namespace - self.service_account_name ||= default_service_account_name + Gitlab::Ci::Variables::Collection.new.tap do |variables| + variables + .append(key: 'KUBE_SERVICE_ACCOUNT', value: service_account_name) + .append(key: 'KUBE_NAMESPACE', value: namespace) + .append(key: 'KUBE_TOKEN', value: service_account_token, public: false) + .append(key: 'KUBECONFIG', value: config, public: false, file: true) + end end - def default_namespace + private + + def kubernetes_or_project_namespace platform_kubernetes&.namespace.presence || project_namespace end @@ -45,5 +60,13 @@ module Clusters def project_slug "#{project.path}-#{project.id}".downcase end + + def kubeconfig + to_kubeconfig( + url: api_url, + namespace: namespace, + token: service_account_token, + ca_pem: ca_pem) + end end end diff --git a/app/models/clusters/platforms/kubernetes.rb b/app/models/clusters/platforms/kubernetes.rb index f0f791742f4..008e08d9914 100644 --- a/app/models/clusters/platforms/kubernetes.rb +++ b/app/models/clusters/platforms/kubernetes.rb @@ -6,6 +6,7 @@ module Clusters include Gitlab::Kubernetes include ReactiveCaching include EnumWithNil + include AfterCommitQueue RESERVED_NAMESPACES = %w(gitlab-managed-apps).freeze @@ -43,6 +44,7 @@ module Clusters validate :prevent_modification, on: :update after_save :clear_reactive_cache! + after_update :update_kubernetes_namespace alias_attribute :ca_pem, :ca_cert @@ -67,21 +69,31 @@ module Clusters end end - def predefined_variables - config = YAML.dump(kubeconfig) - + def predefined_variables(project:) Gitlab::Ci::Variables::Collection.new.tap do |variables| - variables - .append(key: 'KUBE_URL', value: api_url) - .append(key: 'KUBE_TOKEN', value: token, public: false) - .append(key: 'KUBE_NAMESPACE', value: actual_namespace) - .append(key: 'KUBECONFIG', value: config, public: false, file: true) + variables.append(key: 'KUBE_URL', value: api_url) if ca_pem.present? variables .append(key: 'KUBE_CA_PEM', value: ca_pem) .append(key: 'KUBE_CA_PEM_FILE', value: ca_pem, file: true) end + + if kubernetes_namespace = cluster.kubernetes_namespaces.find_by(project: project) + variables.concat(kubernetes_namespace.predefined_variables) + else + # From 11.5, every Clusters::Project should have at least one + # Clusters::KubernetesNamespace, so once migration has been completed, + # this 'else' branch will be removed. For more information, please see + # https://gitlab.com/gitlab-org/gitlab-ce/merge_requests/22433 + config = YAML.dump(kubeconfig) + + variables + .append(key: 'KUBE_URL', value: api_url) + .append(key: 'KUBE_TOKEN', value: token, public: false) + .append(key: 'KUBE_NAMESPACE', value: actual_namespace) + .append(key: 'KUBECONFIG', value: config, public: false, file: true) + end end end @@ -199,6 +211,14 @@ module Clusters true end + + def update_kubernetes_namespace + return unless namespace_changed? + + run_after_commit do + ClusterPlatformConfigureWorker.perform_async(cluster_id) + end + end end end end diff --git a/app/models/commit_status.rb b/app/models/commit_status.rb index 7d36f9982ab..755f8bd4d06 100644 --- a/app/models/commit_status.rb +++ b/app/models/commit_status.rb @@ -51,7 +51,8 @@ class CommitStatus < ActiveRecord::Base missing_dependency_failure: 5, runner_unsupported: 6, stale_schedule: 7, - job_execution_timeout: 8 + job_execution_timeout: 8, + archived_failure: 9 } ## @@ -167,16 +168,18 @@ class CommitStatus < ActiveRecord::Base false end - # To be overriden when inherrited from def retryable? false end - # To be overriden when inherrited from def cancelable? false end + def archived? + false + end + def stuck? false end diff --git a/app/models/concerns/awardable.rb b/app/models/concerns/awardable.rb index 6f29c92d176..60b7ec2815c 100644 --- a/app/models/concerns/awardable.rb +++ b/app/models/concerns/awardable.rb @@ -13,13 +13,13 @@ module Awardable end class_methods do - def awarded(user, name) + def awarded(user, name = nil) sql = <<~EOL EXISTS ( SELECT TRUE FROM award_emoji WHERE user_id = :user_id AND - name = :name AND + #{"name = :name AND" if name.present?} awardable_type = :awardable_type AND awardable_id = #{self.arel_table.name}.id ) @@ -28,6 +28,20 @@ module Awardable where(sql, user_id: user.id, name: name, awardable_type: self.name) end + def not_awarded(user) + sql = <<~EOL + NOT EXISTS ( + SELECT TRUE + FROM award_emoji + WHERE user_id = :user_id AND + awardable_type = :awardable_type AND + awardable_id = #{self.arel_table.name}.id + ) + EOL + + where(sql, user_id: user.id, awardable_type: self.name) + end + def order_upvotes_desc order_votes_desc(AwardEmoji::UPVOTE_NAME) end diff --git a/app/models/concerns/cacheable_attributes.rb b/app/models/concerns/cacheable_attributes.rb index f8034be8376..75592bb63e2 100644 --- a/app/models/concerns/cacheable_attributes.rb +++ b/app/models/concerns/cacheable_attributes.rb @@ -12,12 +12,12 @@ module CacheableAttributes "#{name}:#{Gitlab::VERSION}:#{Rails.version}".freeze end - # Can be overriden + # Can be overridden def current_without_cache last end - # Can be overriden + # Can be overridden def defaults {} end diff --git a/app/models/concerns/deployable.rb b/app/models/concerns/deployable.rb new file mode 100644 index 00000000000..f4f1989f0a9 --- /dev/null +++ b/app/models/concerns/deployable.rb @@ -0,0 +1,29 @@ +# frozen_string_literal: true + +module Deployable + extend ActiveSupport::Concern + + included do + after_create :create_deployment + + def create_deployment + return unless starts_environment? && !has_deployment? + + environment = project.environments.find_or_create_by( + name: expanded_environment_name + ) + + environment.deployments.create!( + project_id: environment.project_id, + environment: environment, + ref: ref, + tag: tag, + sha: sha, + user: user, + deployable: self, + on_stop: on_stop).tap do |_| + self.reload # Reload relationships + end + end + end +end diff --git a/app/models/concerns/fast_destroy_all.rb b/app/models/concerns/fast_destroy_all.rb index c342d01243e..2bfa7da6c1c 100644 --- a/app/models/concerns/fast_destroy_all.rb +++ b/app/models/concerns/fast_destroy_all.rb @@ -7,7 +7,7 @@ # `delete_all` is efficient as it deletes all rows with a single `DELETE` query. # # It's better to use `delete_all` as our best practice, however, -# if external data (e.g. ObjectStorage, FileStorage or Redis) are assosiated with database records, +# if external data (e.g. ObjectStorage, FileStorage or Redis) are associated with database records, # it is difficult to accomplish it. # # This module defines a format to use `delete_all` and delete associated external data. diff --git a/app/models/concerns/issuable.rb b/app/models/concerns/issuable.rb index a808f9ad4ad..69c5affe142 100644 --- a/app/models/concerns/issuable.rb +++ b/app/models/concerns/issuable.rb @@ -363,7 +363,7 @@ module Issuable end ## - # Overriden in MergeRequest + # Overridden in MergeRequest # def wipless_title_changed(old_title) old_title != title diff --git a/app/models/concerns/noteable.rb b/app/models/concerns/noteable.rb index 098eed137ba..eb315058c3a 100644 --- a/app/models/concerns/noteable.rb +++ b/app/models/concerns/noteable.rb @@ -23,7 +23,7 @@ module Noteable end def supports_discussions? - DiscussionNote::NOTEABLE_TYPES.include?(base_class_name) + DiscussionNote.noteable_types.include?(base_class_name) end def discussions_rendered_on_frontend? diff --git a/app/models/concerns/storage/legacy_namespace.rb b/app/models/concerns/storage/legacy_namespace.rb index 7723c07279d..af699eeebce 100644 --- a/app/models/concerns/storage/legacy_namespace.rb +++ b/app/models/concerns/storage/legacy_namespace.rb @@ -94,7 +94,7 @@ module Storage if gitlab_shell.mv_namespace(repository_storage, full_path, new_path) Gitlab::AppLogger.info %Q(Namespace directory "#{full_path}" moved to "#{new_path}") - # Remove namespace directroy async with delay so + # Remove namespace directory async with delay so # GitLab has time to remove all projects first run_after_commit do GitlabShellWorker.perform_in(5.minutes, :rm_namespace, repository_storage, new_path) diff --git a/app/models/concerns/token_authenticatable.rb b/app/models/concerns/token_authenticatable.rb index 66db4bd92de..23a43aec677 100644 --- a/app/models/concerns/token_authenticatable.rb +++ b/app/models/concerns/token_authenticatable.rb @@ -10,6 +10,7 @@ module TokenAuthenticatable def add_authentication_token_field(token_field, options = {}) @token_fields = [] unless @token_fields + unique = options.fetch(:unique, true) if @token_fields.include?(token_field) raise ArgumentError.new("#{token_field} already configured via add_authentication_token_field") @@ -25,8 +26,10 @@ module TokenAuthenticatable TokenAuthenticatableStrategies::Insecure.new(self, token_field, options) end - define_singleton_method("find_by_#{token_field}") do |token| - strategy.find_token_authenticatable(token) + if unique + define_singleton_method("find_by_#{token_field}") do |token| + strategy.find_token_authenticatable(token) + end end define_method(token_field) do diff --git a/app/models/concerns/token_authenticatable_strategies/base.rb b/app/models/concerns/token_authenticatable_strategies/base.rb index f0f7107d627..413721d3e6c 100644 --- a/app/models/concerns/token_authenticatable_strategies/base.rb +++ b/app/models/concerns/token_authenticatable_strategies/base.rb @@ -43,10 +43,14 @@ module TokenAuthenticatableStrategies set_token(instance, new_token) end + def unique + @options.fetch(:unique, true) + end + def generate_available_token loop do token = generate_token - break token unless find_token_authenticatable(token, true) + break token unless unique && find_token_authenticatable(token, true) end end diff --git a/app/models/concerns/with_uploads.rb b/app/models/concerns/with_uploads.rb index e231af5368d..2bdef2a40e4 100644 --- a/app/models/concerns/with_uploads.rb +++ b/app/models/concerns/with_uploads.rb @@ -2,7 +2,7 @@ # Mounted uploaders are destroyed by carrierwave's after_commit # hook. This hook fetches upload location (local vs remote) from -# Upload model. So it's neccessary to make sure that during that +# Upload model. So it's necessary to make sure that during that # after_commit hook model's associated uploads are not deleted yet. # IOW we can not use dependent: :destroy : # has_many :uploads, as: :model, dependent: :destroy diff --git a/app/models/deploy_token.rb b/app/models/deploy_token.rb index 0b2eedf3631..e3524305346 100644 --- a/app/models/deploy_token.rb +++ b/app/models/deploy_token.rb @@ -4,6 +4,7 @@ class DeployToken < ActiveRecord::Base include Expirable include TokenAuthenticatable include PolicyActor + include Gitlab::Utils::StrongMemoize add_authentication_token_field :token AVAILABLE_SCOPES = %i(read_repository read_registry).freeze @@ -49,7 +50,9 @@ class DeployToken < ActiveRecord::Base # to a single project, later we're going to extend # that to be for multiple projects and namespaces. def project - projects.first + strong_memoize(:project) do + projects.first + end end def expires_at diff --git a/app/models/deployment.rb b/app/models/deployment.rb index ee5b96e7454..54a900a3b85 100644 --- a/app/models/deployment.rb +++ b/app/models/deployment.rb @@ -3,6 +3,7 @@ class Deployment < ActiveRecord::Base include AtomicInternalId include IidRoutes + include AfterCommitQueue belongs_to :project, required: true belongs_to :environment, required: true @@ -16,11 +17,44 @@ class Deployment < ActiveRecord::Base delegate :name, to: :environment, prefix: true - after_create :create_ref - after_create :invalidate_cache - scope :for_environment, -> (environment) { where(environment_id: environment) } + state_machine :status, initial: :created do + event :run do + transition created: :running + end + + event :succeed do + transition any - [:success] => :success + end + + event :drop do + transition any - [:failed] => :failed + end + + event :cancel do + transition any - [:canceled] => :canceled + end + + before_transition any => [:success, :failed, :canceled] do |deployment| + deployment.finished_at = Time.now + end + + after_transition any => :success do |deployment| + deployment.run_after_commit do + Deployments::SuccessWorker.perform_async(id) + end + end + end + + enum status: { + created: 0, + running: 1, + success: 2, + failed: 3, + canceled: 4 + } + def self.last_for_environment(environment) ids = self .for_environment(environment) @@ -55,7 +89,11 @@ class Deployment < ActiveRecord::Base end def manual_actions - @manual_actions ||= deployable.try(:other_actions) + @manual_actions ||= deployable.try(:other_manual_actions) + end + + def scheduled_actions + @scheduled_actions ||= deployable.try(:other_scheduled_actions) end def includes_commit?(commit) @@ -65,15 +103,15 @@ class Deployment < ActiveRecord::Base end def update_merge_request_metrics! - return unless environment.update_merge_request_metrics? + return unless environment.update_merge_request_metrics? && success? merge_requests = project.merge_requests .joins(:metrics) .where(target_branch: self.ref, merge_request_metrics: { first_deployed_to_production_at: nil }) - .where("merge_request_metrics.merged_at <= ?", self.created_at) + .where("merge_request_metrics.merged_at <= ?", finished_at) if previous_deployment - merge_requests = merge_requests.where("merge_request_metrics.merged_at >= ?", previous_deployment.created_at) + merge_requests = merge_requests.where("merge_request_metrics.merged_at >= ?", previous_deployment.finished_at) end # Need to use `map` instead of `select` because MySQL doesn't allow `SELECT`ing from the same table @@ -87,7 +125,7 @@ class Deployment < ActiveRecord::Base MergeRequest::Metrics .where(merge_request_id: merge_request_ids, first_deployed_to_production_at: nil) - .update_all(first_deployed_to_production_at: self.created_at) + .update_all(first_deployed_to_production_at: finished_at) end def previous_deployment @@ -105,8 +143,18 @@ class Deployment < ActiveRecord::Base @stop_action ||= manual_actions.find_by(name: on_stop) end + def finished_at + read_attribute(:finished_at) || legacy_finished_at + end + + def deployed_at + return unless success? + + finished_at + end + def formatted_deployment_time - created_at.to_time.in_time_zone.to_s(:medium) + deployed_at&.to_time&.in_time_zone&.to_s(:medium) end def has_metrics? @@ -114,21 +162,17 @@ class Deployment < ActiveRecord::Base end def metrics - return {} unless has_metrics? + return {} unless has_metrics? && success? metrics = prometheus_adapter.query(:deployment, self) - metrics&.merge(deployment_time: created_at.to_i) || {} + metrics&.merge(deployment_time: finished_at.to_i) || {} end def additional_metrics - return {} unless has_metrics? + return {} unless has_metrics? && success? metrics = prometheus_adapter.query(:additional_metrics_deployment, self) - metrics&.merge(deployment_time: created_at.to_i) || {} - end - - def status - 'success' + metrics&.merge(deployment_time: finished_at.to_i) || {} end private @@ -140,4 +184,8 @@ class Deployment < ActiveRecord::Base def ref_path File.join(environment.ref_path, 'deployments', iid.to_s) end + + def legacy_finished_at + self.created_at if success? && !read_attribute(:finished_at) + end end diff --git a/app/models/diff_note.rb b/app/models/diff_note.rb index 95694377fe3..5f59e4832db 100644 --- a/app/models/diff_note.rb +++ b/app/models/diff_note.rb @@ -8,12 +8,14 @@ class DiffNote < Note include DiffPositionableNote include Gitlab::Utils::StrongMemoize - NOTEABLE_TYPES = %w(MergeRequest Commit).freeze + def self.noteable_types + %w(MergeRequest Commit) + end validates :original_position, presence: true validates :position, presence: true validates :line_code, presence: true, line_code: true, if: :on_text? - validates :noteable_type, inclusion: { in: NOTEABLE_TYPES } + validates :noteable_type, inclusion: { in: noteable_types } validate :positions_complete validate :verify_supported validate :diff_refs_match_commit, if: :for_commit? diff --git a/app/models/discussion_note.rb b/app/models/discussion_note.rb index 89d86aaed66..142cbdcdfa6 100644 --- a/app/models/discussion_note.rb +++ b/app/models/discussion_note.rb @@ -5,9 +5,11 @@ # A note of this type can be resolvable. class DiscussionNote < Note # Names of all implementers of `Noteable` that support discussions. - NOTEABLE_TYPES = %w(MergeRequest Issue Commit Snippet).freeze + def self.noteable_types + %w(MergeRequest Issue Commit Snippet) + end - validates :noteable_type, inclusion: { in: NOTEABLE_TYPES } + validates :noteable_type, inclusion: { in: noteable_types } def discussion_class(*) Discussion diff --git a/app/models/environment.rb b/app/models/environment.rb index 1c31c01eb9f..7d104bb0c25 100644 --- a/app/models/environment.rb +++ b/app/models/environment.rb @@ -8,9 +8,9 @@ class Environment < ActiveRecord::Base belongs_to :project, required: true - has_many :deployments, dependent: :destroy # rubocop:disable Cop/ActiveRecordDependent + has_many :deployments, -> { success }, dependent: :destroy # rubocop:disable Cop/ActiveRecordDependent - has_one :last_deployment, -> { order('deployments.id DESC') }, class_name: 'Deployment' + has_one :last_deployment, -> { success.order('deployments.id DESC') }, class_name: 'Deployment' before_validation :nullify_external_url before_validation :generate_slug, if: ->(env) { env.slug.blank? } diff --git a/app/models/environment_status.rb b/app/models/environment_status.rb index a84871f7253..7efc8da09ad 100644 --- a/app/models/environment_status.rb +++ b/app/models/environment_status.rb @@ -8,8 +8,8 @@ class EnvironmentStatus delegate :id, to: :environment delegate :name, to: :environment delegate :project, to: :environment + delegate :status, to: :deployment, allow_nil: true delegate :deployed_at, to: :deployment, allow_nil: true - delegate :status, to: :deployment def self.for_merge_request(mr, user) build_environments_status(mr, user, mr.head_pipeline) @@ -33,10 +33,6 @@ class EnvironmentStatus end end - def deployed_at - deployment&.created_at - end - def changes return [] if project.route_map_for(sha).nil? diff --git a/app/models/issue.rb b/app/models/issue.rb index 4ace5d3ab97..0de5e434b02 100644 --- a/app/models/issue.rb +++ b/app/models/issue.rb @@ -240,7 +240,8 @@ class Issue < ActiveRecord::Base reference_path: issue_reference, real_path: url_helper.project_issue_path(project, self), issue_sidebar_endpoint: url_helper.project_issue_path(project, self, format: :json, serializer: 'sidebar'), - toggle_subscription_endpoint: url_helper.toggle_subscription_project_issue_path(project, self) + toggle_subscription_endpoint: url_helper.toggle_subscription_project_issue_path(project, self), + assignable_labels_endpoint: url_helper.project_labels_path(project, format: :json, include_ancestor_groups: true) ) end diff --git a/app/models/key.rb b/app/models/key.rb index bdb83e12793..8f93418b88b 100644 --- a/app/models/key.rb +++ b/app/models/key.rb @@ -34,6 +34,10 @@ class Key < ActiveRecord::Base after_destroy :post_destroy_hook after_destroy :refresh_user_cache + def self.regular_keys + where(type: ['Key', nil]) + end + def key=(value) write_attribute(:key, value.present? ? Gitlab::SSHPublicKey.sanitize(value) : nil) diff --git a/app/models/label.rb b/app/models/label.rb index 43b49445765..165e4a8f3e5 100644 --- a/app/models/label.rb +++ b/app/models/label.rb @@ -41,8 +41,8 @@ class Label < ActiveRecord::Base scope :templates, -> { where(template: true) } scope :with_title, ->(title) { where(title: title) } scope :with_lists_and_board, -> { joins(lists: :board).merge(List.movable) } - scope :on_group_boards, ->(group_id) { with_lists_and_board.where(boards: { group_id: group_id }) } scope :on_project_boards, ->(project_id) { with_lists_and_board.where(boards: { project_id: project_id }) } + scope :on_board, ->(board_id) { with_lists_and_board.where(boards: { id: board_id }) } scope :order_name_asc, -> { reorder(title: :asc) } scope :order_name_desc, -> { reorder(title: :desc) } scope :subscribed_by, ->(user_id) { joins(:subscriptions).where(subscriptions: { user_id: user_id, subscribed: true }) } diff --git a/app/models/members_preloader.rb b/app/models/members_preloader.rb new file mode 100644 index 00000000000..33855191ca8 --- /dev/null +++ b/app/models/members_preloader.rb @@ -0,0 +1,16 @@ +# frozen_string_literal: true + +class MembersPreloader + attr_reader :members + + def initialize(members) + @members = members + end + + def preload_all + ActiveRecord::Associations::Preloader.new.preload(members, :user) + ActiveRecord::Associations::Preloader.new.preload(members, :source) + ActiveRecord::Associations::Preloader.new.preload(members.map(&:user), :status) + ActiveRecord::Associations::Preloader.new.preload(members.map(&:user), :u2f_registrations) + end +end diff --git a/app/models/merge_request.rb b/app/models/merge_request.rb index 7eef08aa6a3..735d9fba966 100644 --- a/app/models/merge_request.rb +++ b/app/models/merge_request.rb @@ -353,6 +353,15 @@ class MergeRequest < ActiveRecord::Base end end + # Returns true if there are commits that match at least one commit SHA. + def includes_any_commits?(shas) + if persisted? + merge_request_diff.commits_by_shas(shas).exists? + else + (commit_shas & shas).present? + end + end + # Calls `MergeWorker` to proceed with the merge process and # updates `merge_jid` with the MergeWorker#jid. # This helps tracking enqueued and ongoing merge jobs. diff --git a/app/models/merge_request_diff.rb b/app/models/merge_request_diff.rb index 02c6b650f33..bb6ff8921df 100644 --- a/app/models/merge_request_diff.rb +++ b/app/models/merge_request_diff.rb @@ -140,6 +140,12 @@ class MergeRequestDiff < ActiveRecord::Base merge_request_diff_commits.map(&:sha) end + def commits_by_shas(shas) + return [] unless shas.present? + + merge_request_diff_commits.where(sha: shas) + end + def diff_refs=(new_diff_refs) self.base_commit_sha = new_diff_refs&.base_sha self.start_commit_sha = new_diff_refs&.start_sha diff --git a/app/models/namespace.rb b/app/models/namespace.rb index 599bedde27d..4a6627d3ca1 100644 --- a/app/models/namespace.rb +++ b/app/models/namespace.rb @@ -83,7 +83,7 @@ class Namespace < ActiveRecord::Base find_by('lower(path) = :value', value: path.downcase) end - # Case insensetive search for namespace by path or name + # Case insensitive search for namespace by path or name def find_by_path_or_name(path) find_by("lower(path) = :path OR lower(name) = :path", path: path.downcase) end @@ -232,6 +232,12 @@ class Namespace < ActiveRecord::Base Project.inside_path(full_path) end + # Includes pipelines from this namespace and pipelines from all subgroups + # that belongs to this namespace + def all_pipelines + Ci::Pipeline.where(project: all_projects) + end + def has_parent? parent.present? end diff --git a/app/models/note.rb b/app/models/note.rb index 990689a95f5..592efb714f3 100644 --- a/app/models/note.rb +++ b/app/models/note.rb @@ -117,6 +117,8 @@ class Note < ActiveRecord::Base case notes_filter when UserPreference::NOTES_FILTERS[:only_comments] user + when UserPreference::NOTES_FILTERS[:only_activity] + system else all end diff --git a/app/models/pool_repository.rb b/app/models/pool_repository.rb new file mode 100644 index 00000000000..8ef74539209 --- /dev/null +++ b/app/models/pool_repository.rb @@ -0,0 +1,22 @@ +# frozen_string_literal: true + +class PoolRepository < ActiveRecord::Base + POOL_PREFIX = '@pools' + + belongs_to :shard + validates :shard, presence: true + + # For now, only pool repositories are tracked in the database. However, we may + # want to add other repository types in the future + self.table_name = 'repositories' + + has_many :pool_member_projects, class_name: 'Project', foreign_key: :pool_repository_id + + def shard_name + shard&.name + end + + def shard_name=(name) + self.shard = Shard.by_name(name) + end +end diff --git a/app/models/postgresql/replication_slot.rb b/app/models/postgresql/replication_slot.rb index 70c7432e6b5..e264fe88e47 100644 --- a/app/models/postgresql/replication_slot.rb +++ b/app/models/postgresql/replication_slot.rb @@ -4,6 +4,15 @@ module Postgresql class ReplicationSlot < ActiveRecord::Base self.table_name = 'pg_replication_slots' + # Returns true if there are any replication slots in use. + # PostgreSQL-compatible databases such as Aurora don't support + # replication slots, so this will return false as well. + def self.in_use? + transaction { exists? } + rescue ActiveRecord::StatementInvalid + false + end + # Returns true if the lag observed across all replication slots exceeds a # given threshold. # @@ -11,6 +20,8 @@ module Postgresql # statistics it takes between 1 and 5 seconds to replicate around # 100 MB of data. def self.lag_too_great?(max = 100.megabytes) + return false unless in_use? + lag_function = "#{Gitlab::Database.pg_wal_lsn_diff}" \ "(#{Gitlab::Database.pg_current_wal_insert_lsn}(), restart_lsn)::bigint" diff --git a/app/models/project.rb b/app/models/project.rb index d593cbb223a..d5a4ae79c47 100644 --- a/app/models/project.rb +++ b/app/models/project.rb @@ -95,8 +95,7 @@ class Project < ActiveRecord::Base unless: :ci_cd_settings, if: proc { ProjectCiCdSetting.available? } - after_create :set_last_activity_at - after_create :set_last_repository_updated_at + after_create :set_timestamps_for_create after_update :update_forks_visibility_level before_destroy :remove_private_deploy_keys @@ -124,6 +123,7 @@ class Project < ActiveRecord::Base alias_attribute :title, :name # Relations + belongs_to :pool_repository belongs_to :creator, class_name: 'User' belongs_to :group, -> { where(type: 'Group') }, foreign_key: 'namespace_id' belongs_to :namespace @@ -181,7 +181,7 @@ class Project < ActiveRecord::Base has_one :import_export_upload, dependent: :destroy # rubocop:disable Cop/ActiveRecordDependent # Merge Requests for target project should be removed with it - has_many :merge_requests, foreign_key: 'target_project_id' + has_many :merge_requests, foreign_key: 'target_project_id', inverse_of: :target_project has_many :source_of_merge_requests, foreign_key: 'source_project_id', class_name: 'MergeRequest' has_many :issues has_many :labels, class_name: 'ProjectLabel' @@ -254,7 +254,7 @@ class Project < ActiveRecord::Base has_many :variables, class_name: 'Ci::Variable' has_many :triggers, class_name: 'Ci::Trigger' has_many :environments - has_many :deployments + has_many :deployments, -> { success } has_many :pipeline_schedules, class_name: 'Ci::PipelineSchedule' has_many :project_deploy_tokens has_many :deploy_tokens, through: :project_deploy_tokens @@ -665,7 +665,7 @@ class Project < ActiveRecord::Base remove_import_data end - # This method is overriden in EE::Project model + # This method is overridden in EE::Project model def remove_import_data import_data&.destroy end @@ -1829,7 +1829,7 @@ class Project < ActiveRecord::Base end def deployment_variables(environment: nil) - deployment_platform(environment: environment)&.predefined_variables || [] + deployment_platform(environment: environment)&.predefined_variables(project: self) || [] end def auto_devops_variables @@ -2102,13 +2102,8 @@ class Project < ActiveRecord::Base gitlab_shell.exists?(repository_storage, "#{disk_path}.git") end - # set last_activity_at to the same as created_at - def set_last_activity_at - update_column(:last_activity_at, self.created_at) - end - - def set_last_repository_updated_at - update_column(:last_repository_updated_at, self.created_at) + def set_timestamps_for_create + update_columns(last_activity_at: self.created_at, last_repository_updated_at: self.created_at) end def cross_namespace_reference?(from) diff --git a/app/models/project_services/issue_tracker_service.rb b/app/models/project_services/issue_tracker_service.rb index 4f7e16a1f5b..a399982e5ec 100644 --- a/app/models/project_services/issue_tracker_service.rb +++ b/app/models/project_services/issue_tracker_service.rb @@ -9,7 +9,7 @@ class IssueTrackerService < Service # Override this method on services that uses different patterns # This pattern does not support cross-project references # The other code assumes that this pattern is a superset of all - # overriden patterns. See ReferenceRegexes::EXTERNAL_PATTERN + # overridden patterns. See ReferenceRegexes::EXTERNAL_PATTERN def self.reference_pattern(only_long: false) if only_long /(\b[A-Z][A-Z0-9_]*-)(?<issue>\d+)/ diff --git a/app/models/project_services/kubernetes_service.rb b/app/models/project_services/kubernetes_service.rb index 798944d0c06..3459ded7ccf 100644 --- a/app/models/project_services/kubernetes_service.rb +++ b/app/models/project_services/kubernetes_service.rb @@ -104,7 +104,12 @@ class KubernetesService < DeploymentService { success: false, result: err } end - def predefined_variables + # Project param was added on + # https://gitlab.com/gitlab-org/gitlab-ce/merge_requests/22011, + # as a way to keep this service compatible with + # Clusters::Platforms::Kubernetes, it won't be used on this method + # as it's only needed for Clusters::Cluster. + def predefined_variables(project:) config = YAML.dump(kubeconfig) Gitlab::Ci::Variables::Collection.new.tap do |variables| diff --git a/app/models/repository.rb b/app/models/repository.rb index 37a1dd64052..ee5579329a8 100644 --- a/app/models/repository.rb +++ b/app/models/repository.rb @@ -912,10 +912,6 @@ class Repository async_remove_remote(remote_name) if tmp_remote_name end - def fetch_remote(remote, forced: false, ssh_auth: nil, no_tags: false, prune: true) - gitlab_shell.fetch_remote(raw_repository, remote, ssh_auth: ssh_auth, forced: forced, no_tags: no_tags, prune: prune) - end - def async_remove_remote(remote_name) return unless remote_name diff --git a/app/models/shard.rb b/app/models/shard.rb new file mode 100644 index 00000000000..2fa22bd040c --- /dev/null +++ b/app/models/shard.rb @@ -0,0 +1,26 @@ +# frozen_string_literal: true + +class Shard < ActiveRecord::Base + # Store shard names from the configuration file in the database. This is not a + # list of active shards - we just want to assign an immutable, unique ID to + # every shard name for easy indexing / referencing. + def self.populate! + return unless table_exists? + + # The GitLab config does not change for the lifecycle of the process + in_config = Gitlab.config.repositories.storages.keys.map(&:to_s) + + transaction do + in_db = all.pluck(:name) + missing = in_config - in_db + + missing.map { |name| by_name(name) } + end + end + + def self.by_name(name) + find_or_create_by(name: name) + rescue ActiveRecord::RecordNotUnique + retry + end +end diff --git a/app/models/user.rb b/app/models/user.rb index cc2cd1b7723..039a3854edb 100644 --- a/app/models/user.rb +++ b/app/models/user.rb @@ -88,7 +88,7 @@ class User < ActiveRecord::Base has_one :namespace, -> { where(type: nil) }, dependent: :destroy, foreign_key: :owner_id, inverse_of: :owner, autosave: true # rubocop:disable Cop/ActiveRecordDependent # Profile - has_many :keys, -> { where(type: ['Key', nil]) }, dependent: :destroy # rubocop:disable Cop/ActiveRecordDependent + has_many :keys, -> { regular_keys }, dependent: :destroy # rubocop:disable Cop/ActiveRecordDependent has_many :deploy_keys, -> { where(type: 'DeployKey') }, dependent: :nullify # rubocop:disable Cop/ActiveRecordDependent has_many :gpg_keys @@ -460,12 +460,6 @@ class User < ActiveRecord::Base by_username(username).take! end - def find_by_personal_access_token(token_string) - return unless token_string - - PersonalAccessTokensFinder.new(state: 'active').find_by_token(token_string)&.user # rubocop: disable CodeReuse/Finder - end - # Returns a user for the given SSH key. def find_by_ssh_key_id(key_id) Key.find_by(id: key_id)&.user @@ -941,12 +935,17 @@ class User < ActiveRecord::Base if !Gitlab.config.ldap.enabled false elsif ldap_user? - !last_credential_check_at || (last_credential_check_at + 1.hour) < Time.now + !last_credential_check_at || (last_credential_check_at + ldap_sync_time) < Time.now else false end end + def ldap_sync_time + # This number resides in this method so it can be redefined in EE. + 1.hour + end + def try_obtain_ldap_lease # After obtaining this lease LDAP checks will be blocked for 600 seconds # (10 minutes) for this user. diff --git a/app/models/user_preference.rb b/app/models/user_preference.rb index 6cd91abc261..32d0407800f 100644 --- a/app/models/user_preference.rb +++ b/app/models/user_preference.rb @@ -4,7 +4,7 @@ class UserPreference < ActiveRecord::Base # We could use enums, but Rails 4 doesn't support multiple # enum options with same name for multiple fields, also it creates # extra methods that aren't really needed here. - NOTES_FILTERS = { all_notes: 0, only_comments: 1 }.freeze + NOTES_FILTERS = { all_notes: 0, only_comments: 1, only_activity: 2 }.freeze belongs_to :user @@ -14,7 +14,8 @@ class UserPreference < ActiveRecord::Base def notes_filters { s_('Notes|Show all activity') => NOTES_FILTERS[:all_notes], - s_('Notes|Show comments only') => NOTES_FILTERS[:only_comments] + s_('Notes|Show comments only') => NOTES_FILTERS[:only_comments], + s_('Notes|Show history only') => NOTES_FILTERS[:only_activity] } end end diff --git a/app/policies/ci/build_policy.rb b/app/policies/ci/build_policy.rb index 3858b29c82c..0ca3e696f46 100644 --- a/app/policies/ci/build_policy.rb +++ b/app/policies/ci/build_policy.rb @@ -20,12 +20,17 @@ module Ci @subject.project.branch_allows_collaboration?(@user, @subject.ref) end + condition(:archived, scope: :subject) do + @subject.archived? + end + condition(:terminal, scope: :subject) do @subject.has_terminal? end - rule { protected_ref }.policy do + rule { protected_ref | archived }.policy do prevent :update_build + prevent :update_commit_status prevent :erase_build end diff --git a/app/policies/deployment_policy.rb b/app/policies/deployment_policy.rb index 56ac898b6ab..d4f2f3c52b1 100644 --- a/app/policies/deployment_policy.rb +++ b/app/policies/deployment_policy.rb @@ -2,4 +2,13 @@ class DeploymentPolicy < BasePolicy delegate { @subject.project } + + condition(:can_retry_deployable) do + can?(:update_build, @subject.deployable) + end + + rule { ~can_retry_deployable }.policy do + prevent :create_deployment + prevent :update_deployment + end end diff --git a/app/presenters/clusterable_presenter.rb b/app/presenters/clusterable_presenter.rb new file mode 100644 index 00000000000..cff0e74d6ea --- /dev/null +++ b/app/presenters/clusterable_presenter.rb @@ -0,0 +1,46 @@ +# frozen_string_literal: true + +class ClusterablePresenter < Gitlab::View::Presenter::Delegated + presents :clusterable + + def self.fabricate(clusterable, **attributes) + presenter_class = "#{clusterable.class.name}ClusterablePresenter".constantize + attributes_with_presenter_class = attributes.merge(presenter_class: presenter_class) + + Gitlab::View::Presenter::Factory + .new(clusterable, attributes_with_presenter_class) + .fabricate! + end + + def can_create_cluster? + can?(current_user, :create_cluster, clusterable) + end + + def index_path + polymorphic_path([clusterable, :clusters]) + end + + def new_path + new_polymorphic_path([clusterable, :cluster]) + end + + def create_user_clusters_path + polymorphic_path([clusterable, :clusters], action: :create_user) + end + + def create_gcp_clusters_path + polymorphic_path([clusterable, :clusters], action: :create_gcp) + end + + def cluster_status_cluster_path(cluster, params = {}) + raise NotImplementedError + end + + def install_applications_cluster_path(cluster, application) + raise NotImplementedError + end + + def cluster_path(cluster, params = {}) + raise NotImplementedError + end +end diff --git a/app/presenters/clusters/cluster_presenter.rb b/app/presenters/clusters/cluster_presenter.rb index dfdd8e82f97..78d632eb77c 100644 --- a/app/presenters/clusters/cluster_presenter.rb +++ b/app/presenters/clusters/cluster_presenter.rb @@ -11,5 +11,13 @@ module Clusters def can_toggle_cluster? can?(current_user, :update_cluster, cluster) && created? end + + def show_path + if cluster.project_type? + project_cluster_path(project, cluster) + else + raise NotImplementedError + end + end end end diff --git a/app/presenters/commit_status_presenter.rb b/app/presenters/commit_status_presenter.rb index a866e76df5a..0cd77da6303 100644 --- a/app/presenters/commit_status_presenter.rb +++ b/app/presenters/commit_status_presenter.rb @@ -10,7 +10,8 @@ class CommitStatusPresenter < Gitlab::View::Presenter::Delegated missing_dependency_failure: 'There has been a missing dependency failure', runner_unsupported: 'Your runner is outdated, please upgrade your runner', stale_schedule: 'Delayed job could not be executed by some reason, please try again', - job_execution_timeout: 'The script exceeded the maximum execution time set for the job' + job_execution_timeout: 'The script exceeded the maximum execution time set for the job', + archived_failure: 'The job is archived and cannot be run' }.freeze private_constant :CALLOUT_FAILURE_MESSAGES @@ -30,6 +31,6 @@ class CommitStatusPresenter < Gitlab::View::Presenter::Delegated end def unrecoverable? - script_failure? || missing_dependency_failure? + script_failure? || missing_dependency_failure? || archived_failure? end end diff --git a/app/presenters/project_clusterable_presenter.rb b/app/presenters/project_clusterable_presenter.rb new file mode 100644 index 00000000000..12077b2e735 --- /dev/null +++ b/app/presenters/project_clusterable_presenter.rb @@ -0,0 +1,15 @@ +# frozen_string_literal: true + +class ProjectClusterablePresenter < ClusterablePresenter + def cluster_status_cluster_path(cluster, params = {}) + cluster_status_project_cluster_path(clusterable, cluster, params) + end + + def install_applications_cluster_path(cluster, application) + install_applications_project_cluster_path(clusterable, cluster, application) + end + + def cluster_path(cluster, params = {}) + project_cluster_path(clusterable, cluster, params) + end +end diff --git a/app/presenters/project_presenter.rb b/app/presenters/project_presenter.rb index 79cd3606aec..d61124fa787 100644 --- a/app/presenters/project_presenter.rb +++ b/app/presenters/project_presenter.rb @@ -176,7 +176,7 @@ class ProjectPresenter < Gitlab::View::Presenter::Delegated AnchorData.new(false, _('New file'), project_new_blob_path(project, default_branch || 'master'), - 'new') + 'success') end end diff --git a/app/serializers/build_action_entity.rb b/app/serializers/build_action_entity.rb index 0db7875aa87..95833c3528f 100644 --- a/app/serializers/build_action_entity.rb +++ b/app/serializers/build_action_entity.rb @@ -12,7 +12,8 @@ class BuildActionEntity < Grape::Entity end expose :playable?, as: :playable - expose :scheduled_at, if: -> (build) { build.scheduled? } + expose :scheduled?, as: :scheduled + expose :scheduled_at, if: -> (*) { scheduled? } expose :unschedule_path, if: -> (build) { build.scheduled? } do |build| unschedule_project_job_path(build.project, build) @@ -25,4 +26,8 @@ class BuildActionEntity < Grape::Entity def playable? build.playable? && can?(request.current_user, :update_build, build) end + + def scheduled? + build.scheduled? + end end diff --git a/app/serializers/deployment_entity.rb b/app/serializers/deployment_entity.rb index 344148a1fb7..aa1d9e6292c 100644 --- a/app/serializers/deployment_entity.rb +++ b/app/serializers/deployment_entity.rb @@ -25,4 +25,5 @@ class DeploymentEntity < Grape::Entity expose :commit, using: CommitEntity expose :deployable, using: JobEntity expose :manual_actions, using: JobEntity + expose :scheduled_actions, using: JobEntity end diff --git a/app/serializers/job_entity.rb b/app/serializers/job_entity.rb index 0b19cb16955..d0099ae77f2 100644 --- a/app/serializers/job_entity.rb +++ b/app/serializers/job_entity.rb @@ -7,9 +7,10 @@ class JobEntity < Grape::Entity expose :name expose :started?, as: :started + expose :archived?, as: :archived expose :build_path do |build| - build.target_url || path_to(:namespace_project_job, build) + build_path(build) end expose :retry_path, if: -> (*) { retryable? } do |build| @@ -17,7 +18,11 @@ class JobEntity < Grape::Entity end expose :cancel_path, if: -> (*) { cancelable? } do |build| - path_to(:cancel_namespace_project_job, build) + path_to( + :cancel_namespace_project_job, + build, + { continue: { to: build_path(build) } } + ) end expose :play_path, if: -> (*) { playable? } do |build| @@ -29,6 +34,7 @@ class JobEntity < Grape::Entity end expose :playable?, as: :playable + expose :scheduled?, as: :scheduled expose :scheduled_at, if: -> (*) { scheduled? } expose :created_at expose :updated_at @@ -60,8 +66,12 @@ class JobEntity < Grape::Entity build.detailed_status(request.current_user) end - def path_to(route, build) - send("#{route}_path", build.project.namespace, build.project, build) # rubocop:disable GitlabSecurity/PublicSend + def path_to(route, build, params = {}) + send("#{route}_path", build.project.namespace, build.project, build, params) # rubocop:disable GitlabSecurity/PublicSend + end + + def build_path(build) + build.target_url || path_to(:namespace_project_job, build) end def failed? diff --git a/app/serializers/user_preference_entity.rb b/app/serializers/user_preference_entity.rb index fbdaab459b3..b99f80424db 100644 --- a/app/serializers/user_preference_entity.rb +++ b/app/serializers/user_preference_entity.rb @@ -7,4 +7,8 @@ class UserPreferenceEntity < Grape::Entity expose :notes_filters do |user_preference| UserPreference.notes_filters end + + expose :default_notes_filter do |user_preference| + UserPreference::NOTES_FILTERS[:all_notes] + end end diff --git a/app/services/auth/container_registry_authentication_service.rb b/app/services/auth/container_registry_authentication_service.rb index 893b37b831a..f764536e762 100644 --- a/app/services/auth/container_registry_authentication_service.rb +++ b/app/services/auth/container_registry_authentication_service.rb @@ -99,7 +99,7 @@ module Auth ## # Because we do not have two way communication with registry yet, # we create a container repository image resource when push to the - # registry is successfuly authorized. + # registry is successfully authorized. # def ensure_container_repository!(path, actions) return if path.has_repository? diff --git a/app/services/boards/issues/move_service.rb b/app/services/boards/issues/move_service.rb index 7dd87034410..43a26f4264e 100644 --- a/app/services/boards/issues/move_service.rb +++ b/app/services/boards/issues/move_service.rb @@ -70,10 +70,8 @@ module Boards label_ids = if moving_to_list.movable? moving_from_list.label_id - elsif board.group_board? - ::Label.on_group_boards(parent.id).pluck(:label_id) else - ::Label.on_project_boards(parent.id).pluck(:label_id) + ::Label.on_board(board.id).pluck(:label_id) end Array(label_ids).compact diff --git a/app/services/ci/register_job_service.rb b/app/services/ci/register_job_service.rb index 5a7be921389..e06f1c05843 100644 --- a/app/services/ci/register_job_service.rb +++ b/app/services/ci/register_job_service.rb @@ -82,6 +82,11 @@ module Ci return false end + if build.archived? + build.drop!(:archived_failure) + return false + end + build.run! true end diff --git a/app/services/clusters/create_service.rb b/app/services/clusters/create_service.rb index cd843b8ffa8..270db4a52fd 100644 --- a/app/services/clusters/create_service.rb +++ b/app/services/clusters/create_service.rb @@ -8,10 +8,11 @@ module Clusters @current_user, @params = user, params.dup end - def execute(project:, access_token: nil) - raise ArgumentError, _('Instance does not support multiple Kubernetes clusters') unless can_create_cluster?(project) + def execute(access_token: nil) + raise ArgumentError, 'Unknown clusterable provided' unless clusterable + raise ArgumentError, _('Instance does not support multiple Kubernetes clusters') unless can_create_cluster? - cluster_params = params.merge(user: current_user, cluster_type: :project_type, projects: [project]) + cluster_params = params.merge(user: current_user).merge(clusterable_params) cluster_params[:provider_gcp_attributes].try do |provider| provider[:access_token] = access_token end @@ -27,9 +28,20 @@ module Clusters Clusters::Cluster.create(cluster_params) end + def clusterable + @clusterable ||= params.delete(:clusterable) + end + + def clusterable_params + case clusterable + when ::Project + { cluster_type: :project_type, projects: [clusterable] } + end + end + # EE would override this method - def can_create_cluster?(project) - project.clusters.empty? + def can_create_cluster? + clusterable.clusters.empty? end end end diff --git a/app/services/clusters/gcp/finalize_creation_service.rb b/app/services/clusters/gcp/finalize_creation_service.rb index 6ee63db8eb9..3df43657fa0 100644 --- a/app/services/clusters/gcp/finalize_creation_service.rb +++ b/app/services/clusters/gcp/finalize_creation_service.rb @@ -11,8 +11,9 @@ module Clusters configure_provider create_gitlab_service_account! configure_kubernetes - cluster.save! + configure_project_service_account + rescue Google::Apis::ServerError, Google::Apis::ClientError, Google::Apis::AuthorizationError => e provider.make_errored!("Failed to request to CloudPlatform; #{e.message}") rescue Kubeclient::HttpError => e @@ -24,7 +25,10 @@ module Clusters private def create_gitlab_service_account! - Clusters::Gcp::Kubernetes::CreateServiceAccountService.new(kube_client, rbac: create_rbac_cluster?).execute + Clusters::Gcp::Kubernetes::CreateServiceAccountService.gitlab_creator( + kube_client, + rbac: create_rbac_cluster? + ).execute end def configure_provider @@ -44,7 +48,20 @@ module Clusters end def request_kubernetes_token - Clusters::Gcp::Kubernetes::FetchKubernetesTokenService.new(kube_client).execute + Clusters::Gcp::Kubernetes::FetchKubernetesTokenService.new( + kube_client, + Clusters::Gcp::Kubernetes::GITLAB_ADMIN_TOKEN_NAME, + Clusters::Gcp::Kubernetes::GITLAB_SERVICE_ACCOUNT_NAMESPACE + ).execute + end + + def configure_project_service_account + kubernetes_namespace = cluster.find_or_initialize_kubernetes_namespace(cluster.cluster_project) + + Clusters::Gcp::Kubernetes::CreateOrUpdateNamespaceService.new( + cluster: cluster, + kubernetes_namespace: kubernetes_namespace + ).execute end def authorization_type diff --git a/app/services/clusters/gcp/kubernetes.rb b/app/services/clusters/gcp/kubernetes.rb index d014d73b3e8..90ed529670c 100644 --- a/app/services/clusters/gcp/kubernetes.rb +++ b/app/services/clusters/gcp/kubernetes.rb @@ -3,11 +3,12 @@ module Clusters module Gcp module Kubernetes - SERVICE_ACCOUNT_NAME = 'gitlab' - SERVICE_ACCOUNT_NAMESPACE = 'default' - SERVICE_ACCOUNT_TOKEN_NAME = 'gitlab-token' - CLUSTER_ROLE_BINDING_NAME = 'gitlab-admin' - CLUSTER_ROLE_NAME = 'cluster-admin' + GITLAB_SERVICE_ACCOUNT_NAME = 'gitlab' + GITLAB_SERVICE_ACCOUNT_NAMESPACE = 'default' + GITLAB_ADMIN_TOKEN_NAME = 'gitlab-token' + GITLAB_CLUSTER_ROLE_BINDING_NAME = 'gitlab-admin' + GITLAB_CLUSTER_ROLE_NAME = 'cluster-admin' + PROJECT_CLUSTER_ROLE_NAME = 'edit' end end end diff --git a/app/services/clusters/gcp/kubernetes/create_or_update_namespace_service.rb b/app/services/clusters/gcp/kubernetes/create_or_update_namespace_service.rb new file mode 100644 index 00000000000..a888fab2789 --- /dev/null +++ b/app/services/clusters/gcp/kubernetes/create_or_update_namespace_service.rb @@ -0,0 +1,54 @@ +# frozen_string_literal: true + +module Clusters + module Gcp + module Kubernetes + class CreateOrUpdateNamespaceService + def initialize(cluster:, kubernetes_namespace:) + @cluster = cluster + @kubernetes_namespace = kubernetes_namespace + @platform = cluster.platform + end + + def execute + configure_kubernetes_namespace + create_project_service_account + configure_kubernetes_token + + kubernetes_namespace.save! + rescue ::Kubeclient::HttpError => err + raise err unless err.error_code = 404 + end + + private + + attr_reader :cluster, :kubernetes_namespace, :platform + + def configure_kubernetes_namespace + kubernetes_namespace.configure_predefined_credentials + end + + def create_project_service_account + Clusters::Gcp::Kubernetes::CreateServiceAccountService.namespace_creator( + platform.kubeclient, + service_account_name: kubernetes_namespace.service_account_name, + service_account_namespace: kubernetes_namespace.namespace, + rbac: platform.rbac? + ).execute + end + + def configure_kubernetes_token + kubernetes_namespace.service_account_token = fetch_service_account_token + end + + def fetch_service_account_token + Clusters::Gcp::Kubernetes::FetchKubernetesTokenService.new( + platform.kubeclient, + kubernetes_namespace.token_name, + kubernetes_namespace.namespace + ).execute + end + end + end + end +end diff --git a/app/services/clusters/gcp/kubernetes/create_service_account_service.rb b/app/services/clusters/gcp/kubernetes/create_service_account_service.rb index d17744591e6..dfc4bf7a358 100644 --- a/app/services/clusters/gcp/kubernetes/create_service_account_service.rb +++ b/app/services/clusters/gcp/kubernetes/create_service_account_service.rb @@ -4,46 +4,96 @@ module Clusters module Gcp module Kubernetes class CreateServiceAccountService - attr_reader :kubeclient, :rbac - - def initialize(kubeclient, rbac:) + def initialize(kubeclient, service_account_name:, service_account_namespace:, token_name:, rbac:, namespace_creator: false, role_binding_name: nil) @kubeclient = kubeclient + @service_account_name = service_account_name + @service_account_namespace = service_account_namespace + @token_name = token_name @rbac = rbac + @namespace_creator = namespace_creator + @role_binding_name = role_binding_name + end + + def self.gitlab_creator(kubeclient, rbac:) + self.new( + kubeclient, + service_account_name: Clusters::Gcp::Kubernetes::GITLAB_SERVICE_ACCOUNT_NAME, + service_account_namespace: Clusters::Gcp::Kubernetes::GITLAB_SERVICE_ACCOUNT_NAMESPACE, + token_name: Clusters::Gcp::Kubernetes::GITLAB_ADMIN_TOKEN_NAME, + rbac: rbac + ) + end + + def self.namespace_creator(kubeclient, service_account_name:, service_account_namespace:, rbac:) + self.new( + kubeclient, + service_account_name: service_account_name, + service_account_namespace: service_account_namespace, + token_name: "#{service_account_namespace}-token", + rbac: rbac, + namespace_creator: true, + role_binding_name: "gitlab-#{service_account_namespace}" + ) end def execute + ensure_project_namespace_exists if namespace_creator kubeclient.create_service_account(service_account_resource) kubeclient.create_secret(service_account_token_resource) - kubeclient.create_cluster_role_binding(cluster_role_binding_resource) if rbac + create_role_or_cluster_role_binding if rbac end private + attr_reader :kubeclient, :service_account_name, :service_account_namespace, :token_name, :rbac, :namespace_creator, :role_binding_name + + def ensure_project_namespace_exists + Gitlab::Kubernetes::Namespace.new( + service_account_namespace, + kubeclient + ).ensure_exists! + end + + def create_role_or_cluster_role_binding + if namespace_creator + kubeclient.create_role_binding(role_binding_resource) + else + kubeclient.create_cluster_role_binding(cluster_role_binding_resource) + end + end + def service_account_resource - Gitlab::Kubernetes::ServiceAccount.new(service_account_name, service_account_namespace).generate + Gitlab::Kubernetes::ServiceAccount.new( + service_account_name, + service_account_namespace + ).generate end def service_account_token_resource Gitlab::Kubernetes::ServiceAccountToken.new( - SERVICE_ACCOUNT_TOKEN_NAME, service_account_name, service_account_namespace).generate + token_name, + service_account_name, + service_account_namespace + ).generate end def cluster_role_binding_resource subjects = [{ kind: 'ServiceAccount', name: service_account_name, namespace: service_account_namespace }] Gitlab::Kubernetes::ClusterRoleBinding.new( - CLUSTER_ROLE_BINDING_NAME, - CLUSTER_ROLE_NAME, + Clusters::Gcp::Kubernetes::GITLAB_CLUSTER_ROLE_BINDING_NAME, + Clusters::Gcp::Kubernetes::GITLAB_CLUSTER_ROLE_NAME, subjects ).generate end - def service_account_name - SERVICE_ACCOUNT_NAME - end - - def service_account_namespace - SERVICE_ACCOUNT_NAMESPACE + def role_binding_resource + Gitlab::Kubernetes::RoleBinding.new( + name: role_binding_name, + role_name: Clusters::Gcp::Kubernetes::PROJECT_CLUSTER_ROLE_NAME, + namespace: service_account_namespace, + service_account_name: service_account_name + ).generate end end end diff --git a/app/services/clusters/gcp/kubernetes/fetch_kubernetes_token_service.rb b/app/services/clusters/gcp/kubernetes/fetch_kubernetes_token_service.rb index 9e09345c8dc..277cc4b788d 100644 --- a/app/services/clusters/gcp/kubernetes/fetch_kubernetes_token_service.rb +++ b/app/services/clusters/gcp/kubernetes/fetch_kubernetes_token_service.rb @@ -4,10 +4,12 @@ module Clusters module Gcp module Kubernetes class FetchKubernetesTokenService - attr_reader :kubeclient + attr_reader :kubeclient, :service_account_token_name, :namespace - def initialize(kubeclient) + def initialize(kubeclient, service_account_token_name, namespace) @kubeclient = kubeclient + @service_account_token_name = service_account_token_name + @namespace = namespace end def execute @@ -18,7 +20,7 @@ module Clusters private def get_secret - kubeclient.get_secret(SERVICE_ACCOUNT_TOKEN_NAME, SERVICE_ACCOUNT_NAMESPACE).as_json + kubeclient.get_secret(service_account_token_name, namespace).as_json rescue Kubeclient::HttpError => err raise err unless err.error_code == 404 diff --git a/app/services/create_deployment_service.rb b/app/services/create_deployment_service.rb deleted file mode 100644 index bb3f605da28..00000000000 --- a/app/services/create_deployment_service.rb +++ /dev/null @@ -1,74 +0,0 @@ -# frozen_string_literal: true - -class CreateDeploymentService - attr_reader :job - - delegate :expanded_environment_name, - :variables, - :project, - to: :job - - def initialize(job) - @job = job - end - - def execute - return unless executable? - - ActiveRecord::Base.transaction do - environment.external_url = expanded_environment_url if - expanded_environment_url - - environment.fire_state_event(action) - - break unless environment.save - break if environment.stopped? - - deploy.tap(&:update_merge_request_metrics!) - end - end - - private - - def executable? - project && job.environment.present? && environment - end - - def deploy - project.deployments.create( - environment: environment, - ref: job.ref, - tag: job.tag, - sha: job.sha, - user: job.user, - deployable: job, - on_stop: on_stop) - end - - def environment - @environment ||= job.persisted_environment - end - - def environment_options - @environment_options ||= job.options&.dig(:environment) || {} - end - - def expanded_environment_url - return @expanded_environment_url if defined?(@expanded_environment_url) - - @expanded_environment_url = - ExpandVariables.expand(environment_url, variables) if environment_url - end - - def environment_url - environment_options[:url] - end - - def on_stop - environment_options[:on_stop] - end - - def action - environment_options[:action] || 'start' - end -end diff --git a/app/services/issuable_base_service.rb b/app/services/issuable_base_service.rb index 3e8b9f84042..c388913ae65 100644 --- a/app/services/issuable_base_service.rb +++ b/app/services/issuable_base_service.rb @@ -3,6 +3,14 @@ class IssuableBaseService < BaseService private + attr_accessor :params, :skip_milestone_email + + def initialize(project, user = nil, params = {}) + super + + @skip_milestone_email = @params.delete(:skip_milestone_email) + end + def filter_params(issuable) ability_name = :"admin_#{issuable.to_ability_name}" diff --git a/app/services/issues/update_service.rb b/app/services/issues/update_service.rb index b54b0bf6ef6..fba252b0bae 100644 --- a/app/services/issues/update_service.rb +++ b/app/services/issues/update_service.rb @@ -48,6 +48,8 @@ module Issues notification_service.async.relabeled_issue(issue, added_labels, current_user) end + handle_milestone_change(issue) + added_mentions = issue.mentioned_users - old_mentioned_users if added_mentions.present? @@ -91,6 +93,18 @@ module Issues private + def handle_milestone_change(issue) + return if skip_milestone_email + + return unless issue.previous_changes.include?('milestone_id') + + if issue.milestone.nil? + notification_service.async.removed_milestone_issue(issue, current_user) + else + notification_service.async.changed_milestone_issue(issue, issue.milestone, current_user) + end + end + # rubocop: disable CodeReuse/ActiveRecord def get_issue_if_allowed(id, board_group_id = nil) return unless id diff --git a/app/services/keys/destroy_service.rb b/app/services/keys/destroy_service.rb index e2ae4047941..159455f80f3 100644 --- a/app/services/keys/destroy_service.rb +++ b/app/services/keys/destroy_service.rb @@ -6,7 +6,7 @@ module Keys key.destroy if destroy_possible?(key) end - # overriden in EE::Keys::DestroyService + # overridden in EE::Keys::DestroyService def destroy_possible?(key) true end diff --git a/app/services/members/base_service.rb b/app/services/members/base_service.rb index 8248f1441d7..d734571f835 100644 --- a/app/services/members/base_service.rb +++ b/app/services/members/base_service.rb @@ -10,7 +10,7 @@ module Members end def after_execute(args) - # overriden in EE::Members modules + # overridden in EE::Members modules end private diff --git a/app/services/merge_requests/refresh_service.rb b/app/services/merge_requests/refresh_service.rb index f01872b205e..53768ff2cbe 100644 --- a/app/services/merge_requests/refresh_service.rb +++ b/app/services/merge_requests/refresh_service.rb @@ -87,11 +87,8 @@ module MergeRequests filter_merge_requests(merge_requests).each do |merge_request| if branch_and_project_match?(merge_request) || @push.force_push? merge_request.reload_diff(current_user) - else - mr_commit_ids = merge_request.commit_shas - push_commit_ids = @commits.map(&:id) - matches = mr_commit_ids & push_commit_ids - merge_request.reload_diff(current_user) if matches.any? + elsif merge_request.includes_any_commits?(push_commit_ids) + merge_request.reload_diff(current_user) end merge_request.mark_as_unchecked @@ -104,6 +101,10 @@ module MergeRequests end # rubocop: enable CodeReuse/ActiveRecord + def push_commit_ids + @push_commit_ids ||= @commits.map(&:id) + end + def branch_and_project_match?(merge_request) merge_request.source_project == @project && merge_request.source_branch == @push.branch_name diff --git a/app/services/merge_requests/reload_diffs_service.rb b/app/services/merge_requests/reload_diffs_service.rb index b4d48fe92ad..b47d8f3f63a 100644 --- a/app/services/merge_requests/reload_diffs_service.rb +++ b/app/services/merge_requests/reload_diffs_service.rb @@ -36,7 +36,10 @@ module MergeRequests # Remove cache for all diffs on this MR. Do not use the association on the # model, as that will interfere with other actions happening when # reloading the diff. - MergeRequestDiff.where(merge_request: merge_request).each do |merge_request_diff| + MergeRequestDiff + .where(merge_request: merge_request) + .preload(merge_request: :target_project) + .find_each do |merge_request_diff| next if merge_request_diff == new_diff cacheable_collection(merge_request_diff).clear_cache diff --git a/app/services/merge_requests/update_service.rb b/app/services/merge_requests/update_service.rb index b112edbce7f..aacaf10d09c 100644 --- a/app/services/merge_requests/update_service.rb +++ b/app/services/merge_requests/update_service.rb @@ -58,6 +58,8 @@ module MergeRequests merge_request.mark_as_unchecked end + handle_milestone_change(merge_request) + added_labels = merge_request.labels - old_labels if added_labels.present? notification_service.async.relabeled_merge_request( @@ -105,6 +107,18 @@ module MergeRequests private + def handle_milestone_change(merge_request) + return if skip_milestone_email + + return unless merge_request.previous_changes.include?('milestone_id') + + if merge_request.milestone.nil? + notification_service.async.removed_milestone_merge_request(merge_request, current_user) + else + notification_service.async.changed_milestone_merge_request(merge_request, merge_request.milestone, current_user) + end + end + def create_branch_change_note(issuable, branch_type, old_branch, new_branch) SystemNoteService.change_branch( issuable, issuable.project, current_user, branch_type, diff --git a/app/services/milestones/destroy_service.rb b/app/services/milestones/destroy_service.rb index 7cda802c120..87c7a282081 100644 --- a/app/services/milestones/destroy_service.rb +++ b/app/services/milestones/destroy_service.rb @@ -4,7 +4,7 @@ module Milestones class DestroyService < Milestones::BaseService def execute(milestone) Milestone.transaction do - update_params = { milestone: nil } + update_params = { milestone: nil, skip_milestone_email: true } milestone.issues.each do |issue| Issues::UpdateService.new(parent, current_user, update_params).execute(issue) diff --git a/app/services/notification_service.rb b/app/services/notification_service.rb index 50fa373025b..fb9c18ea75d 100644 --- a/app/services/notification_service.rb +++ b/app/services/notification_service.rb @@ -129,6 +129,14 @@ class NotificationService relabeled_resource_email(issue, added_labels, current_user, :relabeled_issue_email) end + def removed_milestone_issue(issue, current_user) + removed_milestone_resource_email(issue, current_user, :removed_milestone_issue_email) + end + + def changed_milestone_issue(issue, new_milestone, current_user) + changed_milestone_resource_email(issue, new_milestone, current_user, :changed_milestone_issue_email) + end + # When create a merge request we should send an email to: # # * mr author @@ -138,7 +146,6 @@ class NotificationService # * users with custom level checked with "new merge request" # # In EE, approvers of the merge request are also included - # def new_merge_request(merge_request, current_user) new_resource_email(merge_request, :new_merge_request_email) end @@ -208,6 +215,14 @@ class NotificationService relabeled_resource_email(merge_request, added_labels, current_user, :relabeled_merge_request_email) end + def removed_milestone_merge_request(merge_request, current_user) + removed_milestone_resource_email(merge_request, current_user, :removed_milestone_merge_request_email) + end + + def changed_milestone_merge_request(merge_request, new_milestone, current_user) + changed_milestone_resource_email(merge_request, new_milestone, current_user, :changed_milestone_merge_request_email) + end + def close_mr(merge_request, current_user) close_resource_email(merge_request, current_user, :closed_merge_request_email) end @@ -500,6 +515,30 @@ class NotificationService end end + def removed_milestone_resource_email(target, current_user, method) + recipients = NotificationRecipientService.build_recipients( + target, + current_user, + action: 'removed_milestone' + ) + + recipients.each do |recipient| + mailer.send(method, recipient.user.id, target.id, current_user.id).deliver_later + end + end + + def changed_milestone_resource_email(target, milestone, current_user, method) + recipients = NotificationRecipientService.build_recipients( + target, + current_user, + action: 'changed_milestone' + ) + + recipients.each do |recipient| + mailer.send(method, recipient.user.id, target.id, milestone, current_user.id).deliver_later + end + end + def reopen_resource_email(target, current_user, method, status) recipients = NotificationRecipientService.build_recipients(target, current_user, action: "reopen") diff --git a/app/services/projects/move_project_authorizations_service.rb b/app/services/projects/move_project_authorizations_service.rb index 2060a263751..2985ba89014 100644 --- a/app/services/projects/move_project_authorizations_service.rb +++ b/app/services/projects/move_project_authorizations_service.rb @@ -3,7 +3,7 @@ # NOTE: This service cannot be used directly because it is part of a # a bigger process. Instead, use the service MoveAccessService which moves # project memberships, project group links, authorizations and refreshes -# the authorizations if neccessary +# the authorizations if necessary module Projects class MoveProjectAuthorizationsService < BaseMoveRelationsService def execute(source_project, remove_remaining_elements: true) diff --git a/app/services/projects/move_project_group_links_service.rb b/app/services/projects/move_project_group_links_service.rb index fb395ecb9a1..36afcd0c503 100644 --- a/app/services/projects/move_project_group_links_service.rb +++ b/app/services/projects/move_project_group_links_service.rb @@ -3,7 +3,7 @@ # NOTE: This service cannot be used directly because it is part of a # a bigger process. Instead, use the service MoveAccessService which moves # project memberships, project group links, authorizations and refreshes -# the authorizations if neccessary +# the authorizations if necessary module Projects class MoveProjectGroupLinksService < BaseMoveRelationsService def execute(source_project, remove_remaining_elements: true) diff --git a/app/services/projects/move_project_members_service.rb b/app/services/projects/move_project_members_service.rb index f28f44adc03..faf389241d2 100644 --- a/app/services/projects/move_project_members_service.rb +++ b/app/services/projects/move_project_members_service.rb @@ -3,7 +3,7 @@ # NOTE: This service cannot be used directly because it is part of a # a bigger process. Instead, use the service MoveAccessService which moves # project memberships, project group links, authorizations and refreshes -# the authorizations if neccessary +# the authorizations if necessary module Projects class MoveProjectMembersService < BaseMoveRelationsService def execute(source_project, remove_remaining_elements: true) diff --git a/app/services/quick_actions/interpret_service.rb b/app/services/quick_actions/interpret_service.rb index 751aae2696d..eb431c36807 100644 --- a/app/services/quick_actions/interpret_service.rb +++ b/app/services/quick_actions/interpret_service.rb @@ -433,14 +433,14 @@ module QuickActions end end - desc 'Add or substract spent time' + desc 'Add or subtract spent time' explanation do |time_spent, time_spent_date| if time_spent if time_spent > 0 verb = 'Adds' value = time_spent else - verb = 'Substracts' + verb = 'Subtracts' value = -time_spent end diff --git a/app/services/search/group_service.rb b/app/services/search/group_service.rb index 00372887985..34803d005e3 100644 --- a/app/services/search/group_service.rb +++ b/app/services/search/group_service.rb @@ -11,13 +11,11 @@ module Search @group = group end - # rubocop: disable CodeReuse/ActiveRecord def projects return Project.none unless group return @projects if defined? @projects @projects = super.inside_path(group.full_path) end - # rubocop: enable CodeReuse/ActiveRecord end end diff --git a/app/services/update_deployment_service.rb b/app/services/update_deployment_service.rb new file mode 100644 index 00000000000..aa7fcca1e2a --- /dev/null +++ b/app/services/update_deployment_service.rb @@ -0,0 +1,53 @@ +# frozen_string_literal: true + +class UpdateDeploymentService + attr_reader :deployment + attr_reader :deployable + + delegate :environment, to: :deployment + delegate :variables, to: :deployable + + def initialize(deployment) + @deployment = deployment + @deployable = deployment.deployable + end + + def execute + deployment.create_ref + deployment.invalidate_cache + + ActiveRecord::Base.transaction do + environment.external_url = expanded_environment_url if + expanded_environment_url + + environment.fire_state_event(action) + + break unless environment.save + break if environment.stopped? + + deployment.tap(&:update_merge_request_metrics!) + end + end + + private + + def environment_options + @environment_options ||= deployable.options&.dig(:environment) || {} + end + + def expanded_environment_url + return @expanded_environment_url if defined?(@expanded_environment_url) + return unless environment_url + + @expanded_environment_url = + ExpandVariables.expand(environment_url, variables) + end + + def environment_url + environment_options[:url] + end + + def action + environment_options[:action] || 'start' + end +end diff --git a/app/views/admin/application_settings/_ci_cd.html.haml b/app/views/admin/application_settings/_ci_cd.html.haml index 97be658cd34..adb496495d1 100644 --- a/app/views/admin/application_settings/_ci_cd.html.haml +++ b/app/views/admin/application_settings/_ci_cd.html.haml @@ -41,5 +41,13 @@ The default unit is in seconds, but you can define an alternative. For example: <code>4 mins 2 sec</code>, <code>2h42min</code>. = link_to icon('question-circle'), help_page_path('user/admin_area/settings/continuous_integration', anchor: 'default-artifacts-expiration') + .form-group + = f.label :archive_builds_in_human_readable, 'Archive builds in', class: 'label-bold' + = f.text_field :archive_builds_in_human_readable, class: 'form-control', placeholder: 'never' + .form-text.text-muted + Set the duration when build gonna be considered old. Archived builds cannot be retried. + Make it empty to never expire builds. It has to be larger than 1 day. + The default unit is in seconds, but you can define an alternative. For example: + <code>4 mins 2 sec</code>, <code>2h42min</code>. = f.submit 'Save changes', class: "btn btn-success" diff --git a/app/views/projects/clusters/_advanced_settings.html.haml b/app/views/clusters/clusters/_advanced_settings.html.haml index 243e8cd9ba0..7037c80aa6b 100644 --- a/app/views/projects/clusters/_advanced_settings.html.haml +++ b/app/views/clusters/clusters/_advanced_settings.html.haml @@ -12,4 +12,4 @@ = s_('ClusterIntegration|Remove Kubernetes cluster integration') %p = s_("ClusterIntegration|Remove this Kubernetes cluster's configuration from this project. This will not delete your actual Kubernetes cluster.") - = link_to(s_('ClusterIntegration|Remove integration'), namespace_project_cluster_path(@project.namespace, @project, @cluster.id), method: :delete, class: 'btn btn-danger', data: { confirm: s_("ClusterIntegration|Are you sure you want to remove this Kubernetes cluster's integration? This will not delete your actual Kubernetes cluster.")}) + = link_to(s_('ClusterIntegration|Remove integration'), clusterable.cluster_path(@cluster), method: :delete, class: 'btn btn-danger', data: { confirm: s_("ClusterIntegration|Are you sure you want to remove this Kubernetes cluster's integration? This will not delete your actual Kubernetes cluster.")}) diff --git a/app/views/projects/clusters/_banner.html.haml b/app/views/clusters/clusters/_banner.html.haml index 73cfea0ef92..160c5f009a7 100644 --- a/app/views/projects/clusters/_banner.html.haml +++ b/app/views/clusters/clusters/_banner.html.haml @@ -7,9 +7,3 @@ .hidden.js-cluster-success.bs-callout.bs-callout-success{ role: 'alert' } = s_("ClusterIntegration|Kubernetes cluster was successfully created on Google Kubernetes Engine. Refresh the page to see Kubernetes cluster's details") - -- if show_cluster_security_warning? - .js-cluster-security-warning.alert.alert-block.alert-dismissable.bs-callout.bs-callout-warning - %button.close{ type: "button", data: { feature_id: UserCalloutsHelper::CLUSTER_SECURITY_WARNING, dismiss_endpoint: user_callouts_path } } × - = s_("ClusterIntegration|The default cluster configuration grants access to many functionalities needed to successfully build and deploy a containerised application.") - = link_to s_("More information"), help_page_path('user/project/clusters/index.md', anchor: 'security-implications') diff --git a/app/views/projects/clusters/_cluster.html.haml b/app/views/clusters/clusters/_cluster.html.haml index 2d7f7c6b1fb..facbcb7fc59 100644 --- a/app/views/projects/clusters/_cluster.html.haml +++ b/app/views/clusters/clusters/_cluster.html.haml @@ -2,7 +2,7 @@ .table-section.section-30 .table-mobile-header{ role: "rowheader" }= s_("ClusterIntegration|Kubernetes cluster") .table-mobile-content - = link_to cluster.name, namespace_project_cluster_path(@project.namespace, @project, cluster) + = link_to cluster.name, cluster.show_path .table-section.section-30 .table-mobile-header{ role: "rowheader" }= s_("ClusterIntegration|Environment scope") .table-mobile-content= cluster.environment_scope @@ -16,7 +16,7 @@ class: "#{'is-checked' if cluster.enabled?} #{'is-disabled' if !cluster.can_toggle_cluster?}", "aria-label": s_("ClusterIntegration|Toggle Kubernetes Cluster"), disabled: !cluster.can_toggle_cluster?, - data: { endpoint: namespace_project_cluster_path(@project.namespace, @project, cluster, format: :json) } } + data: { endpoint: clusterable.cluster_path(cluster, format: :json) } } %input.js-project-feature-toggle-input{ type: "hidden", value: cluster.enabled? } = icon("spinner spin", class: "loading-icon") %span.toggle-icon diff --git a/app/views/projects/clusters/_empty_state.html.haml b/app/views/clusters/clusters/_empty_state.html.haml index b8a3556a206..800e76d92ef 100644 --- a/app/views/projects/clusters/_empty_state.html.haml +++ b/app/views/clusters/clusters/_empty_state.html.haml @@ -7,6 +7,6 @@ - link_to_help_page = link_to(_('Learn more about Kubernetes'), help_page_path('user/project/clusters/index'), target: '_blank', rel: 'noopener noreferrer') %p= s_('ClusterIntegration|Kubernetes clusters allow you to use review apps, deploy your applications, run your pipelines, and much more in an easy way. %{link_to_help_page}').html_safe % { link_to_help_page: link_to_help_page} - - if can?(current_user, :create_cluster, @project) + - if clusterable.can_create_cluster? .text-center - = link_to s_('ClusterIntegration|Add Kubernetes cluster'), new_project_cluster_path(@project), class: 'btn btn-success' + = link_to s_('ClusterIntegration|Add Kubernetes cluster'), clusterable.new_path, class: 'btn btn-success' diff --git a/app/views/projects/clusters/_gcp_signup_offer_banner.html.haml b/app/views/clusters/clusters/_gcp_signup_offer_banner.html.haml index 73b11d509d3..73b11d509d3 100644 --- a/app/views/projects/clusters/_gcp_signup_offer_banner.html.haml +++ b/app/views/clusters/clusters/_gcp_signup_offer_banner.html.haml diff --git a/app/views/projects/clusters/_integration_form.html.haml b/app/views/clusters/clusters/_integration_form.html.haml index d0a553e3414..5e451f60c9d 100644 --- a/app/views/projects/clusters/_integration_form.html.haml +++ b/app/views/clusters/clusters/_integration_form.html.haml @@ -1,4 +1,4 @@ -= form_for @cluster, url: namespace_project_cluster_path(@project.namespace, @project, @cluster), as: :cluster do |field| += form_for @cluster, url: clusterable.cluster_path(@cluster), as: :cluster do |field| = form_errors(@cluster) .form-group %h5= s_('ClusterIntegration|Integration status') @@ -13,7 +13,7 @@ = sprite_icon('status_failed_borderless', size: 16, css_class: 'toggle-icon-svg toggle-status-unchecked') .form-text.text-muted= s_('ClusterIntegration|Enable or disable GitLab\'s connection to your Kubernetes cluster.') - - if has_multiple_clusters?(@project) + - if has_multiple_clusters? .form-group %h5= s_('ClusterIntegration|Environment scope') = field.text_field :environment_scope, class: 'col-md-6 form-control js-select-on-focus', placeholder: s_('ClusterIntegration|Environment scope') @@ -23,7 +23,7 @@ .form-group = field.submit _('Save changes'), class: 'btn btn-success' - - unless has_multiple_clusters?(@project) + - unless has_multiple_clusters? %h5= s_('ClusterIntegration|Environment scope') %p %code * diff --git a/app/views/projects/clusters/_sidebar.html.haml b/app/views/clusters/clusters/_sidebar.html.haml index 3d10348212f..3d10348212f 100644 --- a/app/views/projects/clusters/_sidebar.html.haml +++ b/app/views/clusters/clusters/_sidebar.html.haml diff --git a/app/views/projects/clusters/gcp/_form.html.haml b/app/views/clusters/clusters/gcp/_form.html.haml index 171ceeceb68..8ed4666e79a 100644 --- a/app/views/projects/clusters/gcp/_form.html.haml +++ b/app/views/clusters/clusters/gcp/_form.html.haml @@ -12,14 +12,14 @@ %p= link_to('Select a different Google account', @authorize_url) -= form_for @gcp_cluster, html: { class: 'js-gke-cluster-creation prepend-top-20', data: { token: token_in_session } }, url: create_gcp_namespace_project_clusters_path(@project.namespace, @project), as: :cluster do |field| += form_for @gcp_cluster, html: { class: 'js-gke-cluster-creation prepend-top-20', data: { token: token_in_session } }, url: clusterable.create_gcp_clusters_path, as: :cluster do |field| = form_errors(@gcp_cluster) .form-group = field.label :name, s_('ClusterIntegration|Kubernetes cluster name'), class: 'label-bold' = field.text_field :name, class: 'form-control', placeholder: s_('ClusterIntegration|Kubernetes cluster name') .form-group = field.label :environment_scope, s_('ClusterIntegration|Environment scope'), class: 'label-bold' - = field.text_field :environment_scope, class: 'form-control', readonly: !has_multiple_clusters?(@project), placeholder: s_('ClusterIntegration|Environment scope') + = field.text_field :environment_scope, class: 'form-control', readonly: !has_multiple_clusters?, placeholder: s_('ClusterIntegration|Environment scope') = field.fields_for :provider_gcp, @gcp_cluster.provider_gcp do |provider_gcp_field| .form-group @@ -64,7 +64,7 @@ .form-group .form-check = provider_gcp_field.check_box :legacy_abac, { class: 'form-check-input' }, false, true - = provider_gcp_field.label :legacy_abac, s_('ClusterIntegration|RBAC-enabled cluster (experimental)'), class: 'form-check-label label-bold' + = provider_gcp_field.label :legacy_abac, s_('ClusterIntegration|RBAC-enabled cluster'), class: 'form-check-label label-bold' .form-text.text-muted = s_('ClusterIntegration|Enable this setting if using role-based access control (RBAC).') = s_('ClusterIntegration|This option will allow you to install applications on RBAC clusters.') diff --git a/app/views/projects/clusters/gcp/_header.html.haml b/app/views/clusters/clusters/gcp/_header.html.haml index a2ad3cd64df..a2ad3cd64df 100644 --- a/app/views/projects/clusters/gcp/_header.html.haml +++ b/app/views/clusters/clusters/gcp/_header.html.haml diff --git a/app/views/projects/clusters/gcp/_show.html.haml b/app/views/clusters/clusters/gcp/_show.html.haml index 779c9c245c1..ca55ccb8fdf 100644 --- a/app/views/projects/clusters/gcp/_show.html.haml +++ b/app/views/clusters/clusters/gcp/_show.html.haml @@ -6,7 +6,7 @@ %span.input-group-append = clipboard_button(text: @cluster.name, title: s_('ClusterIntegration|Copy Kubernetes cluster name'), class: 'input-group-text btn-default') -= form_for @cluster, url: namespace_project_cluster_path(@project.namespace, @project, @cluster), as: :cluster do |field| += form_for @cluster, url: clusterable.cluster_path(@cluster), as: :cluster do |field| = form_errors(@cluster) = field.fields_for :platform_kubernetes, @cluster.platform_kubernetes do |platform_kubernetes_field| @@ -40,7 +40,7 @@ .form-group .form-check = platform_kubernetes_field.check_box :authorization_type, { class: 'form-check-input', disabled: true }, 'rbac', 'abac' - = platform_kubernetes_field.label :authorization_type, s_('ClusterIntegration|RBAC-enabled cluster (experimental)'), class: 'form-check-label label-bold' + = platform_kubernetes_field.label :authorization_type, s_('ClusterIntegration|RBAC-enabled cluster'), class: 'form-check-label label-bold' .form-text.text-muted = s_('ClusterIntegration|Enable this setting if using role-based access control (RBAC).') = s_('ClusterIntegration|This option will allow you to install applications on RBAC clusters.') diff --git a/app/views/projects/clusters/index.html.haml b/app/views/clusters/clusters/index.html.haml index a55de84b5cd..a55de84b5cd 100644 --- a/app/views/projects/clusters/index.html.haml +++ b/app/views/clusters/clusters/index.html.haml diff --git a/app/views/projects/clusters/new.html.haml b/app/views/clusters/clusters/new.html.haml index a38003f5750..eeeef6bd824 100644 --- a/app/views/projects/clusters/new.html.haml +++ b/app/views/clusters/clusters/new.html.haml @@ -19,9 +19,9 @@ .tab-content.gitlab-tab-content .tab-pane{ id: 'create-gcp-cluster-pane', class: active_when(active_tab == 'gcp'), role: 'tabpanel' } - = render 'projects/clusters/gcp/header' + = render 'clusters/clusters/gcp/header' - if @valid_gcp_token - = render 'projects/clusters/gcp/form' + = render 'clusters/clusters/gcp/form' - elsif @authorize_url .signin-with-google = link_to(image_tag('auth_buttons/signin_with_google.png', width: '191px'), @authorize_url) @@ -32,5 +32,5 @@ = s_('Google authentication is not %{link_to_documentation}. Ask your GitLab administrator if you want to use this service.').html_safe % { link_to_documentation: link } .tab-pane{ id: 'add-user-cluster-pane', class: active_when(active_tab == 'user'), role: 'tabpanel' } - = render 'projects/clusters/user/header' - = render 'projects/clusters/user/form' + = render 'clusters/clusters/user/header' + = render 'clusters/clusters/user/form' diff --git a/app/views/projects/clusters/show.html.haml b/app/views/clusters/clusters/show.html.haml index eddd3613c5f..1e1157c34bd 100644 --- a/app/views/projects/clusters/show.html.haml +++ b/app/views/clusters/clusters/show.html.haml @@ -1,24 +1,25 @@ - @content_class = "limit-container-width" unless fluid_layout -- add_to_breadcrumbs "Kubernetes Clusters", project_clusters_path(@project) +- add_to_breadcrumbs "Kubernetes Clusters", clusterable.index_path - breadcrumb_title @cluster.name - page_title _("Kubernetes Cluster") +- manage_prometheus_path = edit_project_service_path(@cluster.project, 'prometheus') if @project - expanded = Rails.env.test? -- status_path = status_namespace_project_cluster_path(@cluster.project.namespace, @cluster.project, @cluster.id, format: :json) if can?(current_user, :admin_cluster, @cluster) +- status_path = clusterable.cluster_status_cluster_path(@cluster.id, format: :json) if can?(current_user, :admin_cluster, @cluster) .edit-cluster-form.js-edit-cluster-form{ data: { status_path: status_path, - install_helm_path: install_applications_namespace_project_cluster_path(@cluster.project.namespace, @cluster.project, @cluster, :helm), - install_ingress_path: install_applications_namespace_project_cluster_path(@cluster.project.namespace, @cluster.project, @cluster, :ingress), - install_prometheus_path: install_applications_namespace_project_cluster_path(@cluster.project.namespace, @cluster.project, @cluster, :prometheus), - install_runner_path: install_applications_namespace_project_cluster_path(@cluster.project.namespace, @cluster.project, @cluster, :runner), - install_jupyter_path: install_applications_namespace_project_cluster_path(@cluster.project.namespace, @cluster.project, @cluster, :jupyter), + install_helm_path: clusterable.install_applications_cluster_path(@cluster, :helm), + install_ingress_path: clusterable.install_applications_cluster_path(@cluster, :ingress), + install_prometheus_path: clusterable.install_applications_cluster_path(@cluster, :prometheus), + install_runner_path: clusterable.install_applications_cluster_path(@cluster, :runner), + install_jupyter_path: clusterable.install_applications_cluster_path(@cluster, :jupyter), toggle_status: @cluster.enabled? ? 'true': 'false', cluster_status: @cluster.status_name, cluster_status_reason: @cluster.status_reason, help_path: help_page_path('user/project/clusters/index.md', anchor: 'installing-applications'), ingress_help_path: help_page_path('user/project/clusters/index.md', anchor: 'getting-the-external-ip-address'), ingress_dns_help_path: help_page_path('topics/autodevops/quick_start_guide.md', anchor: 'point-dns-at-cluster-ip'), - manage_prometheus_path: edit_project_service_path(@cluster.project, 'prometheus') } } + manage_prometheus_path: manage_prometheus_path } } .js-cluster-application-notice .flash-container @@ -38,9 +39,9 @@ %p= s_('ClusterIntegration|See and edit the details for your Kubernetes cluster') .settings-content - if @cluster.managed? - = render 'projects/clusters/gcp/show' + = render 'clusters/clusters/gcp/show' - else - = render 'projects/clusters/user/show' + = render 'clusters/clusters/user/show' %section.settings.no-animate#js-cluster-advanced-settings{ class: ('expanded' if expanded) } .settings-header diff --git a/app/views/projects/clusters/user/_form.html.haml b/app/views/clusters/clusters/user/_form.html.haml index 54a6e685bb0..e4758938059 100644 --- a/app/views/projects/clusters/user/_form.html.haml +++ b/app/views/clusters/clusters/user/_form.html.haml @@ -1,9 +1,9 @@ -= form_for @user_cluster, url: create_user_namespace_project_clusters_path(@project.namespace, @project), as: :cluster do |field| += form_for @user_cluster, url: clusterable.create_user_clusters_path, as: :cluster do |field| = form_errors(@user_cluster) .form-group = field.label :name, s_('ClusterIntegration|Kubernetes cluster name'), class: 'label-bold' = field.text_field :name, class: 'form-control', placeholder: s_('ClusterIntegration|Kubernetes cluster name') - - if has_multiple_clusters?(@project) + - if has_multiple_clusters? .form-group = field.label :environment_scope, s_('ClusterIntegration|Environment scope'), class: 'label-bold' = field.text_field :environment_scope, class: 'form-control', placeholder: s_('ClusterIntegration|Environment scope') @@ -28,7 +28,7 @@ .form-group .form-check = platform_kubernetes_field.check_box :authorization_type, { class: 'form-check-input qa-rbac-checkbox' }, 'rbac', 'abac' - = platform_kubernetes_field.label :authorization_type, s_('ClusterIntegration|RBAC-enabled cluster (experimental)'), class: 'form-check-label label-bold' + = platform_kubernetes_field.label :authorization_type, s_('ClusterIntegration|RBAC-enabled cluster'), class: 'form-check-label label-bold' .form-text.text-muted = s_('ClusterIntegration|Enable this setting if using role-based access control (RBAC).') = s_('ClusterIntegration|This option will allow you to install applications on RBAC clusters.') diff --git a/app/views/projects/clusters/user/_header.html.haml b/app/views/clusters/clusters/user/_header.html.haml index 749177fa6c1..749177fa6c1 100644 --- a/app/views/projects/clusters/user/_header.html.haml +++ b/app/views/clusters/clusters/user/_header.html.haml diff --git a/app/views/projects/clusters/user/_show.html.haml b/app/views/clusters/clusters/user/_show.html.haml index 5b57f7ceb7d..ad8c35e32e3 100644 --- a/app/views/projects/clusters/user/_show.html.haml +++ b/app/views/clusters/clusters/user/_show.html.haml @@ -1,4 +1,4 @@ -= form_for @cluster, url: namespace_project_cluster_path(@project.namespace, @project, @cluster), as: :cluster do |field| += form_for @cluster, url: clusterable.cluster_path(@cluster), as: :cluster do |field| = form_errors(@cluster) .form-group = field.label :name, s_('ClusterIntegration|Kubernetes cluster name'), class: 'label-bold' @@ -29,7 +29,7 @@ .form-group .form-check = platform_kubernetes_field.check_box :authorization_type, { class: 'form-check-input', disabled: true }, 'rbac', 'abac' - = platform_kubernetes_field.label :authorization_type, s_('ClusterIntegration|RBAC-enabled cluster (experimental)'), class: 'form-check-label label-bold' + = platform_kubernetes_field.label :authorization_type, s_('ClusterIntegration|RBAC-enabled cluster'), class: 'form-check-label label-bold' .form-text.text-muted = s_('ClusterIntegration|Enable this setting if using role-based access control (RBAC).') = s_('ClusterIntegration|This option will allow you to install applications on RBAC clusters.') diff --git a/app/views/events/_event.html.haml b/app/views/events/_event.html.haml index 78a1d1a0553..2fcb1d1fd2b 100644 --- a/app/views/events/_event.html.haml +++ b/app/views/events/_event.html.haml @@ -1,5 +1,5 @@ - if event.visible_to_user?(current_user) - .event-item{ class: event_row_class(event) } + .event-item .event-item-timestamp #{time_ago_with_tooltip(event.created_at)} diff --git a/app/views/events/event/_common.html.haml b/app/views/events/event/_common.html.haml index 829a3da1558..96d6553a2ac 100644 --- a/app/views/events/event/_common.html.haml +++ b/app/views/events/event/_common.html.haml @@ -1,20 +1,19 @@ = icon_for_profile_event(event) -.event-title - %span.author_name= link_to_author(event) - %span{ class: event.action_name } += event_user_info(event) + +.event-title.d-flex.flex-wrap + = inline_event_icon(event) - if event.target - = event.action_name - %strong - = link_to [event.project.namespace.becomes(Namespace), event.project, event.target], class: 'has-tooltip', title: event.target_title do - = event.target_type.titleize.downcase - = event.target.reference_link_text + %span.event-type.d-inline-block.append-right-4{ class: event.action_name } + = event.action_name + %span.event-target-type.append-right-4= event.target_type.titleize.downcase + = link_to [event.project.namespace.becomes(Namespace), event.project, event.target], class: 'has-tooltip event-target-link append-right-4', title: event.target_title do + = event.target.reference_link_text + - unless event.milestone? + %span.event-target-title.append-right-4= """.html_safe + event.target.title + """.html_safe - else - = event_action_name(event) + %span.event-type.d-inline-block.append-right-4{ class: event.action_name } + = event_action_name(event) = render "events/event_scope", event: event - -- if event.target.respond_to?(:title) - .event-body - .event-note - = event.target.title diff --git a/app/views/events/event/_created_project.html.haml b/app/views/events/event/_created_project.html.haml index 6ad7e157131..2f156603414 100644 --- a/app/views/events/event/_created_project.html.haml +++ b/app/views/events/event/_created_project.html.haml @@ -1,8 +1,10 @@ = icon_for_profile_event(event) -.event-title - %span.author_name= link_to_author(event) - %span{ class: event.action_name } += event_user_info(event) + +.event-title.d-flex.flex-wrap + = inline_event_icon(event) + %span.event-type.d-inline-block.append-right-4{ class: event.action_name } = event_action_name(event) - if event.project diff --git a/app/views/events/event/_note.html.haml b/app/views/events/event/_note.html.haml index cdacd998a69..fb0d2c3b8b0 100644 --- a/app/views/events/event/_note.html.haml +++ b/app/views/events/event/_note.html.haml @@ -1,9 +1,13 @@ = icon_for_profile_event(event) -.event-title - %span.author_name= link_to_author(event) - = event.action_name += event_user_info(event) + +.event-title.d-flex.flex-wrap + = inline_event_icon(event) + %span.event-type.d-inline-block.append-right-4{ class: event.action_name } + = event.action_name = event_note_title_html(event) + %span.event-target-title.append-right-4= """.html_safe + event.target.title + """.html_safe = render "events/event_scope", event: event diff --git a/app/views/events/event/_private.html.haml b/app/views/events/event/_private.html.haml index ccd2aacb4ea..d91f30c07cb 100644 --- a/app/views/events/event/_private.html.haml +++ b/app/views/events/event/_private.html.haml @@ -1,10 +1,11 @@ -.event-inline.event-item +.event-item .event-item-timestamp = time_ago_with_tooltip(event.created_at) - .system-note-image= sprite_icon('eye-slash', size: 16, css_class: 'icon') + .system-note-image= sprite_icon('eye-slash', size: 24, css_class: 'icon') - .event-title - - author_name = capture do - %span.author_name= link_to_author(event) - = s_('Profiles|%{author_name} made a private contribution').html_safe % { author_name: author_name } + = event_user_info(event) + + .event-title.d-flex.flex-wrap + = inline_event_icon(event) + = s_('Profiles|Made a private contribution') diff --git a/app/views/events/event/_push.html.haml b/app/views/events/event/_push.html.haml index 5f0ee79cd9b..82693ec832e 100644 --- a/app/views/events/event/_push.html.haml +++ b/app/views/events/event/_push.html.haml @@ -2,13 +2,15 @@ = icon_for_profile_event(event) -.event-title - %span.author_name= link_to_author(event) - %span.pushed #{event.action_name} #{event.ref_type} - %strong += event_user_info(event) + +.event-title.d-flex.flex-wrap + = inline_event_icon(event) + %span.event-type.d-inline-block.append-right-4.pushed #{event.action_name} #{event.ref_type} + %span - commits_link = project_commits_path(project, event.ref_name) - should_link = event.tag? ? project.repository.tag_exists?(event.ref_name) : project.repository.branch_exists?(event.ref_name) - = link_to_if should_link, event.ref_name, commits_link, class: 'ref-name' + = link_to_if should_link, event.ref_name, commits_link, class: 'ref-name append-right-4' = render "events/event_scope", event: event diff --git a/app/views/groups/labels/index.html.haml b/app/views/groups/labels/index.html.haml index 5b78ce910b8..4df3d831942 100644 --- a/app/views/groups/labels/index.html.haml +++ b/app/views/groups/labels/index.html.haml @@ -6,7 +6,7 @@ - subscribed = params[:subscribed] - labels_or_filters = @labels.exists? || search.present? || subscribed.present? -- if can_admin_label +- if @labels.present? && can_admin_label - content_for(:header_content) do .nav-controls = link_to _('New label'), new_group_label_path(@group), class: "btn btn-success" diff --git a/app/views/notify/changed_milestone_issue_email.html.haml b/app/views/notify/changed_milestone_issue_email.html.haml new file mode 100644 index 00000000000..7d5425fc72d --- /dev/null +++ b/app/views/notify/changed_milestone_issue_email.html.haml @@ -0,0 +1,3 @@ +%p + Milestone changed to + %strong= link_to(@milestone.name, @milestone_url) diff --git a/app/views/notify/changed_milestone_issue_email.text.erb b/app/views/notify/changed_milestone_issue_email.text.erb new file mode 100644 index 00000000000..c5fc0b61518 --- /dev/null +++ b/app/views/notify/changed_milestone_issue_email.text.erb @@ -0,0 +1 @@ +Milestone changed to <%= @milestone.name %> ( <%= @milestone_url %> ) diff --git a/app/views/notify/changed_milestone_merge_request_email.html.haml b/app/views/notify/changed_milestone_merge_request_email.html.haml new file mode 100644 index 00000000000..7d5425fc72d --- /dev/null +++ b/app/views/notify/changed_milestone_merge_request_email.html.haml @@ -0,0 +1,3 @@ +%p + Milestone changed to + %strong= link_to(@milestone.name, @milestone_url) diff --git a/app/views/notify/changed_milestone_merge_request_email.text.erb b/app/views/notify/changed_milestone_merge_request_email.text.erb new file mode 100644 index 00000000000..c5fc0b61518 --- /dev/null +++ b/app/views/notify/changed_milestone_merge_request_email.text.erb @@ -0,0 +1 @@ +Milestone changed to <%= @milestone.name %> ( <%= @milestone_url %> ) diff --git a/app/views/notify/removed_milestone_issue_email.html.haml b/app/views/notify/removed_milestone_issue_email.html.haml new file mode 100644 index 00000000000..7e9205b6491 --- /dev/null +++ b/app/views/notify/removed_milestone_issue_email.html.haml @@ -0,0 +1,2 @@ +%p + Milestone removed diff --git a/app/views/notify/removed_milestone_issue_email.text.erb b/app/views/notify/removed_milestone_issue_email.text.erb new file mode 100644 index 00000000000..0b83ed7a4c5 --- /dev/null +++ b/app/views/notify/removed_milestone_issue_email.text.erb @@ -0,0 +1 @@ +Milestone removed diff --git a/app/views/notify/removed_milestone_merge_request_email.html.haml b/app/views/notify/removed_milestone_merge_request_email.html.haml new file mode 100644 index 00000000000..7e9205b6491 --- /dev/null +++ b/app/views/notify/removed_milestone_merge_request_email.html.haml @@ -0,0 +1,2 @@ +%p + Milestone removed diff --git a/app/views/notify/removed_milestone_merge_request_email.text.erb b/app/views/notify/removed_milestone_merge_request_email.text.erb new file mode 100644 index 00000000000..0b83ed7a4c5 --- /dev/null +++ b/app/views/notify/removed_milestone_merge_request_email.text.erb @@ -0,0 +1 @@ +Milestone removed diff --git a/app/views/projects/ci/builds/_build.html.haml b/app/views/projects/ci/builds/_build.html.haml index f5685d3b50d..0b10c66777a 100644 --- a/app/views/projects/ci/builds/_build.html.haml +++ b/app/views/projects/ci/builds/_build.html.haml @@ -105,10 +105,10 @@ = icon('remove', class: 'cred') - elsif job.scheduled? .btn-group - .btn.btn-default.has-tooltip{ disabled: true, - title: job.scheduled_at } + .btn.btn-default{ disabled: true } = sprite_icon('planning') - = duration_in_numbers(job.execute_in) + %time.js-remaining-time{ datetime: job.scheduled_at.utc.iso8601 } + = duration_in_numbers(job.execute_in) - confirmation_message = s_("DelayedJobs|Are you sure you want to run %{job_name} immediately? This job will run automatically after it's timer finishes.") % { job_name: job.name } = link_to play_project_job_path(job.project, job, return_to: request.original_url), method: :post, diff --git a/app/views/projects/deployments/_rollback.haml b/app/views/projects/deployments/_rollback.haml index 281e042c915..1bd538a08ff 100644 --- a/app/views/projects/deployments/_rollback.haml +++ b/app/views/projects/deployments/_rollback.haml @@ -1,4 +1,4 @@ -- if can?(current_user, :create_deployment, deployment) && deployment.deployable +- if can?(current_user, :create_deployment, deployment) - tooltip = deployment.last? ? s_('Environments|Re-deploy to environment') : s_('Environments|Rollback environment') = link_to [:retry, @project.namespace.becomes(Namespace), @project, deployment.deployable], method: :post, class: 'btn btn-build has-tooltip', title: tooltip do - if deployment.last? diff --git a/app/views/projects/edit.html.haml b/app/views/projects/edit.html.haml index 07fc9e1c682..3aff5538813 100644 --- a/app/views/projects/edit.html.haml +++ b/app/views/projects/edit.html.haml @@ -148,7 +148,7 @@ = link_to 'Archive project', archive_project_path(@project), data: { confirm: "Are you sure that you want to archive this project?" }, method: :post, class: "btn btn-warning" - .sub-section.rename-respository + .sub-section.rename-repository %h4.warning-title Rename repository = render 'projects/errors' diff --git a/app/views/projects/empty.html.haml b/app/views/projects/empty.html.haml index 75f35360e5e..936900a0087 100644 --- a/app/views/projects/empty.html.haml +++ b/app/views/projects/empty.html.haml @@ -36,7 +36,7 @@ .scrolling-tabs-container.inner-page-scroll-tabs.is-smaller .fade-left= icon('angle-left') .fade-right= icon('angle-right') - .nav-links.scrolling-tabs + .nav-links.scrolling-tabs.quick-links = render 'stat_anchor_list', anchors: @project.empty_repo_statistics_anchors = render 'stat_anchor_list', anchors: @project.empty_repo_statistics_buttons diff --git a/app/views/projects/labels/index.html.haml b/app/views/projects/labels/index.html.haml index 06ee883d6dc..2c6484c2c99 100644 --- a/app/views/projects/labels/index.html.haml +++ b/app/views/projects/labels/index.html.haml @@ -5,7 +5,7 @@ - subscribed = params[:subscribed] - labels_or_filters = @labels.exists? || @prioritized_labels.exists? || search.present? || subscribed.present? -- if can_admin_label +- if @labels.present? && can_admin_label - content_for(:header_content) do .nav-controls = link_to _('New label'), new_project_label_path(@project), class: "btn btn-success qa-label-create-new" diff --git a/app/views/projects/merge_requests/show.html.haml b/app/views/projects/merge_requests/show.html.haml index 5d1bbb077af..515499956a2 100644 --- a/app/views/projects/merge_requests/show.html.haml +++ b/app/views/projects/merge_requests/show.html.haml @@ -34,7 +34,7 @@ .fade-left= icon('angle-left') .fade-right= icon('angle-right') %ul.merge-request-tabs.nav-tabs.nav.nav-links.scrolling-tabs - %li.notes-tab + %li.notes-tab.qa-notes-tab = tab_link_for @merge_request, :show, force_link: @commit.present? do Discussion %span.badge.badge-pill= @merge_request.related_notes.user.count @@ -48,7 +48,7 @@ = tab_link_for @merge_request, :pipelines do Pipelines %span.badge.badge-pill.js-pipelines-mr-count= @pipelines.size - %li.diffs-tab + %li.diffs-tab.qa-diffs-tab = tab_link_for @merge_request, :diffs do Changes %span.badge.badge-pill= @merge_request.diff_size diff --git a/app/views/projects/pipelines/_with_tabs.html.haml b/app/views/projects/pipelines/_with_tabs.html.haml index 95bba47802c..66e202103a9 100644 --- a/app/views/projects/pipelines/_with_tabs.html.haml +++ b/app/views/projects/pipelines/_with_tabs.html.haml @@ -61,12 +61,14 @@ %td.responsive-table-cell.build-failure{ data: { column: _("Failure")} } = build.present.callout_failure_message %td.responsive-table-cell.build-actions - = link_to retry_project_job_path(build.project, build, return_to: request.original_url), method: :post, title: _('Retry'), class: 'btn btn-build' do - = icon('repeat') - %tr.build-trace-row.responsive-table-border-end - %td - %td.responsive-table-cell.build-trace-container{ colspan: 4 } - %pre.build-trace.build-trace-rounded - %code.bash.js-build-output - = build_summary(build) + - if can?(current_user, :update_build, job) + = link_to retry_project_job_path(build.project, build, return_to: request.original_url), method: :post, title: _('Retry'), class: 'btn btn-build' do + = icon('repeat') + - if can?(current_user, :read_build, job) + %tr.build-trace-row.responsive-table-border-end + %td + %td.responsive-table-cell.build-trace-container{ colspan: 4 } + %pre.build-trace.build-trace-rounded + %code.bash.js-build-output + = build_summary(build) = render_if_exists "projects/pipelines/tabs_content", pipeline: @pipeline, project: @project diff --git a/app/views/projects/show.html.haml b/app/views/projects/show.html.haml index 283031b06da..f29ce4f5c06 100644 --- a/app/views/projects/show.html.haml +++ b/app/views/projects/show.html.haml @@ -22,7 +22,7 @@ .scrolling-tabs-container.inner-page-scroll-tabs.is-smaller .fade-left= icon('angle-left') .fade-right= icon('angle-right') - .nav-links.scrolling-tabs + .nav-links.scrolling-tabs.quick-links = render 'stat_anchor_list', anchors: @project.statistics_anchors(show_auto_devops_callout: show_auto_devops_callout) = render 'stat_anchor_list', anchors: @project.statistics_buttons(show_auto_devops_callout: show_auto_devops_callout) diff --git a/app/views/projects/tree/_blob_item.html.haml b/app/views/projects/tree/_blob_item.html.haml deleted file mode 100644 index f79f3af36d4..00000000000 --- a/app/views/projects/tree/_blob_item.html.haml +++ /dev/null @@ -1,12 +0,0 @@ -- is_lfs_blob = @lfs_blob_ids.include?(blob_item.id) -%tr{ class: "tree-item #{tree_hex_class(blob_item)}" } - %td.tree-item-file-name - = tree_icon(type, blob_item.mode, blob_item.name) - - file_name = blob_item.name - = link_to project_blob_path(@project, tree_join(@id || @commit.id, blob_item.name)), class: 'str-truncated', title: file_name do - %span= file_name - - if is_lfs_blob - %span.badge.label-lfs.prepend-left-5 LFS - %td.d-none.d-sm-table-cell.tree-commit - %td.tree-time-ago.cgray.text-right - = render 'projects/tree/spinner' diff --git a/app/views/projects/tree/_spinner.html.haml b/app/views/projects/tree/_spinner.html.haml deleted file mode 100644 index b47ad0f41e4..00000000000 --- a/app/views/projects/tree/_spinner.html.haml +++ /dev/null @@ -1,3 +0,0 @@ -%span.log_loading.hide - %i.fa.fa-spinner.fa-spin - Loading commit data... diff --git a/app/views/projects/tree/_submodule_item.html.haml b/app/views/projects/tree/_submodule_item.html.haml deleted file mode 100644 index e563c8c4036..00000000000 --- a/app/views/projects/tree/_submodule_item.html.haml +++ /dev/null @@ -1,6 +0,0 @@ -%tr.tree-item - %td.tree-item-file-name - %i.fa.fa-archive.fa-fw - = submodule_link(submodule_item, @ref) - %td - %td.d-none.d-sm-table-cell diff --git a/app/views/projects/tree/_tree_item.html.haml b/app/views/projects/tree/_tree_item.html.haml deleted file mode 100644 index ce0cd95b468..00000000000 --- a/app/views/projects/tree/_tree_item.html.haml +++ /dev/null @@ -1,9 +0,0 @@ -%tr{ class: "tree-item #{tree_hex_class(tree_item)}" } - %td.tree-item-file-name - = tree_icon(type, tree_item.mode, tree_item.name) - - path = flatten_tree(@path, tree_item) - = link_to project_tree_path(@project, tree_join(@id || @commit.id, path)), class: 'str-truncated', title: path do - %span= path - %td.d-none.d-sm-table-cell.tree-commit - %td.tree-time-ago.text-right - = render 'projects/tree/spinner' diff --git a/app/views/projects/tree/_tree_row.html.haml b/app/views/projects/tree/_tree_row.html.haml index 0a5c6f048f7..8a27ea66523 100644 --- a/app/views/projects/tree/_tree_row.html.haml +++ b/app/views/projects/tree/_tree_row.html.haml @@ -1,6 +1,27 @@ -- if tree_row.type == :tree - = render partial: 'projects/tree/tree_item', object: tree_row, as: 'tree_item', locals: { type: 'folder' } -- elsif tree_row.type == :blob - = render partial: 'projects/tree/blob_item', object: tree_row, as: 'blob_item', locals: { type: 'file' } -- elsif tree_row.type == :commit - = render partial: 'projects/tree/submodule_item', object: tree_row, as: 'submodule_item' +- tree_row_name = tree_row.name +- tree_row_type = tree_row.type + +%tr{ class: "tree-item file_#{hexdigest(tree_row_name)}" } + %td.tree-item-file-name + - if tree_row_type == :tree + = tree_icon('folder', tree_row.mode, tree_row.name) + - path = flatten_tree(@path, tree_row) + %a.str-truncated{ href: fast_project_tree_path(@project, tree_join(@id || @commit.id, path)), title: path } + %span= path + + - elsif tree_row_type == :blob + = tree_icon('file', tree_row.mode, tree_row_name) + %a.str-truncated{ href: fast_project_blob_path(@project, tree_join(@id || @commit.id, tree_row_name)), title: tree_row_name } + %span= tree_row_name + - if @lfs_blob_ids.include?(tree_row.id) + %span.badge.label-lfs.prepend-left-5 LFS + + - elsif tree_row_type == :commit + = tree_icon('archive', tree_row.mode, tree_row.name) + = submodule_link(tree_row, @ref) + + %td.d-none.d-sm-table-cell.tree-commit + %td.tree-time-ago.text-right + %span.log_loading.hide + %i.fa.fa-spinner.fa-spin + Loading commit data... diff --git a/app/views/shared/boards/components/sidebar/_labels.html.haml b/app/views/shared/boards/components/sidebar/_labels.html.haml index 6138914206b..19159684420 100644 --- a/app/views/shared/boards/components/sidebar/_labels.html.haml +++ b/app/views/shared/boards/components/sidebar/_labels.html.haml @@ -19,13 +19,13 @@ ":value" => "label.id" } .dropdown %button.dropdown-menu-toggle.js-label-select.js-multiselect.js-issue-board-sidebar{ type: "button", - "v-bind:data-selected" => "selectedLabels", + ":data-selected" => "selectedLabels", + ":data-labels" => "issue.assignableLabelsEndpoint", data: { toggle: "dropdown", field_name: "issue[label_names][]", show_no: "true", show_any: "true", project_id: @project&.try(:id), - labels: labels_filter_path_with_defaults, namespace_path: @namespace_path, project_path: @project.try(:path) } } %span.dropdown-toggle-text diff --git a/app/views/shared/empty_states/_labels.html.haml b/app/views/shared/empty_states/_labels.html.haml index b629ceafeb3..9133ce8ed22 100644 --- a/app/views/shared/empty_states/_labels.html.haml +++ b/app/views/shared/empty_states/_labels.html.haml @@ -6,6 +6,9 @@ .text-content %h4= _("Labels can be applied to issues and merge requests to categorize them.") %p= _("You can also star a label to make it a priority label.") - - if can?(current_user, :admin_label, @project) - = link_to _('New label'), new_project_label_path(@project), class: 'btn btn-success', title: _('New label'), id: 'new_label_link' - = link_to _('Generate a default set of labels'), generate_project_labels_path(@project), method: :post, class: 'btn btn-success btn-inverted', title: _('Generate a default set of labels'), id: 'generate_labels_link' + .text-center + - if can?(current_user, :admin_label, @project) + = link_to _('New label'), new_project_label_path(@project), class: 'btn btn-success', title: _('New label'), id: 'new_label_link' + = link_to _('Generate a default set of labels'), generate_project_labels_path(@project), method: :post, class: 'btn btn-success btn-inverted', title: _('Generate a default set of labels'), id: 'generate_labels_link' + - if can?(current_user, :admin_label, @group) + = link_to _('New label'), new_group_label_path(@group), class: 'btn btn-success', title: _('New label'), id: 'new_label_link' diff --git a/app/views/shared/issuable/_search_bar.html.haml b/app/views/shared/issuable/_search_bar.html.haml index 1d876cc4a5d..d27f79dc404 100644 --- a/app/views/shared/issuable/_search_bar.html.haml +++ b/app/views/shared/issuable/_search_bar.html.haml @@ -105,6 +105,14 @@ %span.label-title.js-data-value {{title}} #js-dropdown-my-reaction.filtered-search-input-dropdown-menu.dropdown-menu + %ul{ data: { dropdown: true } } + %li.filter-dropdown-item{ data: { value: 'none' } } + %button.btn.btn-link{ type: 'button' } + = _('None') + %li.filter-dropdown-item{ data: { value: 'any' } } + %button.btn.btn-link{ type: 'button' } + = _('Any') + %li.divider.droplab-item-ignore %ul.filter-dropdown{ data: { dynamic: true, dropdown: true } } %li.filter-dropdown-item %button.btn.btn-link{ type: 'button' } diff --git a/app/views/shared/members/_member.html.haml b/app/views/shared/members/_member.html.haml index 2682d92fc56..b4b3f4a6b7e 100644 --- a/app/views/shared/members/_member.html.haml +++ b/app/views/shared/members/_member.html.haml @@ -14,6 +14,8 @@ = user_status(user) %span.cgray= user.to_reference + = render_if_exists 'shared/members/ee/sso_badge', member: member + - if user == current_user %span.badge.badge-success.prepend-left-5 It's you diff --git a/app/views/shared/projects/_search_form.html.haml b/app/views/shared/projects/_search_form.html.haml index b89194bcc67..3b5c13ed93a 100644 --- a/app/views/shared/projects/_search_form.html.haml +++ b/app/views/shared/projects/_search_form.html.haml @@ -21,3 +21,5 @@ - if params[:visibility_level].present? = hidden_field_tag :visibility_level, params[:visibility_level] + + = render_if_exists 'shared/projects/search_fields' diff --git a/app/views/users/_overview.html.haml b/app/views/users/_overview.html.haml index f8b3754840d..cf525f2bb2d 100644 --- a/app/views/users/_overview.html.haml +++ b/app/views/users/_overview.html.haml @@ -11,8 +11,8 @@ - if can?(current_user, :read_cross_project) .activities-block - .content-block - %h5.prepend-top-10 + .border-bottom.prepend-top-16 + %h5 = s_('UserProfile|Recent contributions') .overview-content-list{ data: { href: user_path } } .center.light.loading @@ -22,7 +22,7 @@ .col-md-12.col-lg-6 .projects-block - .content-block + .border-bottom.prepend-top-16 %h4 = s_('UserProfile|Personal projects') .overview-content-list{ data: { href: user_projects_path } } diff --git a/app/workers/all_queues.yml b/app/workers/all_queues.yml index f21789de37d..953ab95735b 100644 --- a/app/workers/all_queues.yml +++ b/app/workers/all_queues.yml @@ -28,6 +28,7 @@ - gcp_cluster:cluster_wait_for_app_installation - gcp_cluster:wait_for_cluster_creation - gcp_cluster:cluster_wait_for_ingress_ip_address +- gcp_cluster:cluster_platform_configure - github_import_advance_stage - github_importer:github_import_import_diff_note @@ -72,6 +73,8 @@ - pipeline_processing:update_head_pipeline_for_merge_request - pipeline_processing:ci_build_schedule +- deployment:deployments_success + - repository_check:repository_check_clear - repository_check:repository_check_batch - repository_check:repository_check_single_repository diff --git a/app/workers/build_success_worker.rb b/app/workers/build_success_worker.rb index c17608f7378..9a865fea621 100644 --- a/app/workers/build_success_worker.rb +++ b/app/workers/build_success_worker.rb @@ -10,13 +10,27 @@ class BuildSuccessWorker def perform(build_id) Ci::Build.find_by(id: build_id).try do |build| create_deployment(build) if build.has_environment? + stop_environment(build) if build.stops_environment? end end # rubocop: enable CodeReuse/ActiveRecord private + ## + # Deprecated: + # As of 11.5, we started creating a deployment record when ci_builds record is created. + # Therefore we no longer need to create a deployment, after a build succeeded. + # We're leaving this code for the transition period, but we can remove this code in 11.6. def create_deployment(build) - CreateDeploymentService.new(build).execute + build.create_deployment.try do |deployment| + deployment.succeed + end + end + + ## + # TODO: This should be processed in DeploymentSuccessWorker once we started storing `action` value in `deployments` records + def stop_environment(build) + build.persisted_environment.fire_state_event(:stop) end end diff --git a/app/workers/cluster_platform_configure_worker.rb b/app/workers/cluster_platform_configure_worker.rb new file mode 100644 index 00000000000..68e8335a09d --- /dev/null +++ b/app/workers/cluster_platform_configure_worker.rb @@ -0,0 +1,22 @@ +# frozen_string_literal: true + +class ClusterPlatformConfigureWorker + include ApplicationWorker + include ClusterQueue + + def perform(cluster_id) + Clusters::Cluster.find_by_id(cluster_id).try do |cluster| + next unless cluster.cluster_project + + kubernetes_namespace = cluster.find_or_initialize_kubernetes_namespace(cluster.cluster_project) + + Clusters::Gcp::Kubernetes::CreateOrUpdateNamespaceService.new( + cluster: cluster, + kubernetes_namespace: kubernetes_namespace + ).execute + end + + rescue ::Kubeclient::HttpError => err + Rails.logger.error "Failed to create/update Kubernetes Namespace. id: #{kubernetes_namespace.id} message: #{err.message}" + end +end diff --git a/app/workers/cluster_provision_worker.rb b/app/workers/cluster_provision_worker.rb index 59de7903c1c..3d5894b73ec 100644 --- a/app/workers/cluster_provision_worker.rb +++ b/app/workers/cluster_provision_worker.rb @@ -9,6 +9,8 @@ class ClusterProvisionWorker cluster.provider.try do |provider| Clusters::Gcp::ProvisionService.new.execute(provider) if cluster.gcp? end + + ClusterPlatformConfigureWorker.perform_async(cluster.id) if cluster.user? end end end diff --git a/app/workers/deployments/success_worker.rb b/app/workers/deployments/success_worker.rb new file mode 100644 index 00000000000..da517f3fb26 --- /dev/null +++ b/app/workers/deployments/success_worker.rb @@ -0,0 +1,17 @@ +# frozen_string_literal: true + +module Deployments + class SuccessWorker + include ApplicationWorker + + queue_namespace :deployment + + def perform(deployment_id) + Deployment.find_by_id(deployment_id).try do |deployment| + break unless deployment.success? + + UpdateDeploymentService.new(deployment).execute + end + end + end +end diff --git a/app/workers/gitlab/github_import/advance_stage_worker.rb b/app/workers/gitlab/github_import/advance_stage_worker.rb index cd2ceb8dcdf..2b49860025a 100644 --- a/app/workers/gitlab/github_import/advance_stage_worker.rb +++ b/app/workers/gitlab/github_import/advance_stage_worker.rb @@ -14,7 +14,7 @@ module Gitlab INTERVAL = 30.seconds.to_i # The number of seconds to wait (while blocking the thread) before - # continueing to the next waiter. + # continuing to the next waiter. BLOCKING_WAIT_TIME = 5 # The known importer stages and their corresponding Sidekiq workers. diff --git a/changelogs/unreleased/18933-render-index-as-readme.yml b/changelogs/unreleased/18933-render-index-as-readme.yml new file mode 100644 index 00000000000..44acc2c719a --- /dev/null +++ b/changelogs/unreleased/18933-render-index-as-readme.yml @@ -0,0 +1,5 @@ +--- +title: Make index.* render like README.* when it's present in a repository +merge_request: 22639 +author: Jakub Jirutka +type: added diff --git a/changelogs/unreleased/25140-disable-stop-button.yml b/changelogs/unreleased/25140-disable-stop-button.yml new file mode 100644 index 00000000000..a6ef52c3155 --- /dev/null +++ b/changelogs/unreleased/25140-disable-stop-button.yml @@ -0,0 +1,5 @@ +--- +title: Disables stop environment button while the deploy is in progress +merge_request: +author: +type: other diff --git a/changelogs/unreleased/28249-add-pagination.yml b/changelogs/unreleased/28249-add-pagination.yml new file mode 100644 index 00000000000..df15094405a --- /dev/null +++ b/changelogs/unreleased/28249-add-pagination.yml @@ -0,0 +1,5 @@ +--- +title: Adds pagination to pipelines table in merge request page +merge_request: +author: +type: performance diff --git a/changelogs/unreleased/44012-filter-reactions-none-any.yml b/changelogs/unreleased/44012-filter-reactions-none-any.yml new file mode 100644 index 00000000000..5d685010f8a --- /dev/null +++ b/changelogs/unreleased/44012-filter-reactions-none-any.yml @@ -0,0 +1,5 @@ +--- +title: Add None / Any options to reactions filter +merge_request: 22638 +author: Heinrich Lee Yu +type: added diff --git a/changelogs/unreleased/49403-redesign-activity-feed.yml b/changelogs/unreleased/49403-redesign-activity-feed.yml new file mode 100644 index 00000000000..cec53a3ef5a --- /dev/null +++ b/changelogs/unreleased/49403-redesign-activity-feed.yml @@ -0,0 +1,4 @@ +title: Redesign activity feed +merge_request: 22217 +author: +type: other diff --git a/changelogs/unreleased/51259-ci-cd-gitlab-ui.yml b/changelogs/unreleased/51259-ci-cd-gitlab-ui.yml new file mode 100644 index 00000000000..a15f1c033b3 --- /dev/null +++ b/changelogs/unreleased/51259-ci-cd-gitlab-ui.yml @@ -0,0 +1,5 @@ +--- +title: Uses gitlab-ui components in jobs components +merge_request: +author: +type: other diff --git a/changelogs/unreleased/51620-cannot-add-label-to-issue-from-board.yml b/changelogs/unreleased/51620-cannot-add-label-to-issue-from-board.yml new file mode 100644 index 00000000000..9e99779d352 --- /dev/null +++ b/changelogs/unreleased/51620-cannot-add-label-to-issue-from-board.yml @@ -0,0 +1,4 @@ +title: Make Issue Board sidebar show project-specific labels based on selected Issue +merge_request: 22475 +author: +type: fixed diff --git a/changelogs/unreleased/51716-add-kubernetes-namespace-background-migration.yml b/changelogs/unreleased/51716-add-kubernetes-namespace-background-migration.yml new file mode 100644 index 00000000000..89a91e8deaf --- /dev/null +++ b/changelogs/unreleased/51716-add-kubernetes-namespace-background-migration.yml @@ -0,0 +1,5 @@ +--- +title: Add background migration to populate Kubernetes namespaces +merge_request: 22433 +author: +type: added diff --git a/changelogs/unreleased/51716-create-kube-namespace.yml b/changelogs/unreleased/51716-create-kube-namespace.yml new file mode 100644 index 00000000000..851e19c0a38 --- /dev/null +++ b/changelogs/unreleased/51716-create-kube-namespace.yml @@ -0,0 +1,5 @@ +--- +title: Extend RBAC by having a service account restricted to project's namespace +merge_request: 22011 +author: +type: other diff --git a/changelogs/unreleased/52300-pool-repositories.yml b/changelogs/unreleased/52300-pool-repositories.yml new file mode 100644 index 00000000000..5435f3aa21f --- /dev/null +++ b/changelogs/unreleased/52300-pool-repositories.yml @@ -0,0 +1,5 @@ +--- +title: Start tracking shards and pool repositories in the database +merge_request: 22482 +author: +type: other diff --git a/changelogs/unreleased/52382-filter-milestone-api-none-any.yml b/changelogs/unreleased/52382-filter-milestone-api-none-any.yml new file mode 100644 index 00000000000..a7559a25645 --- /dev/null +++ b/changelogs/unreleased/52382-filter-milestone-api-none-any.yml @@ -0,0 +1,5 @@ +--- +title: Standardize milestones filter in APIs to None / Any +merge_request: 22637 +author: Heinrich Lee Yu +type: changed diff --git a/changelogs/unreleased/52548-links-in-tabs-of-the-labels-index-pages-ends-with-html.yml b/changelogs/unreleased/52548-links-in-tabs-of-the-labels-index-pages-ends-with-html.yml new file mode 100644 index 00000000000..052ef70c41a --- /dev/null +++ b/changelogs/unreleased/52548-links-in-tabs-of-the-labels-index-pages-ends-with-html.yml @@ -0,0 +1,5 @@ +--- +title: Fix bug when links in tabs of the labels index pages ends with .html +merge_request: 22716 +author: +type: fixed diff --git a/changelogs/unreleased/52925-scheduled-pipelines-ui-problems.yml b/changelogs/unreleased/52925-scheduled-pipelines-ui-problems.yml new file mode 100644 index 00000000000..792b24d75ac --- /dev/null +++ b/changelogs/unreleased/52925-scheduled-pipelines-ui-problems.yml @@ -0,0 +1,5 @@ +--- +title: Fixing styling issues on the scheduled pipelines page +merge_request: +author: +type: fixed diff --git a/changelogs/unreleased/53070-fix-enable-usage-ping-link.yml b/changelogs/unreleased/53070-fix-enable-usage-ping-link.yml deleted file mode 100644 index 605d3679159..00000000000 --- a/changelogs/unreleased/53070-fix-enable-usage-ping-link.yml +++ /dev/null @@ -1,5 +0,0 @@ ---- -title: "fix link to enable usage ping from convdev index" -merge_request: 22545 -author: Anand Capur -type: fixed diff --git a/changelogs/unreleased/53230-remove_personal_access_tokens_finder_find_by_method.yml b/changelogs/unreleased/53230-remove_personal_access_tokens_finder_find_by_method.yml new file mode 100644 index 00000000000..d4d78a2fd06 --- /dev/null +++ b/changelogs/unreleased/53230-remove_personal_access_tokens_finder_find_by_method.yml @@ -0,0 +1,5 @@ +--- +title: Remove PersonalAccessTokensFinder#find_by method +merge_request: 22617 +author: +type: fixed diff --git a/changelogs/unreleased/53362-allow-concurrency-in-puma.yml b/changelogs/unreleased/53362-allow-concurrency-in-puma.yml new file mode 100644 index 00000000000..5fbda0161c1 --- /dev/null +++ b/changelogs/unreleased/53362-allow-concurrency-in-puma.yml @@ -0,0 +1,5 @@ +--- +title: Allow Rails concurrency when running in Puma +merge_request: 22751 +author: +type: performance diff --git a/changelogs/unreleased/53450-wrong-value-for-kubernetes_version-variable.yml b/changelogs/unreleased/53450-wrong-value-for-kubernetes_version-variable.yml new file mode 100644 index 00000000000..cd9300ca2d1 --- /dev/null +++ b/changelogs/unreleased/53450-wrong-value-for-kubernetes_version-variable.yml @@ -0,0 +1,5 @@ +--- +title: Bump KUBERNETES_VERSION for Auto DevOps to latest 1.10 series +merge_request: 22757 +author: +type: other diff --git a/changelogs/unreleased/53533-fix-broken-link.yml b/changelogs/unreleased/53533-fix-broken-link.yml new file mode 100644 index 00000000000..6d55c75d82e --- /dev/null +++ b/changelogs/unreleased/53533-fix-broken-link.yml @@ -0,0 +1,5 @@ +--- +title: Render unescaped link for failed pipeline status +merge_request: 22807 +author: +type: fixed diff --git a/changelogs/unreleased/7737-ci-pipeline-view-slowed-down-massivly-if-security-tabs-has-many-entries-ee.yml b/changelogs/unreleased/7737-ci-pipeline-view-slowed-down-massivly-if-security-tabs-has-many-entries-ee.yml new file mode 100644 index 00000000000..aaae8feb220 --- /dev/null +++ b/changelogs/unreleased/7737-ci-pipeline-view-slowed-down-massivly-if-security-tabs-has-many-entries-ee.yml @@ -0,0 +1,5 @@ +--- +title: Improve performance of rendering large reports +merge_request: 22835 +author: +type: performance diff --git a/changelogs/unreleased/ab-45608-stuck-mr-query.yml b/changelogs/unreleased/ab-45608-stuck-mr-query.yml new file mode 100644 index 00000000000..3b64534e480 --- /dev/null +++ b/changelogs/unreleased/ab-45608-stuck-mr-query.yml @@ -0,0 +1,5 @@ +--- +title: Add index to find stuck merge requests. +merge_request: 22749 +author: +type: performance diff --git a/changelogs/unreleased/add-scheduled-flag-to-job-entity.yml b/changelogs/unreleased/add-scheduled-flag-to-job-entity.yml new file mode 100644 index 00000000000..a80b5a931b9 --- /dev/null +++ b/changelogs/unreleased/add-scheduled-flag-to-job-entity.yml @@ -0,0 +1,5 @@ +--- +title: Add scheduled flag to job entity +merge_request: 22710 +author: +type: other diff --git a/changelogs/unreleased/ccr-51052_keep_labels_on_issue.yml b/changelogs/unreleased/ccr-51052_keep_labels_on_issue.yml new file mode 100644 index 00000000000..7ef857d38ed --- /dev/null +++ b/changelogs/unreleased/ccr-51052_keep_labels_on_issue.yml @@ -0,0 +1,5 @@ +--- +title: Fixed label removal from issue +merge_request: 22762 +author: +type: fixed diff --git a/changelogs/unreleased/ccr-51520_change_milestone_email.yml b/changelogs/unreleased/ccr-51520_change_milestone_email.yml new file mode 100644 index 00000000000..ce4beba2c5f --- /dev/null +++ b/changelogs/unreleased/ccr-51520_change_milestone_email.yml @@ -0,0 +1,5 @@ +--- +title: Add email for milestone change +merge_request: 22279 +author: +type: added diff --git a/changelogs/unreleased/disallow-retry-of-old-builds.yml b/changelogs/unreleased/disallow-retry-of-old-builds.yml new file mode 100644 index 00000000000..03992fc0213 --- /dev/null +++ b/changelogs/unreleased/disallow-retry-of-old-builds.yml @@ -0,0 +1,5 @@ +--- +title: Soft-archive old jobs +merge_request: +author: +type: added diff --git a/changelogs/unreleased/fast_project_blob_path.yml b/changelogs/unreleased/fast_project_blob_path.yml new file mode 100644 index 00000000000..b56c9d9cf59 --- /dev/null +++ b/changelogs/unreleased/fast_project_blob_path.yml @@ -0,0 +1,5 @@ +--- +title: Improve performance of tree rendering in repositories with lots of items +merge_request: +author: +type: performance diff --git a/changelogs/unreleased/frozen-string-enable-lib-gitlab-ci-remain.yml b/changelogs/unreleased/frozen-string-enable-lib-gitlab-ci-remain.yml new file mode 100644 index 00000000000..ecbfc323080 --- /dev/null +++ b/changelogs/unreleased/frozen-string-enable-lib-gitlab-ci-remain.yml @@ -0,0 +1,5 @@ +--- +title: Enable frozen string for remaining lib/gitlab/ci/**/*.rb +merge_request: +author: gfyoung +type: performance diff --git a/changelogs/unreleased/gl-ui-modal.yml b/changelogs/unreleased/gl-ui-modal.yml new file mode 100644 index 00000000000..fbdb8260d24 --- /dev/null +++ b/changelogs/unreleased/gl-ui-modal.yml @@ -0,0 +1,5 @@ +--- +title: Remove gitlab-ui's modal from global +merge_request: +author: +type: performance diff --git a/changelogs/unreleased/gl-ui-pagination.yml b/changelogs/unreleased/gl-ui-pagination.yml new file mode 100644 index 00000000000..cf73d6a1f8f --- /dev/null +++ b/changelogs/unreleased/gl-ui-pagination.yml @@ -0,0 +1,5 @@ +--- +title: Remove gitlab-ui's pagination from global +merge_request: +author: +type: performance diff --git a/changelogs/unreleased/gl-ui-progress-bar.yml b/changelogs/unreleased/gl-ui-progress-bar.yml new file mode 100644 index 00000000000..1e584dacd6f --- /dev/null +++ b/changelogs/unreleased/gl-ui-progress-bar.yml @@ -0,0 +1,5 @@ +--- +title: Remove gitlab-ui's progress bar from global +merge_request: +author: +type: performance diff --git a/changelogs/unreleased/gl-ui-tooltip.yml b/changelogs/unreleased/gl-ui-tooltip.yml new file mode 100644 index 00000000000..99ded9f812e --- /dev/null +++ b/changelogs/unreleased/gl-ui-tooltip.yml @@ -0,0 +1,5 @@ +--- +title: Remove gitlab-ui's tooltip from global +merge_request: +author: +type: performance diff --git a/changelogs/unreleased/gt-fix-ide-typos-in-props.yml b/changelogs/unreleased/gt-fix-ide-typos-in-props.yml new file mode 100644 index 00000000000..a81b227c82f --- /dev/null +++ b/changelogs/unreleased/gt-fix-ide-typos-in-props.yml @@ -0,0 +1,5 @@ +--- +title: Fix IDE typos in props +merge_request: 22685 +author: George Tsiolis +type: other diff --git a/changelogs/unreleased/gt-fix-quick-links-button-styles.yml b/changelogs/unreleased/gt-fix-quick-links-button-styles.yml new file mode 100644 index 00000000000..4c1150631f8 --- /dev/null +++ b/changelogs/unreleased/gt-fix-quick-links-button-styles.yml @@ -0,0 +1,5 @@ +--- +title: Fix quick links button styles +merge_request: 22657 +author: George Tsiolis +type: fixed diff --git a/changelogs/unreleased/gt-update-project-and-group-labels-empty-state.yml b/changelogs/unreleased/gt-update-project-and-group-labels-empty-state.yml new file mode 100644 index 00000000000..d644ca86b79 --- /dev/null +++ b/changelogs/unreleased/gt-update-project-and-group-labels-empty-state.yml @@ -0,0 +1,5 @@ +--- +title: Update project and group labels empty state +merge_request: 22745 +author: George Tsiolis +type: changed diff --git a/changelogs/unreleased/issue_51323.yml b/changelogs/unreleased/issue_51323.yml new file mode 100644 index 00000000000..b0e83e303d1 --- /dev/null +++ b/changelogs/unreleased/issue_51323.yml @@ -0,0 +1,5 @@ +--- +title: Add 'only history' option to notes filter +merge_request: +author: +type: changed diff --git a/changelogs/unreleased/kinolaev-master-patch-91872.yml b/changelogs/unreleased/kinolaev-master-patch-91872.yml new file mode 100644 index 00000000000..053e9101e39 --- /dev/null +++ b/changelogs/unreleased/kinolaev-master-patch-91872.yml @@ -0,0 +1,5 @@ +--- +title: Change HELM_HOST in Auto-DevOps template to work behind proxy +merge_request: 22596 +author: Sergej Nikolaev <kinolaev@gmail.com> +type: fixed diff --git a/changelogs/unreleased/mr-file-tree-inline-fluid-width-fix.yml b/changelogs/unreleased/mr-file-tree-inline-fluid-width-fix.yml deleted file mode 100644 index b61f47724fc..00000000000 --- a/changelogs/unreleased/mr-file-tree-inline-fluid-width-fix.yml +++ /dev/null @@ -1,5 +0,0 @@ ---- -title: Fixed merge request fill tree toggling not respecting fluid width preference -merge_request: -author: -type: fixed diff --git a/changelogs/unreleased/rake-gitaly-check.yml b/changelogs/unreleased/rake-gitaly-check.yml new file mode 100644 index 00000000000..90fbd62d203 --- /dev/null +++ b/changelogs/unreleased/rake-gitaly-check.yml @@ -0,0 +1,5 @@ +--- +title: Add gitlab:gitaly:check task for Gitaly health check +merge_request: 22063 +author: +type: other diff --git a/changelogs/unreleased/related_mrs.yml b/changelogs/unreleased/related_mrs.yml new file mode 100644 index 00000000000..cc89e9d0cdb --- /dev/null +++ b/changelogs/unreleased/related_mrs.yml @@ -0,0 +1,5 @@ +--- +title: Add API endpoint to list issue related merge requests +merge_request: 21806 +author: Helmut Januschka +type: added diff --git a/changelogs/unreleased/remove-ci_enable_scheduled_build-feature-flag.yml b/changelogs/unreleased/remove-ci_enable_scheduled_build-feature-flag.yml new file mode 100644 index 00000000000..ce52a487551 --- /dev/null +++ b/changelogs/unreleased/remove-ci_enable_scheduled_build-feature-flag.yml @@ -0,0 +1,5 @@ +--- +title: Remove `ci_enable_scheduled_build` feature flag +merge_request: 22742 +author: +type: other diff --git a/changelogs/unreleased/remove-experimental-label-from-cluster-views.yml b/changelogs/unreleased/remove-experimental-label-from-cluster-views.yml new file mode 100644 index 00000000000..af9512b27e9 --- /dev/null +++ b/changelogs/unreleased/remove-experimental-label-from-cluster-views.yml @@ -0,0 +1,5 @@ +--- +title: Removes experimental labels from cluster views +merge_request: 22550 +author: +type: other diff --git a/changelogs/unreleased/replace-tooltip-in-markdown-component.yml b/changelogs/unreleased/replace-tooltip-in-markdown-component.yml new file mode 100644 index 00000000000..5047e75c06a --- /dev/null +++ b/changelogs/unreleased/replace-tooltip-in-markdown-component.yml @@ -0,0 +1,5 @@ +--- +title: Replace tooltip in markdown component with gl-tooltip +merge_request: 21989 +author: George Tsiolis +type: other diff --git a/changelogs/unreleased/scheduled-manual-jobs-environment-play-buttons.yml b/changelogs/unreleased/scheduled-manual-jobs-environment-play-buttons.yml new file mode 100644 index 00000000000..c89af78d989 --- /dev/null +++ b/changelogs/unreleased/scheduled-manual-jobs-environment-play-buttons.yml @@ -0,0 +1,5 @@ +--- +title: Add the Play button for delayed jobs in environment page +merge_request: 22106 +author: +type: added diff --git a/changelogs/unreleased/security-kubeclient-ssrf.yml b/changelogs/unreleased/security-kubeclient-ssrf.yml new file mode 100644 index 00000000000..45fc41029fc --- /dev/null +++ b/changelogs/unreleased/security-kubeclient-ssrf.yml @@ -0,0 +1,5 @@ +--- +title: Monkey kubeclient to not follow any redirects. +merge_request: +author: +type: security diff --git a/changelogs/unreleased/sh-fix-issue-52176.yml b/changelogs/unreleased/sh-fix-issue-52176.yml new file mode 100644 index 00000000000..7269e14d910 --- /dev/null +++ b/changelogs/unreleased/sh-fix-issue-52176.yml @@ -0,0 +1,5 @@ +--- +title: Disable replication lag check for Aurora PostgreSQL databases +merge_request: 22786 +author: +type: fixed diff --git a/changelogs/unreleased/sh-optimize-merge-request-project-lookup.yml b/changelogs/unreleased/sh-optimize-merge-request-project-lookup.yml new file mode 100644 index 00000000000..241b89c4633 --- /dev/null +++ b/changelogs/unreleased/sh-optimize-merge-request-project-lookup.yml @@ -0,0 +1,5 @@ +--- +title: Reduce SQL queries needed to load open merge requests +merge_request: 22709 +author: +type: performance diff --git a/changelogs/unreleased/sh-optimize-mr-commit-sha-lookup.yml b/changelogs/unreleased/sh-optimize-mr-commit-sha-lookup.yml new file mode 100644 index 00000000000..bea73f8d329 --- /dev/null +++ b/changelogs/unreleased/sh-optimize-mr-commit-sha-lookup.yml @@ -0,0 +1,5 @@ +--- +title: Optimize merge request refresh by using the database to check commit SHAs +merge_request: 22731 +author: +type: performance diff --git a/changelogs/unreleased/sh-optimize-reload-diffs-service.yml b/changelogs/unreleased/sh-optimize-reload-diffs-service.yml new file mode 100644 index 00000000000..422102560ed --- /dev/null +++ b/changelogs/unreleased/sh-optimize-reload-diffs-service.yml @@ -0,0 +1,5 @@ +--- +title: Significantly cut memory usage and SQL queries when reloading diffs +merge_request: 22725 +author: +type: performance diff --git a/changelogs/unreleased/stateful_deployments.yml b/changelogs/unreleased/stateful_deployments.yml new file mode 100644 index 00000000000..4caa5ad77b8 --- /dev/null +++ b/changelogs/unreleased/stateful_deployments.yml @@ -0,0 +1,5 @@ +--- +title: Add status to Deployment +merge_request: 22380 +author: +type: changed diff --git a/changelogs/unreleased/toggle-sidebar-alignment.yml b/changelogs/unreleased/toggle-sidebar-alignment.yml new file mode 100644 index 00000000000..428fe61da9b --- /dev/null +++ b/changelogs/unreleased/toggle-sidebar-alignment.yml @@ -0,0 +1,5 @@ +--- +title: Align toggle sidebar button across all browsers and OSs +merge_request: 22771 +author: +type: fixed diff --git a/changelogs/unreleased/top_level_clusters_controller.yml b/changelogs/unreleased/top_level_clusters_controller.yml new file mode 100644 index 00000000000..1fe1d048de4 --- /dev/null +++ b/changelogs/unreleased/top_level_clusters_controller.yml @@ -0,0 +1,6 @@ +--- +title: Change to top level controller for clusters so that we can use it for project + clusters (now) and group clusters (later) +merge_request: 22438 +author: +type: other diff --git a/changelogs/unreleased/update_license_management_job.yml b/changelogs/unreleased/update_license_management_job.yml new file mode 100644 index 00000000000..d6e56080e77 --- /dev/null +++ b/changelogs/unreleased/update_license_management_job.yml @@ -0,0 +1,5 @@ +--- +title: "Remove dind from license_management auto-devops job definition" +merge_request: 22732 +author: +type: performance diff --git a/changelogs/unreleased/winh-job-list-dynamic-timer.yml b/changelogs/unreleased/winh-job-list-dynamic-timer.yml new file mode 100644 index 00000000000..333a974d6aa --- /dev/null +++ b/changelogs/unreleased/winh-job-list-dynamic-timer.yml @@ -0,0 +1,5 @@ +--- +title: Add dynamic timer for delayed jobs in job list +merge_request: 22656 +author: +type: changed diff --git a/changelogs/unreleased/zj-bump-gitaly-0-128.yml b/changelogs/unreleased/zj-bump-gitaly-0-128.yml new file mode 100644 index 00000000000..451df4b800e --- /dev/null +++ b/changelogs/unreleased/zj-bump-gitaly-0-128.yml @@ -0,0 +1,5 @@ +--- +title: Bump Gitaly to 0.128.0 +merge_request: +author: +type: added diff --git a/config/application.rb b/config/application.rb index 9074cf02c46..95b0f74a5a3 100644 --- a/config/application.rb +++ b/config/application.rb @@ -144,7 +144,7 @@ module Gitlab config.assets.precompile << "errors.css" # Import gitlab-svgs directly from vendored directory - config.assets.paths << "#{config.root}/node_modules/@gitlab-org/gitlab-svgs/dist" + config.assets.paths << "#{config.root}/node_modules/@gitlab/svgs/dist" config.assets.precompile << "icons.svg" config.assets.precompile << "icons.json" config.assets.precompile << "illustrations/*.svg" diff --git a/config/dependency_decisions.yml b/config/dependency_decisions.yml index 62760ffee3a..488728e26ab 100644 --- a/config/dependency_decisions.yml +++ b/config/dependency_decisions.yml @@ -461,7 +461,7 @@ :versions: [] :when: 2017-09-13 17:31:16.425819400 Z - - :license - - "@gitlab-org/gitlab-svgs" + - "@gitlab/svgs" - MIT - :who: Tim Zallmann :why: Our own library - GitLab License https://gitlab.com/gitlab-org/gitlab-svgs diff --git a/config/environments/development.rb b/config/environments/development.rb index 23790b84e3c..494ddd72556 100644 --- a/config/environments/development.rb +++ b/config/environments/development.rb @@ -45,4 +45,6 @@ Rails.application.configure do # Do not log asset requests config.assets.quiet = true + + config.allow_concurrency = defined?(::Puma) end diff --git a/config/environments/production.rb b/config/environments/production.rb index 9941987929c..71195164e7a 100644 --- a/config/environments/production.rb +++ b/config/environments/production.rb @@ -83,5 +83,5 @@ Rails.application.configure do config.eager_load = true - config.allow_concurrency = false + config.allow_concurrency = defined?(::Puma) end diff --git a/config/gitlab.yml.example b/config/gitlab.yml.example index a4db125f831..09e21b2c6f2 100644 --- a/config/gitlab.yml.example +++ b/config/gitlab.yml.example @@ -207,6 +207,10 @@ production: &base # endpoint: 'http://127.0.0.1:9000' # default: nil # path_style: true # Use 'host/bucket_name/object' instead of 'bucket_name.host/object' + ## Packages (maven repository so far) + packages: + enabled: false + ## GitLab Pages pages: enabled: false @@ -587,7 +591,7 @@ production: &base gitaly: # Path to the directory containing Gitaly client executables. client_path: /home/git/gitaly/bin - # Default Gitaly authentication token. Can be overriden per storage. Can + # Default Gitaly authentication token. Can be overridden per storage. Can # be left blank when Gitaly is running locally on a Unix socket, which # is the normal way to deploy Gitaly. token: diff --git a/config/initializers/8_metrics.rb b/config/initializers/8_metrics.rb index c8d261d415e..468f80939d7 100644 --- a/config/initializers/8_metrics.rb +++ b/config/initializers/8_metrics.rb @@ -98,7 +98,11 @@ end # check: https://github.com/rspec/rspec-mocks#settings-mocks-or-stubs-on-any-instance-of-a-class # # Related issue: https://gitlab.com/gitlab-org/gitlab-ce/issues/33587 -if Gitlab::Metrics.enabled? && !Rails.env.test? +# +# In development mode, we turn off eager loading when we're running +# `rails generate migration` because eager loading short-circuits the +# loading of our custom migration templates. +if Gitlab::Metrics.enabled? && !Rails.env.test? && !(Rails.env.development? && defined?(Rails::Generators)) require 'pathname' require 'influxdb' require 'connection_pool' diff --git a/config/initializers/fill_shards.rb b/config/initializers/fill_shards.rb new file mode 100644 index 00000000000..0f45cf44621 --- /dev/null +++ b/config/initializers/fill_shards.rb @@ -0,0 +1,4 @@ +return unless Shard.connected? +return if Gitlab::Database.read_only? + +Shard.populate! diff --git a/config/initializers/kubeclient.rb b/config/initializers/kubeclient.rb index 7f115268b37..2d9f439fdc0 100644 --- a/config/initializers/kubeclient.rb +++ b/config/initializers/kubeclient.rb @@ -13,4 +13,25 @@ class Kubeclient::Client ns_prefix = build_namespace_prefix(namespace) rest_client["#{ns_prefix}#{entity_name_plural}/#{name}:#{port}/proxy"].url end + + # Monkey patch to set `max_redirects: 0`, so that kubeclient + # does not follow redirects and expose internal services. + # See https://gitlab.com/gitlab-org/gitlab-ce/issues/53158 + def create_rest_client(path = nil) + path ||= @api_endpoint.path + options = { + ssl_ca_file: @ssl_options[:ca_file], + ssl_cert_store: @ssl_options[:cert_store], + verify_ssl: @ssl_options[:verify_ssl], + ssl_client_cert: @ssl_options[:client_cert], + ssl_client_key: @ssl_options[:client_key], + proxy: @http_proxy_uri, + user: @auth_options[:username], + password: @auth_options[:password], + open_timeout: @timeouts[:open], + read_timeout: @timeouts[:read], + max_redirects: 0 + } + RestClient::Resource.new(@api_endpoint.merge(path).to_s, options) + end end diff --git a/config/routes.rb b/config/routes.rb index 37c7f98ec98..d2d91647d0b 100644 --- a/config/routes.rb +++ b/config/routes.rb @@ -84,6 +84,23 @@ Rails.application.routes.draw do draw :instance_statistics end + concern :clusterable do + resources :clusters, only: [:index, :new, :show, :update, :destroy] do + collection do + post :create_user + post :create_gcp + end + + member do + scope :applications do + post '/:application', to: 'clusters/applications#create', as: :install_applications + end + + get :cluster_status, format: :json + end + end + end + draw :api draw :sidekiq draw :help diff --git a/config/routes/project.rb b/config/routes/project.rb index 73c46f72168..387d2363552 100644 --- a/config/routes/project.rb +++ b/config/routes/project.rb @@ -206,20 +206,7 @@ constraints(::Constraints::ProjectUrlConstrainer.new) do end end - resources :clusters, except: [:edit, :create] do - collection do - post :create_gcp - post :create_user - end - - member do - get :status, format: :json - - scope :applications do - post '/:application', to: 'clusters/applications#create', as: :install_applications - end - end - end + concerns :clusterable resources :environments, except: [:destroy] do member do diff --git a/config/sidekiq_queues.yml b/config/sidekiq_queues.yml index 0e723cdeb9c..53e1c8778b6 100644 --- a/config/sidekiq_queues.yml +++ b/config/sidekiq_queues.yml @@ -29,6 +29,7 @@ - [pipeline_creation, 4] - [pipeline_default, 3] - [pipeline_cache, 3] + - [deployment, 3] - [pipeline_hooks, 2] - [gitlab_shell, 2] - [email_receiver, 2] diff --git a/db/fixtures/development/17_cycle_analytics.rb b/db/fixtures/development/17_cycle_analytics.rb index 285436f4324..7a86fe2eb7c 100644 --- a/db/fixtures/development/17_cycle_analytics.rb +++ b/db/fixtures/development/17_cycle_analytics.rb @@ -180,11 +180,8 @@ class Gitlab::Seeder::CycleAnalytics ref: "refs/heads/#{merge_request.source_branch}") pipeline = service.execute(:push, ignore_skip_ci: true, save_on_errors: false) - pipeline.run! - Timecop.travel rand(1..6).hours.from_now - pipeline.succeed! - - PipelineMetricsWorker.new.perform(pipeline.id) + pipeline.builds.map(&:run!) + pipeline.update_status end end @@ -204,7 +201,8 @@ class Gitlab::Seeder::CycleAnalytics job = merge_request.head_pipeline.builds.where.not(environment: nil).last - CreateDeploymentService.new(job).execute + job.success! + pipeline.update_status end end end diff --git a/db/migrate/20180413022611_create_missing_namespace_for_internal_users.rb b/db/migrate/20180413022611_create_missing_namespace_for_internal_users.rb index 8fc558be733..b7b346cb10e 100644 --- a/db/migrate/20180413022611_create_missing_namespace_for_internal_users.rb +++ b/db/migrate/20180413022611_create_missing_namespace_for_internal_users.rb @@ -45,7 +45,7 @@ class CreateMissingNamespaceForInternalUsers < ActiveRecord::Migration connection.exec_query(query).present? end - insert_query = "INSERT INTO namespaces(owner_id, path, name) VALUES(#{user_id}, '#{path}', '#{path}')" + insert_query = "INSERT INTO namespaces(owner_id, path, name, created_at, updated_at) VALUES(#{user_id}, '#{path}', '#{path}', NOW(), NOW())" namespace_id = connection.insert_sql(insert_query) create_route(namespace_id) @@ -57,7 +57,7 @@ class CreateMissingNamespaceForInternalUsers < ActiveRecord::Migration row = connection.exec_query("SELECT id, path FROM namespaces WHERE id=#{namespace_id}").first id, path = row.values_at('id', 'path') - execute("INSERT INTO routes(source_id, source_type, path, name) VALUES(#{id}, 'Namespace', '#{path}', '#{path}')") + execute("INSERT INTO routes(source_id, source_type, path, name, created_at, updated_at) VALUES(#{id}, 'Namespace', '#{path}', '#{path}', NOW(), NOW())") end def set_notification_email(user_id) diff --git a/db/migrate/20180927073410_add_index_to_project_deploy_tokens_deploy_token_id.rb b/db/migrate/20180927073410_add_index_to_project_deploy_tokens_deploy_token_id.rb new file mode 100644 index 00000000000..61d32fe16eb --- /dev/null +++ b/db/migrate/20180927073410_add_index_to_project_deploy_tokens_deploy_token_id.rb @@ -0,0 +1,18 @@ +# frozen_string_literal: true + +class AddIndexToProjectDeployTokensDeployTokenId < ActiveRecord::Migration + include Gitlab::Database::MigrationHelpers + + DOWNTIME = false + + disable_ddl_transaction! + + def up + # MySQL already has index inserted + add_concurrent_index :project_deploy_tokens, :deploy_token_id if Gitlab::Database.postgresql? + end + + def down + remove_concurrent_index(:project_deploy_tokens, :deploy_token_id) if Gitlab::Database.postgresql? + end +end diff --git a/db/migrate/20181015155839_add_finished_at_to_deployments.rb b/db/migrate/20181015155839_add_finished_at_to_deployments.rb new file mode 100644 index 00000000000..1a061bb0f5f --- /dev/null +++ b/db/migrate/20181015155839_add_finished_at_to_deployments.rb @@ -0,0 +1,15 @@ +# frozen_string_literal: true + +class AddFinishedAtToDeployments < ActiveRecord::Migration + include Gitlab::Database::MigrationHelpers + + DOWNTIME = false + + def up + add_column :deployments, :finished_at, :datetime_with_timezone + end + + def down + remove_column :deployments, :finished_at, :datetime_with_timezone + end +end diff --git a/db/migrate/20181016141739_add_status_to_deployments.rb b/db/migrate/20181016141739_add_status_to_deployments.rb new file mode 100644 index 00000000000..321172696b4 --- /dev/null +++ b/db/migrate/20181016141739_add_status_to_deployments.rb @@ -0,0 +1,29 @@ +# frozen_string_literal: true + +class AddStatusToDeployments < ActiveRecord::Migration + include Gitlab::Database::MigrationHelpers + + DEPLOYMENT_STATUS_SUCCESS = 2 # Equivalent to Deployment.state_machine.states['success'].value + + DOWNTIME = false + + disable_ddl_transaction! + + ## + # NOTE: + # Ideally, `status` column should not have default value because it should be leveraged by state machine (i.e. application level). + # However, we have to use the default value for avoiding `NOT NULL` violation during the transition period. + # The default value should be removed in the future release. + def up + add_column_with_default(:deployments, + :status, + :integer, + limit: 2, + default: DEPLOYMENT_STATUS_SUCCESS, + allow_null: false) + end + + def down + remove_column(:deployments, :status) + end +end diff --git a/db/migrate/20181019032400_add_shards_table.rb b/db/migrate/20181019032400_add_shards_table.rb new file mode 100644 index 00000000000..5e0a6960548 --- /dev/null +++ b/db/migrate/20181019032400_add_shards_table.rb @@ -0,0 +1,11 @@ +# frozen_string_literal: true + +class AddShardsTable < ActiveRecord::Migration + DOWNTIME = false + + def change + create_table :shards do |t| + t.string :name, null: false, index: { unique: true } + end + end +end diff --git a/db/migrate/20181019032408_add_repositories_table.rb b/db/migrate/20181019032408_add_repositories_table.rb new file mode 100644 index 00000000000..077f264d3ce --- /dev/null +++ b/db/migrate/20181019032408_add_repositories_table.rb @@ -0,0 +1,15 @@ +# frozen_string_literal: true + +class AddRepositoriesTable < ActiveRecord::Migration + DOWNTIME = false + + def change + create_table :repositories, id: :bigserial do |t| + t.references :shard, null: false, index: true, foreign_key: { on_delete: :restrict } + t.string :disk_path, null: false, index: { unique: true } + end + + add_column :projects, :pool_repository_id, :bigint + add_index :projects, :pool_repository_id, where: 'pool_repository_id IS NOT NULL' + end +end diff --git a/db/migrate/20181019105553_add_projects_pool_repository_id_foreign_key.rb b/db/migrate/20181019105553_add_projects_pool_repository_id_foreign_key.rb new file mode 100644 index 00000000000..059988de38a --- /dev/null +++ b/db/migrate/20181019105553_add_projects_pool_repository_id_foreign_key.rb @@ -0,0 +1,22 @@ +# frozen_string_literal: true + +class AddProjectsPoolRepositoryIdForeignKey < ActiveRecord::Migration + include Gitlab::Database::MigrationHelpers + + DOWNTIME = false + + disable_ddl_transaction! + + def up + add_concurrent_foreign_key( + :projects, + :repositories, + column: :pool_repository_id, + on_delete: :nullify + ) + end + + def down + remove_foreign_key(:projects, column: :pool_repository_id) + end +end diff --git a/db/migrate/20181022135539_add_index_on_status_to_deployments.rb b/db/migrate/20181022135539_add_index_on_status_to_deployments.rb new file mode 100644 index 00000000000..2eed20aa855 --- /dev/null +++ b/db/migrate/20181022135539_add_index_on_status_to_deployments.rb @@ -0,0 +1,19 @@ +# frozen_string_literal: true + +class AddIndexOnStatusToDeployments < ActiveRecord::Migration + include Gitlab::Database::MigrationHelpers + + DOWNTIME = false + + disable_ddl_transaction! + + def up + add_concurrent_index :deployments, [:project_id, :status] + add_concurrent_index :deployments, [:environment_id, :status] + end + + def down + remove_concurrent_index :deployments, [:project_id, :status] + remove_concurrent_index :deployments, [:environment_id, :status] + end +end diff --git a/db/migrate/20181023104858_add_archive_builds_duration_to_application_settings.rb b/db/migrate/20181023104858_add_archive_builds_duration_to_application_settings.rb new file mode 100644 index 00000000000..744748b3fad --- /dev/null +++ b/db/migrate/20181023104858_add_archive_builds_duration_to_application_settings.rb @@ -0,0 +1,11 @@ +# frozen_string_literal: true + +class AddArchiveBuildsDurationToApplicationSettings < ActiveRecord::Migration + include Gitlab::Database::MigrationHelpers + + DOWNTIME = false + + def change + add_column(:application_settings, :archive_builds_in_seconds, :integer, allow_null: true) + end +end diff --git a/db/migrate/20181023144439_add_partial_index_for_legacy_successful_deployments.rb b/db/migrate/20181023144439_add_partial_index_for_legacy_successful_deployments.rb new file mode 100644 index 00000000000..5896102af1c --- /dev/null +++ b/db/migrate/20181023144439_add_partial_index_for_legacy_successful_deployments.rb @@ -0,0 +1,18 @@ +# frozen_string_literal: true + +class AddPartialIndexForLegacySuccessfulDeployments < ActiveRecord::Migration + include Gitlab::Database::MigrationHelpers + + DOWNTIME = false + INDEX_NAME = 'partial_index_deployments_for_legacy_successful_deployments'.freeze + + disable_ddl_transaction! + + def up + add_concurrent_index(:deployments, :id, where: "finished_at IS NULL AND status = 2", name: INDEX_NAME) + end + + def down + remove_concurrent_index_by_name(:deployments, INDEX_NAME) + end +end diff --git a/db/migrate/20181101144347_add_index_for_stuck_mr_query.rb b/db/migrate/20181101144347_add_index_for_stuck_mr_query.rb new file mode 100644 index 00000000000..5d3ace54e5c --- /dev/null +++ b/db/migrate/20181101144347_add_index_for_stuck_mr_query.rb @@ -0,0 +1,16 @@ +# frozen_string_literal: true +class AddIndexForStuckMrQuery < ActiveRecord::Migration + include Gitlab::Database::MigrationHelpers + + DOWNTIME = false + + disable_ddl_transaction! + + def up + add_concurrent_index :merge_requests, [:id, :merge_jid], where: "merge_jid IS NOT NULL and state = 'locked'" + end + + def down + remove_concurrent_index :merge_requests, [:id, :merge_jid], where: "merge_jid IS NOT NULL and state = 'locked'" + end +end diff --git a/db/post_migrate/20170717111152_cleanup_move_system_upload_folder_symlink.rb b/db/post_migrate/20170717111152_cleanup_move_system_upload_folder_symlink.rb index c48f1c938d0..3ae4406ff96 100644 --- a/db/post_migrate/20170717111152_cleanup_move_system_upload_folder_symlink.rb +++ b/db/post_migrate/20170717111152_cleanup_move_system_upload_folder_symlink.rb @@ -13,7 +13,7 @@ class CleanupMoveSystemUploadFolderSymlink < ActiveRecord::Migration say "Removing #{old_directory} -> #{new_directory} symlink" FileUtils.rm(old_directory) else - say "Symlink #{old_directory} non existant, nothing to do." + say "Symlink #{old_directory} non existent, nothing to do." end end diff --git a/db/post_migrate/20181022173835_enqueue_populate_cluster_kubernetes_namespace.rb b/db/post_migrate/20181022173835_enqueue_populate_cluster_kubernetes_namespace.rb new file mode 100644 index 00000000000..f80a2aa6eac --- /dev/null +++ b/db/post_migrate/20181022173835_enqueue_populate_cluster_kubernetes_namespace.rb @@ -0,0 +1,18 @@ +# frozen_string_literal: true + +class EnqueuePopulateClusterKubernetesNamespace < ActiveRecord::Migration + include Gitlab::Database::MigrationHelpers + + DOWNTIME = false + MIGRATION = 'PopulateClusterKubernetesNamespaceTable'.freeze + + disable_ddl_transaction! + + def up + BackgroundMigrationWorker.perform_async(MIGRATION) + end + + def down + Clusters::KubernetesNamespace.delete_all + end +end diff --git a/db/post_migrate/20181030135124_fill_empty_finished_at_in_deployments.rb b/db/post_migrate/20181030135124_fill_empty_finished_at_in_deployments.rb new file mode 100644 index 00000000000..32b271c472a --- /dev/null +++ b/db/post_migrate/20181030135124_fill_empty_finished_at_in_deployments.rb @@ -0,0 +1,27 @@ +# frozen_string_literal: true + +class FillEmptyFinishedAtInDeployments < ActiveRecord::Migration + include Gitlab::Database::MigrationHelpers + + DOWNTIME = false + DEPLOYMENT_STATUS_SUCCESS = 2 # Equivalent to Deployment.statuses[:success] + + class Deployments < ActiveRecord::Base + self.table_name = 'deployments' + + include EachBatch + end + + def up + FillEmptyFinishedAtInDeployments::Deployments + .where('finished_at IS NULL') + .where('status = ?', DEPLOYMENT_STATUS_SUCCESS) + .each_batch(of: 10_000) do |relation| + relation.update_all('finished_at=created_at') + end + end + + def down + # no-op + end +end diff --git a/db/schema.rb b/db/schema.rb index 6b0bb33f969..fe30c5375f4 100644 --- a/db/schema.rb +++ b/db/schema.rb @@ -11,7 +11,7 @@ # # It's strongly recommended that you check this file into your version control system. -ActiveRecord::Schema.define(version: 20181017001059) do +ActiveRecord::Schema.define(version: 20181101144347) do # These are extensions that must be enabled in order to support this database enable_extension "plpgsql" @@ -165,6 +165,7 @@ ActiveRecord::Schema.define(version: 20181017001059) do t.integer "usage_stats_set_by_user_id" t.integer "receive_max_input_size" t.integer "diff_max_patch_bytes", default: 102400, null: false + t.integer "archive_builds_in_seconds" end create_table "audit_events", force: :cascade do |t| @@ -824,13 +825,18 @@ ActiveRecord::Schema.define(version: 20181017001059) do t.datetime "created_at" t.datetime "updated_at" t.string "on_stop" + t.integer "status", limit: 2, default: 2, null: false + t.datetime_with_timezone "finished_at" end add_index "deployments", ["created_at"], name: "index_deployments_on_created_at", using: :btree add_index "deployments", ["deployable_type", "deployable_id"], name: "index_deployments_on_deployable_type_and_deployable_id", using: :btree add_index "deployments", ["environment_id", "id"], name: "index_deployments_on_environment_id_and_id", using: :btree add_index "deployments", ["environment_id", "iid", "project_id"], name: "index_deployments_on_environment_id_and_iid_and_project_id", using: :btree + add_index "deployments", ["environment_id", "status"], name: "index_deployments_on_environment_id_and_status", using: :btree + add_index "deployments", ["id"], name: "partial_index_deployments_for_legacy_successful_deployments", where: "((finished_at IS NULL) AND (status = 2))", using: :btree add_index "deployments", ["project_id", "iid"], name: "index_deployments_on_project_id_and_iid", unique: true, using: :btree + add_index "deployments", ["project_id", "status"], name: "index_deployments_on_project_id_and_status", using: :btree create_table "emails", force: :cascade do |t| t.integer "user_id", null: false @@ -1323,6 +1329,7 @@ ActiveRecord::Schema.define(version: 20181017001059) do add_index "merge_requests", ["created_at"], name: "index_merge_requests_on_created_at", using: :btree add_index "merge_requests", ["description"], name: "index_merge_requests_on_description_trigram", using: :gin, opclasses: {"description"=>"gin_trgm_ops"} add_index "merge_requests", ["head_pipeline_id"], name: "index_merge_requests_on_head_pipeline_id", using: :btree + add_index "merge_requests", ["id", "merge_jid"], name: "index_merge_requests_on_id_and_merge_jid", where: "((merge_jid IS NOT NULL) AND ((state)::text = 'locked'::text))", using: :btree add_index "merge_requests", ["latest_merge_request_diff_id"], name: "index_merge_requests_on_latest_merge_request_diff_id", using: :btree add_index "merge_requests", ["merge_user_id"], name: "index_merge_requests_on_merge_user_id", where: "(merge_user_id IS NOT NULL)", using: :btree add_index "merge_requests", ["milestone_id"], name: "index_merge_requests_on_milestone_id", using: :btree @@ -1616,6 +1623,7 @@ ActiveRecord::Schema.define(version: 20181017001059) do t.datetime_with_timezone "created_at", null: false end + add_index "project_deploy_tokens", ["deploy_token_id"], name: "index_project_deploy_tokens_on_deploy_token_id", using: :btree add_index "project_deploy_tokens", ["project_id", "deploy_token_id"], name: "index_project_deploy_tokens_on_project_id_and_deploy_token_id", unique: true, using: :btree create_table "project_features", force: :cascade do |t| @@ -1731,6 +1739,7 @@ ActiveRecord::Schema.define(version: 20181017001059) do t.integer "jobs_cache_index" t.boolean "pages_https_only", default: true t.boolean "remote_mirror_available_overridden" + t.integer "pool_repository_id", limit: 8 end add_index "projects", ["ci_id"], name: "index_projects_on_ci_id", using: :btree @@ -1747,6 +1756,7 @@ ActiveRecord::Schema.define(version: 20181017001059) do add_index "projects", ["path"], name: "index_projects_on_path", using: :btree add_index "projects", ["path"], name: "index_projects_on_path_trigram", using: :gin, opclasses: {"path"=>"gin_trgm_ops"} add_index "projects", ["pending_delete"], name: "index_projects_on_pending_delete", using: :btree + add_index "projects", ["pool_repository_id"], name: "index_projects_on_pool_repository_id", where: "(pool_repository_id IS NOT NULL)", using: :btree add_index "projects", ["repository_storage", "created_at"], name: "idx_project_repository_check_partial", where: "(last_repository_check_at IS NULL)", using: :btree add_index "projects", ["repository_storage"], name: "index_projects_on_repository_storage", using: :btree add_index "projects", ["runners_token"], name: "index_projects_on_runners_token", using: :btree @@ -1879,6 +1889,14 @@ ActiveRecord::Schema.define(version: 20181017001059) do add_index "remote_mirrors", ["last_successful_update_at"], name: "index_remote_mirrors_on_last_successful_update_at", using: :btree add_index "remote_mirrors", ["project_id"], name: "index_remote_mirrors_on_project_id", using: :btree + create_table "repositories", id: :bigserial, force: :cascade do |t| + t.integer "shard_id", null: false + t.string "disk_path", null: false + end + + add_index "repositories", ["disk_path"], name: "index_repositories_on_disk_path", unique: true, using: :btree + add_index "repositories", ["shard_id"], name: "index_repositories_on_shard_id", using: :btree + create_table "repository_languages", id: false, force: :cascade do |t| t.integer "project_id", null: false t.integer "programming_language_id", null: false @@ -1959,6 +1977,12 @@ ActiveRecord::Schema.define(version: 20181017001059) do add_index "services", ["project_id"], name: "index_services_on_project_id", using: :btree add_index "services", ["template"], name: "index_services_on_template", using: :btree + create_table "shards", force: :cascade do |t| + t.string "name", null: false + end + + add_index "shards", ["name"], name: "index_shards_on_name", unique: true, using: :btree + create_table "site_statistics", force: :cascade do |t| t.integer "repositories_count", default: 0, null: false end @@ -2481,6 +2505,7 @@ ActiveRecord::Schema.define(version: 20181017001059) do add_foreign_key "project_import_data", "projects", name: "fk_ffb9ee3a10", on_delete: :cascade add_foreign_key "project_mirror_data", "projects", on_delete: :cascade add_foreign_key "project_statistics", "projects", on_delete: :cascade + add_foreign_key "projects", "repositories", column: "pool_repository_id", name: "fk_6e5c14658a", on_delete: :nullify add_foreign_key "prometheus_metrics", "projects", on_delete: :cascade add_foreign_key "protected_branch_merge_access_levels", "protected_branches", name: "fk_8a3072ccb3", on_delete: :cascade add_foreign_key "protected_branch_push_access_levels", "protected_branches", name: "fk_9ffc86a3d9", on_delete: :cascade @@ -2492,6 +2517,7 @@ ActiveRecord::Schema.define(version: 20181017001059) do add_foreign_key "push_event_payloads", "events", name: "fk_36c74129da", on_delete: :cascade add_foreign_key "releases", "projects", name: "fk_47fe2a0596", on_delete: :cascade add_foreign_key "remote_mirrors", "projects", on_delete: :cascade + add_foreign_key "repositories", "shards", on_delete: :restrict add_foreign_key "repository_languages", "projects", on_delete: :cascade add_foreign_key "resource_label_events", "issues", on_delete: :cascade add_foreign_key "resource_label_events", "labels", on_delete: :nullify diff --git a/doc/administration/raketasks/maintenance.md b/doc/administration/raketasks/maintenance.md index 29af07d12dc..0d863594fc7 100644 --- a/doc/administration/raketasks/maintenance.md +++ b/doc/administration/raketasks/maintenance.md @@ -53,6 +53,7 @@ Git: /usr/bin/git Runs the following rake tasks: - `gitlab:gitlab_shell:check` +- `gitlab:gitaly:check` - `gitlab:sidekiq:check` - `gitlab:app:check` @@ -252,7 +253,7 @@ clear it. To clear all exclusive leases: -DANGER: **DANGER**: +DANGER: **DANGER**: Don't run it while GitLab or Sidekiq is running ```bash diff --git a/doc/api/issues.md b/doc/api/issues.md index 57e861bc62e..0dc9d706120 100644 --- a/doc/api/issues.md +++ b/doc/api/issues.md @@ -37,11 +37,11 @@ GET /issues?my_reaction_emoji=star | ------------------- | ---------------- | ---------- | --------------------------------------------------------------------------------------------------------------------------------------------------- | | `state` | string | no | Return all issues or just those that are `opened` or `closed` | | `labels` | string | no | Comma-separated list of label names, issues must have all labels to be returned. `No+Label` lists all issues with no labels | -| `milestone` | string | no | The milestone title. `No+Milestone` lists all issues with no milestone. `Any+Milestone` lists all issues that have an assigned milestone | +| `milestone` | string | no | The milestone title. `None` lists all issues with no milestone. `Any` lists all issues that have an assigned milestone. | | `scope` | string | no | Return issues 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.<br> _([Introduced][ce-13004] in GitLab 9.5. [Changed to snake_case][ce-18935] in GitLab 11.0)_ | | `author_id` | integer | no | Return issues created by the given user `id`. Combine with `scope=all` or `scope=assigned_to_me`. _([Introduced][ce-13004] in GitLab 9.5)_ | | `assignee_id` | integer | no | Return issues assigned to the given user `id`. `None` returns unassigned issues. `Any` returns issues with an assignee. _([Introduced][ce-13004] in GitLab 9.5)_ | -| `my_reaction_emoji` | string | no | Return issues reacted by the authenticated user by the given `emoji` _([Introduced][ce-14016] in GitLab 10.0)_ | +| `my_reaction_emoji` | string | no | Return issues 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)_ | | `iids[]` | Array[integer] | no | Return only the issues having the given `iid` | | `order_by` | string | no | Return issues ordered by `created_at` or `updated_at` fields. Default is `created_at` | | `sort` | string | no | Return issues sorted in `asc` or `desc` order. Default is `desc` | @@ -151,11 +151,11 @@ GET /groups/:id/issues?my_reaction_emoji=star | `state` | string | no | Return all issues or just those that are `opened` or `closed` | | `labels` | string | no | Comma-separated list of label names, issues must have all labels to be returned. `No+Label` lists all issues with no labels | | `iids[]` | Array[integer] | no | Return only the issues having the given `iid` | -| `milestone` | string | no | The milestone title. `No+Milestone` lists all issues with no milestone | +| `milestone` | string | no | The milestone title. `None` lists all issues with no milestone. `Any` lists all issues that have an assigned milestone. | | `scope` | string | no | Return issues 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-13004] in GitLab 9.5. [Changed to snake_case][ce-18935] in GitLab 11.0)_ | | `author_id` | integer | no | Return issues created by the given user `id` _([Introduced][ce-13004] in GitLab 9.5)_ | | `assignee_id` | integer | no | Return issues assigned to the given user `id`. `None` returns unassigned issues. `Any` returns issues with an assignee. _([Introduced][ce-13004] in GitLab 9.5)_ | -| `my_reaction_emoji` | string | no | Return issues reacted by the authenticated user by the given `emoji` _([Introduced][ce-14016] in GitLab 10.0)_ | +| `my_reaction_emoji` | string | no | Return issues 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)_ | | `order_by` | string | no | Return issues ordered by `created_at` or `updated_at` fields. Default is `created_at` | | `sort` | string | no | Return issues sorted in `asc` or `desc` order. Default is `desc` | | `search` | string | no | Search group issues against their `title` and `description` | @@ -265,11 +265,11 @@ GET /projects/:id/issues?my_reaction_emoji=star | `iids[]` | Array[integer] | no | Return only the milestone having the given `iid` | | `state` | string | no | Return all issues or just those that are `opened` or `closed` | | `labels` | string | no | Comma-separated list of label names, issues must have all labels to be returned. `No+Label` lists all issues with no labels | -| `milestone` | string | no | The milestone title. `No+Milestone` lists all issues with no milestone | +| `milestone` | string | no | The milestone title. `None` lists all issues with no milestone. `Any` lists all issues that have an assigned milestone. | | `scope` | string | no | Return issues 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-13004] in GitLab 9.5. [Changed to snake_case][ce-18935] in GitLab 11.0)_ | | `author_id` | integer | no | Return issues created by the given user `id` _([Introduced][ce-13004] in GitLab 9.5)_ | | `assignee_id` | integer | no | Return issues assigned to the given user `id`. `None` returns unassigned issues. `Any` returns issues with an assignee. _([Introduced][ce-13004] in GitLab 9.5)_ | -| `my_reaction_emoji` | string | no | Return issues reacted by the authenticated user by the given `emoji` _([Introduced][ce-14016] in GitLab 10.0)_ | +| `my_reaction_emoji` | string | no | Return issues 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)_ | | `order_by` | string | no | Return issues ordered by `created_at` or `updated_at` fields. Default is `created_at` | | `sort` | string | no | Return issues sorted in `asc` or `desc` order. Default is `desc` | | `search` | string | no | Search project issues against their `title` and `description` | @@ -1113,6 +1113,93 @@ Example response: } ``` +## List merge requests related to issue + +Get all the merge requests that are related to the issue. + +``` +GET /projects/:id/issues/:issue_id/related_merge_requests +``` + +| Attribute | Type | Required | Description | +|-------------|---------|----------|--------------------------------------| +| `id` | integer/string | yes | The ID or [URL-encoded path of the project](README.md#namespaced-path-encoding) owned by the authenticated user | +| `issue_iid` | integer | yes | The internal ID of a project's issue | + +```sh +curl --request GET --header "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" https://gitlab.example.com/api/v4/projects/1/issues/11/related_merge_requests +``` + +Example response: + +```json +[ + { + "id": 29, + "iid": 11, + "project_id": 1, + "title": "Provident eius eos blanditiis consequatur neque odit.", + "description": "Ut consequatur ipsa aspernatur quisquam voluptatum fugit. Qui harum corporis quo fuga ut incidunt veritatis. Autem necessitatibus et harum occaecati nihil ea.\r\n\r\ntwitter/flight#8", + "state": "opened", + "created_at": "2018-09-18T14:36:15.510Z", + "updated_at": "2018-09-19T07:45:13.089Z", + "target_branch": "v2.x", + "source_branch": "so_long_jquery", + "upvotes": 0, + "downvotes": 0, + "author": { + "id": 14, + "name": "Verna Hills", + "username": "lawanda_reinger", + "state": "active", + "avatar_url": "https://www.gravatar.com/avatar/de68a91aeab1cff563795fb98a0c2cc0?s=80&d=identicon", + "web_url": "https://gitlab.example.com/lawanda_reinger" + }, + "assignee": { + "id": 19, + "name": "Jody Baumbach", + "username": "felipa.kuvalis", + "state": "active", + "avatar_url": "https://www.gravatar.com/avatar/6541fc75fc4e87e203529bd275fafd07?s=80&d=identicon", + "web_url": "https://gitlab.example.com/felipa.kuvalis" + }, + "source_project_id": 1, + "target_project_id": 1, + "labels": [], + "work_in_progress": false, + "milestone": { + "id": 27, + "iid": 2, + "project_id": 1, + "title": "v1.0", + "description": "Et tenetur voluptatem minima doloribus vero dignissimos vitae.", + "state": "active", + "created_at": "2018-09-18T14:35:44.353Z", + "updated_at": "2018-09-18T14:35:44.353Z", + "due_date": null, + "start_date": null, + "web_url": "https://gitlab.example.com/twitter/flight/milestones/2" + }, + "merge_when_pipeline_succeeds": false, + "merge_status": "cannot_be_merged", + "sha": "3b7b528e9353295c1c125dad281ac5b5deae5f12", + "merge_commit_sha": null, + "user_notes_count": 9, + "discussion_locked": null, + "should_remove_source_branch": null, + "force_remove_source_branch": false, + "web_url": "https://gitlab.example.com/twitter/flight/merge_requests/4", + "time_stats": { + "time_estimate": 0, + "total_time_spent": 0, + "human_time_estimate": null, + "human_total_time_spent": null + }, + "squash": false + } +] +``` + ## List merge requests that will close issue on merge Get all the merge requests that will close issue when merged. diff --git a/doc/api/merge_requests.md b/doc/api/merge_requests.md index 0291b7e00c2..f3cfe0ad218 100644 --- a/doc/api/merge_requests.md +++ b/doc/api/merge_requests.md @@ -30,10 +30,10 @@ Parameters: | Attribute | Type | Required | Description | | ------------------- | -------- | -------- | ---------------------------------------------------------------------------------------------------------------------- | -| `state` | string | no | Return all merge requests or just those that are `opened`, `closed`, `locked`, or `merged` | +| `state` | string | no | Return all merge requests or just those that are `opened`, `closed`, `locked`, or `merged` | | `order_by` | string | no | Return requests ordered by `created_at` or `updated_at` fields. Default is `created_at` | | `sort` | string | no | Return requests sorted in `asc` or `desc` order. Default is `desc` | -| `milestone` | string | no | Return merge requests for a specific milestone | +| `milestone` | string | no | Return merge requests for a specific milestone. `None` returns merge requests with no milestone. `Any` returns merge requests that have an assigned milestone. | | `view` | string | no | If `simple`, returns the `iid`, URL, title, description, and basic state of merge request | | `labels` | string | no | Return merge requests matching a comma separated list of labels | | `created_after` | datetime | no | Return merge requests created on or after the given time | @@ -43,11 +43,11 @@ 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. | -| `my_reaction_emoji` | string | no | Return merge requests reacted by the authenticated user by the given `emoji` _([Introduced][ce-14016] in GitLab 10.0)_ | +| `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 | | `search` | string | no | Search merge requests against their `title` and `description` | -| `wip` | string | no | Filter merge requests against their `wip` status. `yes` to return *only* WIP merge requests, `no` to return *non* WIP merge requests | +| `wip` | string | no | Filter merge requests against their `wip` status. `yes` to return *only* WIP merge requests, `no` to return *non* WIP merge requests | ```json [ @@ -154,10 +154,10 @@ Parameters: | ------------------- | -------------- | -------- | ------------------------------------------------------------------------------------------------------------------------------ | | `id` | integer | yes | The ID of a project | | `iids[]` | Array[integer] | no | Return the request having the given `iid` | -| `state` | string | no | Return all merge requests or just those that are `opened`, `closed`, `locked`, or `merged` | +| `state` | string | no | Return all merge requests or just those that are `opened`, `closed`, `locked`, or `merged` | | `order_by` | string | no | Return requests ordered by `created_at` or `updated_at` fields. Default is `created_at` | | `sort` | string | no | Return requests sorted in `asc` or `desc` order. Default is `desc` | -| `milestone` | string | no | Return merge requests for a specific milestone | +| `milestone` | string | no | Return merge requests for a specific milestone. `None` returns merge requests with no milestone. `Any` returns merge requests that have an assigned milestone. | | `view` | string | no | If `simple`, returns the `iid`, URL, title, description, and basic state of merge request | | `labels` | string | no | Return merge requests matching a comma separated list of labels | | `created_after` | datetime | no | Return merge requests created on or after the given time | @@ -167,9 +167,9 @@ 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)_ | -| `my_reaction_emoji` | string | no | Return merge requests reacted by the authenticated user by the given `emoji` _([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 | +| `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 | | `search` | string | no | Search merge requests against their `title` and `description` | ```json @@ -266,11 +266,11 @@ Parameters: | Attribute | Type | Required | Description | | ------------------- | -------------- | -------- | ------------------------------------------------------------------------------------------------------------------------------ | -| `id` | integer | yes | The ID of a group | -| `state` | string | no | Return all merge requests or just those that are `opened`, `closed`, `locked`, or `merged` | -| `order_by` | string | no | Return merge requests ordered by `created_at` or `updated_at` fields. Default is `created_at` | -| `sort` | string | no | Return merge requests sorted in `asc` or `desc` order. Default is `desc` | -| `milestone` | string | no | Return merge requests for a specific milestone | +| `id` | integer | yes | The ID of a group | +| `state` | string | no | Return all merge requests or just those that are `opened`, `closed`, `locked`, or `merged` | +| `order_by` | string | no | Return merge requests ordered by `created_at` or `updated_at` fields. Default is `created_at` | +| `sort` | string | no | Return merge requests sorted in `asc` or `desc` order. Default is `desc` | +| `milestone` | string | no | Return merge requests for a specific milestone. `None` returns merge requests with no milestone. `Any` returns merge requests that have an assigned milestone. | | `view` | string | no | If `simple`, returns the `iid`, URL, title, description, and basic state of merge request | | `labels` | string | no | Return merge requests matching a comma separated list of labels | | `created_after` | datetime | no | Return merge requests created on or after the given time | @@ -280,9 +280,9 @@ 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)_ | -| `my_reaction_emoji` | string | no | Return merge requests reacted by the authenticated user by the given `emoji` _([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 | +| `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 | | `search` | string | no | Search merge requests against their `title` and `description` | ```json diff --git a/doc/ci/examples/artifactory_and_gitlab/index.md b/doc/ci/examples/artifactory_and_gitlab/index.md index 9657f52159e..6aa0edd87b4 100644 --- a/doc/ci/examples/artifactory_and_gitlab/index.md +++ b/doc/ci/examples/artifactory_and_gitlab/index.md @@ -16,8 +16,8 @@ to build a [Maven](https://maven.apache.org/) project, deploy it to [Artifactory You'll create two different projects: -- `simple-maven-dep`: the app built and deployed to Artifactory (available at https://gitlab.com/gitlab-examples/maven/simple-maven-dep) -- `simple-maven-app`: the app using the previous one as a dependency (available at https://gitlab.com/gitlab-examples/maven/simple-maven-app) +- `simple-maven-dep`: the app built and deployed to Artifactory (available at https://gitlab.com/gitlab-examples/maven/simple-maven-dep ) +- `simple-maven-app`: the app using the previous one as a dependency (available at https://gitlab.com/gitlab-examples/maven/simple-maven-app ) We assume that you already have a GitLab account on [GitLab.com](https://gitlab.com/), and that you know the basic usage of Git and [GitLab CI/CD](https://about.gitlab.com/features/gitlab-ci-cd/). We also assume that an Artifactory instance is available and reachable from the internet, and that you have valid credentials to deploy on it. diff --git a/doc/ci/services/mysql.md b/doc/ci/services/mysql.md index 338368dbbc9..b76f9618fc9 100644 --- a/doc/ci/services/mysql.md +++ b/doc/ci/services/mysql.md @@ -31,7 +31,7 @@ Database: el_duderino ``` If you are wondering why we used `mysql` for the `Host`, read more at -[How is service linked to the job](../docker/using_docker_images.md#how-is-service-linked-to-the-job). +[How services are linked to the job](../docker/using_docker_images.md#how-services-are-linked-to-the-job). You can also use any other docker image available on [Docker Hub][hub-mysql]. For example, to use MySQL 5.5 the service becomes `mysql:5.5`. diff --git a/doc/development/api_graphql_styleguide.md b/doc/development/api_graphql_styleguide.md index 6c6e198a7c3..95722c027ba 100644 --- a/doc/development/api_graphql_styleguide.md +++ b/doc/development/api_graphql_styleguide.md @@ -379,7 +379,7 @@ let(:mutation) do ) end -it 'returns a successfull response' do +it 'returns a successful response' do post_graphql_mutation(mutation, current_user: user) expect(response).to have_gitlab_http_status(:success) diff --git a/doc/development/code_review.md b/doc/development/code_review.md index 3fe79943fdc..96f3861f8d7 100644 --- a/doc/development/code_review.md +++ b/doc/development/code_review.md @@ -23,6 +23,9 @@ one of the [Merge request coaches][team]. Depending on the areas your merge request touches, it must be **approved** by one or more [maintainers](https://about.gitlab.com/handbook/engineering/#maintainer): +For approvals, we use the approval functionality found in the merge request +widget. Reviewers can add their approval by [approving additionally](https://docs.gitlab.com/ee/user/project/merge_requests/merge_request_approvals.html#adding-or-removing-an-approval). + 1. If your merge request includes backend changes [^1], it must be **approved by a [backend maintainer](https://about.gitlab.com/handbook/engineering/projects/#gitlab-ce_maintainers_backend)**. 1. If your merge request includes frontend changes [^1], it must be @@ -97,6 +100,9 @@ If a developer who happens to also be a maintainer was involved in a merge reque as a domain expert and/or reviewer, it is recommended that they are not also picked as the maintainer to ultimately approve and merge it. +Maintainers should check before merging if the merge request is approved by the +required approvers. + ## Best practices ### Everyone diff --git a/doc/development/contributing/merge_request_workflow.md b/doc/development/contributing/merge_request_workflow.md index 1764e2d8b21..5b32b5cd46f 100644 --- a/doc/development/contributing/merge_request_workflow.md +++ b/doc/development/contributing/merge_request_workflow.md @@ -171,6 +171,7 @@ the feature you contribute through all of these steps. 1. Added to [the website](https://gitlab.com/gitlab-com/www-gitlab-com/), if relevant 1. Community questions answered 1. Answers to questions radiated (in docs/wiki/support etc.) +1. [Black-box tests/end-to-end tests](../testing_guide/testing_levels.md#black-box-tests-or-end-to-end-tests) added if required. Please contact [the quality team](https://about.gitlab.com/handbook/engineering/quality/#teams) with any questions If you add a dependency in GitLab (such as an operating system package) please consider updating the following and note the applicability of each in your @@ -185,7 +186,7 @@ merge request: 1. Omnibus package creator https://gitlab.com/gitlab-org/omnibus-gitlab [definition-of-done]: http://guide.agilealliance.org/guide/definition-of-done.html -[testing]: ../testing_guide/index.md +[testing]: ../testing_guide/index.md --- diff --git a/doc/development/fe_guide/icons.md b/doc/development/fe_guide/icons.md index 3d8da6accc1..533e2001300 100644 --- a/doc/development/fe_guide/icons.md +++ b/doc/development/fe_guide/icons.md @@ -3,7 +3,7 @@ We manage our own Icon and Illustration library in the [gitlab-svgs][gitlab-svgs] repository. This repository is published on [npm][npm] and managed as a dependency via yarn. You can browse all available Icons and Illustrations [here][svg-preview]. -To upgrade to a new version run `yarn upgrade @gitlab-org/gitlab-svgs`. +To upgrade to a new version run `yarn upgrade @gitlab/svgs`. ## Icons @@ -111,6 +111,6 @@ export default { </template> ``` -[npm]: https://www.npmjs.com/package/@gitlab-org/gitlab-svgs +[npm]: https://www.npmjs.com/package/@gitlab/svgs [gitlab-svgs]: https://gitlab.com/gitlab-org/gitlab-svgs [svg-preview]: https://gitlab-org.gitlab.io/gitlab-svgs diff --git a/doc/development/ux_guide/tips.md b/doc/development/ux_guide/tips.md index 8348de4f8a2..ceb9ed56ac4 100644 --- a/doc/development/ux_guide/tips.md +++ b/doc/development/ux_guide/tips.md @@ -1,20 +1,16 @@ # Tips -## Contents -* [SVGs](#svgs) - ---- - ## SVGs When exporting SVGs, be sure to follow the following guidelines: 1. Convert all strokes to outlines. -2. Use pathfinder tools to combine overlapping paths and create compound paths. -3. SVGs that are limited to one color should be exported without a fill color so the color can be set using CSS. -4. Ensure that exported SVGs have been run through an [SVG cleaner](https://github.com/RazrFalcon/SVGCleaner) to remove unused elements and attributes. +1. Use pathfinder tools to combine overlapping paths and create compound paths. +1. SVGs that are limited to one color should be exported without a fill color so the color can be set using CSS. +1. Ensure that exported SVGs have been run through an [SVG cleaner](https://github.com/RazrFalcon/SVGCleaner) to remove unused elements and attributes. + +You can open your SVG in a text editor to ensure that it is clean. -You can open your svg in a text editor to ensure that it is clean. Incorrect files will look like this: ```xml @@ -35,10 +31,10 @@ Incorrect files will look like this: </svg> ``` -Correct file will look like this: +Correct files will look like this: ```xml <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 16 17" enable-background="new 0 0 16 17"><path d="m15.1 1h-2.1v-1h-2v1h-6v-1h-2v1h-2.1c-.5 0-.9.5-.9 1v14c0 .6.4 1 .9 1h14.2c.5 0 .9-.4.9-1v-14c0-.5-.4-1-.9-1m-1.1 14h-12v-9h12v9m0-11h-12v-1h12v1"/><path d="m5.4 11.6l1.5 1.2c.4.3 1.1.3 1.4-.1l2.5-3c.3-.4.3-1.1-.1-1.4-.5-.4-1.1-.3-1.5.1l-1.8 2.2-.8-.6c-.4-.3-1.1-.3-1.4.2-.3.4-.3 1 .2 1.4"/></svg> ``` -> TODO: Checkout [https://github.com/svg/svgo](https://github.com/svg/svgo) +> TODO: Checkout <https://github.com/svg/svgo>. diff --git a/doc/install/digitaloceandocker.md b/doc/install/digitaloceandocker.md index 676392eacf2..d67695d75b4 100644 --- a/doc/install/digitaloceandocker.md +++ b/doc/install/digitaloceandocker.md @@ -1,7 +1,8 @@ # Digital Ocean and Docker Machine test environment -## Warning. This guide is for quickly testing different versions of GitLab and -## not recommended for ease of future upgrades or keeping the data you create. +CAUTION: **Caution:** +This guide is for quickly testing different versions of GitLab and not recommended for ease of +future upgrades or keeping the data you create. ## Initial setup @@ -12,92 +13,88 @@ locally on either macOS or Linux. #### Install Docker Toolbox -1. [https://www.docker.com/products/docker-toolbox](https://www.docker.com/products/docker-toolbox) +- <https://www.docker.com/products/docker-toolbox> ### On Linux #### Install Docker Engine -1. [https://docs.docker.com/engine/installation/linux](https://docs.docker.com/engine/installation/linux/) +- <https://docs.docker.com/engine/installation/linux/> #### Install Docker Machine -1. [https://docs.docker.com/machine/install-machine](https://docs.docker.com/machine/install-machine/) +- <https://docs.docker.com/machine/install-machine/> -_The rest of the steps are identical for macOS and Linux_ +NOTE: **Note:** +The rest of the steps are identical for macOS and Linux. ### Create new docker host -1. Login to Digital Ocean -1. Generate a new API token at https://cloud.digitalocean.com/settings/api/tokens +1. Login to Digital Ocean. +1. Generate a new API token at <https://cloud.digitalocean.com/settings/api/tokens>. + This command will create a new DO droplet called `gitlab-test-env-do` that will act as a docker host. -This command will create a new DO droplet called `gitlab-test-env-do` that will act as a docker host. + NOTE: **Note:** + 4GB is the minimum requirement for a Docker host that will run more than one GitLab instance. -**Note: 4GB is the minimum requirement for a Docker host that will run more then one GitLab instance** + - RAM: 4GB + - Name: `gitlab-test-env-do` + - Driver: `digitalocean` -+ RAM: 4GB -+ Name: `gitlab-test-env-do` -+ Driver: `digitalocean` +1. Set the DO token: + ```sh + export DOTOKEN=<your generated token> + ``` -**Set the DO token** - Replace the string below with your generated token +1. Create the machine: -``` -export DOTOKEN=cf3dfd0662933203005c4a73396214b7879d70aabc6352573fe178d340a80248 -``` - -**Create the machine** - -``` -docker-machine create \ - --driver digitalocean \ - --digitalocean-access-token=$DOTOKEN \ - --digitalocean-size "4gb" \ - gitlab-test-env-do -``` - -+ Resource: https://docs.docker.com/machine/drivers/digital-ocean/ + ```sh + docker-machine create \ + --driver digitalocean \ + --digitalocean-access-token=$DOTOKEN \ + --digitalocean-size "4gb" \ + gitlab-test-env-do + ``` +Resource: <https://docs.docker.com/machine/drivers/digital-ocean/>. ### Creating GitLab test instance - #### Connect your shell to the new machine - In this example we'll create a GitLab EE 8.10.8 instance. - First connect the docker client to the docker host you created previously. -``` +```sh eval "$(docker-machine env gitlab-test-env-do)" ``` You can add this to your `~/.bash_profile` file to ensure the `docker` client uses the `gitlab-test-env-do` docker host - #### Create new GitLab container -+ HTTP port: `8888` -+ SSH port: `2222` - + Set `gitlab_shell_ssh_port` using `--env GITLAB_OMNIBUS_CONFIG ` -+ Hostname: IP of docker host -+ Container name: `gitlab-test-8.10` -+ GitLab version: **EE** `8.10.8-ee.0` +- HTTP port: `8888` +- SSH port: `2222` + - Set `gitlab_shell_ssh_port` using `--env GITLAB_OMNIBUS_CONFIG` +- Hostname: IP of docker host +- Container name: `gitlab-test-8.10` +- GitLab version: **EE** `8.10.8-ee.0` -##### Set up container settings +##### Set up container settings -``` +```sh export SSH_PORT=2222 export HTTP_PORT=8888 export VERSION=8.10.8-ee.0 export NAME=gitlab-test-8.10 ``` -##### Create container -``` +##### Create container + +```sh docker run --detach \ --env GITLAB_OMNIBUS_CONFIG="external_url 'http://$(docker-machine ip gitlab-test-env-do):$HTTP_PORT'; gitlab_rails['gitlab_shell_ssh_port'] = $SSH_PORT;" \ --hostname $(docker-machine ip gitlab-test-env-do) \ @@ -110,23 +107,20 @@ gitlab/gitlab-ee:$VERSION ##### Retrieve the docker host IP -``` +```sh docker-machine ip gitlab-test-env-do # example output: 192.168.151.134 ``` - -+ Browse to: http://192.168.151.134:8888/ - +Browse to: <http://192.168.151.134:8888/>. ##### Execute interactive shell/edit configuration - -``` +```sh docker exec -it $NAME /bin/bash ``` -``` +```sh # example commands root@192:/# vi /etc/gitlab/gitlab.rb root@192:/# gitlab-ctl reconfigure @@ -134,6 +128,6 @@ root@192:/# gitlab-ctl reconfigure #### Resources -+ [https://docs.gitlab.com/omnibus/docker/](https://docs.gitlab.com/omnibus/docker/) -+ [https://docs.docker.com/machine/get-started/](https://docs.docker.com/machine/get-started/) -+ [https://docs.docker.com/machine/reference/ip/](https://docs.docker.com/machine/reference/ip/)+ +- <https://docs.gitlab.com/omnibus/docker/>. +- <https://docs.docker.com/machine/get-started/>. +- <https://docs.docker.com/machine/reference/ip/>. diff --git a/doc/ssh/README.md b/doc/ssh/README.md index 5db042326f3..c5b7813b285 100644 --- a/doc/ssh/README.md +++ b/doc/ssh/README.md @@ -8,163 +8,224 @@ you need a secure communication channel for sharing information. The SSH protocol provides this security and allows you to authenticate to the GitLab remote server without supplying your username or password each time. -For a more detailed explanation of how the SSH protocol works, we advise you to -read [this nice tutorial by DigitalOcean](https://www.digitalocean.com/community/tutorials/understanding-the-ssh-encryption-and-connection-process). +For a more detailed explanation of how the SSH protocol works, read +[this nice tutorial by DigitalOcean](https://www.digitalocean.com/community/tutorials/understanding-the-ssh-encryption-and-connection-process). -## Locating an existing SSH key pair +## Requirements -Before generating a new SSH key pair check if your system already has one -at the default location by opening a shell, or Command Prompt on Windows, -and running the following command: +The only requirement is to have the OpenSSH client installed on your system. This +comes pre-installed on GNU/Linux and macOS, but not on Windows. -**Windows Command Prompt:** +Depending on your Windows version, there are different methods to work with +SSH keys. -```bash -type %userprofile%\.ssh\id_rsa.pub -``` +### Installing the SSH client for Windows 10 -**Git Bash on Windows / GNU/Linux / macOS / PowerShell:** +Starting with Windows 10, you can +[install the Windows Subsystem for Linux (WSL)](https://docs.microsoft.com/en-us/windows/wsl/install-win10) +where you can run Linux distributions directly on Windows, without the overhead +of a virtual machine. Once installed and set up, you'll have the Git and SSH +clients at your disposal. -```bash -cat ~/.ssh/id_rsa.pub -``` +### Installing the SSH client for Windows 8.1 and Windows 7 + +The easiest way to install Git and the SSH client on Windows 8.1 and Windows 7 +is [Git for Windows](https://gitforwindows.com). It provides a BASH +emulation (Git Bash) used for running Git from the command line and the +`ssh-keygen` command that is useful to create SSH keys as you'll learn below. + +NOTE: **Alternative tools:** +Although not explored in this page, you can use some alternative tools. +[Cygwin](https://www.cygwin.com) is a large collection of GNU and open source +tools which provide functionality similar to a Unix distribution. +[PuttyGen](https://www.chiark.greenend.org.uk/~sgtatham/putty/latest.html) +provides a graphical user interface to [create SSH keys](https://tartarus.org/~simon/putty-snapshots/htmldoc/Chapter8.html#pubkey-puttygen). + +## Types of SSH keys and which to choose + +GitLab supports RSA, DSA, ECDSA, and ED25519 keys. Their difference lies on +the signing algorithm, and some of them have advantages over the others. For +more information, you can read this +[nice article on ArchWiki](https://wiki.archlinux.org/index.php/SSH_keys#Choosing_the_authentication_key_type). +We'll focus on ED25519 and RSA and here. + +NOTE: **Note:** +As an admin, you can restrict +[which keys should be permitted and their minimum length](../security/ssh_keys_restrictions.md). +By default, all keys are permitted, which is also the case for +[GitLab.com](../user/gitlab_com/index.md#ssh-host-keys-fingerprints). -If you see a string starting with `ssh-rsa` you already have an SSH key pair -and you can skip the generate portion of the next section and skip to the copy -to clipboard step. -If you don't see the string or would like to generate a SSH key pair with a -custom name continue onto the next step. +## ED25519 SSH keys -Note that Public SSH key may also be named as follows: +Following [best practices](https://linux-audit.com/using-ed25519-openssh-keys-instead-of-dsa-rsa-ecdsa/), +you should always favor [ED25519](https://ed25519.cr.yp.to/) SSH keys, since they +are more secure and have better performance over the other types. -- `id_dsa.pub` -- `id_ecdsa.pub` -- `id_ed25519.pub` +They were introduced in OpenSSH 6.5, so any modern OS should include the +option to create them. If for any reason your OS or the GitLab instance you +interact with doesn't support this, you can fallback to RSA. + +## RSA SSH keys + +RSA keys are the most common ones and therefore the most compatible with +servers that may have an old OpenSSH version. Use them if the GitLab server +doesn't work with ED25519 keys. + +The minimum key size is 1024 bits, defaulting to 2048. If you wish to generate a +stronger RSA key pair, specify the `-b` flag with a higher bit value than the +default. + +The old, default password encoding for SSH private keys keys is +[insecure](https://latacora.singles/2018/08/03/the-default-openssh.html); +it's only a single round of an MD5 hash. Since OpenSSH version 6.5, you should +use the `-o` option to `ssh-keygen` to encode your private key in a new, more +secure format. + +If you already have an RSA SSH key pair to use with GitLab, consider upgrading it +to use the more secure password encryption format by using the following command +on the private key: + +```bash +ssh-keygen -o -f ~/.ssh/id_rsa +``` ## Generating a new SSH key pair -1. To generate a new SSH key pair, use the following command: +Before creating an SSH key pair, make sure to read about the +[different types of keys](#types-of-ssh-keys-and-which-to-choose) to understand +their differences. + +To create a new SSH key pair: - **Git Bash on Windows / GNU/Linux / macOS:** +1. Open a terminal on Linux or macOS, or Git Bash / WSL on Windows. +1. Generate a new ED25519 SSH key pair: ```bash - ssh-keygen -o -t rsa -C "your.email@example.com" -b 4096 + ssh-keygen -t ed25519 -C "email@example.com" ``` - (Note: the `-o` option was introduced in 2014; if this command does not work for you, simply remove the `-o` option and try again) + Or, if you want to use RSA: - **Windows:** + ```bash + ssh-keygen -o -t rsa -b 4096 -C "email@example.com" + ``` - Alternatively on Windows you can download - [PuttyGen](http://www.chiark.greenend.org.uk/~sgtatham/putty/download.html) - and follow [this documentation article][winputty] to generate a SSH key pair. + The `-C` flag adds a comment in the key in case you have multiple of them + and want to tell which is which. It is optional. 1. Next, you will be prompted to input a file path to save your SSH key pair to. + If you don't already have an SSH key pair, use the suggested path by pressing + <kbd>Enter</kbd>. Using the suggested path will normally allow your SSH client + to automatically use the SSH key pair with no additional configuration. - If you don't already have an SSH key pair use the suggested path by pressing - enter. Using the suggested path will normally allow your SSH client - to automatically use the SSH key pair with no additional configuration. + If you already have an SSH key pair with the suggested file path, you will need + to input a new file path and [declare what host](#working-with-non-default-ssh-key-pair-paths) + this SSH key pair will be used for in your `~/.ssh/config` file. - If you already have a SSH key pair with the suggested file path, you will need - to input a new file path and declare what host this SSH key pair will be used - for in your `.ssh/config` file, see [**Working with non-default SSH key pair paths**](#working-with-non-default-ssh-key-pair-paths) - for more information. +1. Once the path is decided, you will be prompted to input a password to + secure your new SSH key pair. It's a best practice to use a password, + but it's not required and you can skip creating it by pressing + <kbd>Enter</kbd> twice. -1. Once you have input a file path you will be prompted to input a password to - secure your SSH key pair. It is a best practice to use a password for an SSH - key pair, but it is not required and you can skip creating a password by - pressing enter. + If, in any case, you want to add or change the password of your SSH key pair, + you can use the `-p`flag: - NOTE: **Note:** - If you want to change the password of your SSH key pair, you can use - `ssh-keygen -p -o -f <keyname>`. - The `-o` option was added in 2014, so if this command does not work for you, - simply remove the `-o` option and try again. + ``` + ssh-keygen -p -o -f <keyname> + ``` -## Adding a SSH key to your GitLab account +Now, it's time to add the newly created public key to your GitLab account. -1. The next step is to copy the public SSH key as we will need it afterwards. +## Adding an SSH key to your GitLab account - To copy your public SSH key to the clipboard, use the appropriate code below: +1. Copy your **public** SSH key to the clipboard by using one of the commands below + depending on your Operating System: **macOS:** ```bash - pbcopy < ~/.ssh/id_rsa.pub + pbcopy < ~/.ssh/id_ed25519.pub ``` - **GNU/Linux (requires the xclip package):** + **WSL / GNU/Linux (requires the xclip package):** ```bash - xclip -sel clip < ~/.ssh/id_rsa.pub + xclip -sel clip < ~/.ssh/id_ed25519.pub ``` - **Windows Command Line:** + **Git Bash on Windows:** ```bash - type %userprofile%\.ssh\id_rsa.pub | clip + cat ~/.ssh/id_ed25519.pub | clip ``` - **Git Bash on Windows / Windows PowerShell:** + You can also open the key in a graphical editor and copy it from there, + but be careful not to accidentally change anything. - ```bash - cat ~/.ssh/id_rsa.pub | clip - ``` - -1. The final step is to add your public SSH key to GitLab. + NOTE: **Note:** + If you opted to create an RSA key, the name might differ. - Navigate to the 'SSH Keys' tab in your 'Profile Settings'. - Paste your key in the 'Key' section and give it a relevant 'Title'. - Use an identifiable title like 'Work Laptop - Windows 7' or - 'Home MacBook Pro 15'. +1. Add your public SSH key to your GitLab account by clicking your avatar + in the upper right corner and selecting **Settings**. From there on, + navigate to **SSH Keys** and paste your public key in the "Key" section. + If you created the key with a comment, this will appear under "Title". + If not, give your key an identifiable title like _Work Laptop_ or + _Home Workstation_, and click **Add key**. + NOTE: **Note:** If you manually copied your public SSH key make sure you copied the entire - key starting with `ssh-rsa` and ending with your email. + key starting with `ssh-ed25519` (or `ssh-rsa`) and ending with your email. + +## Testing that everything is set up correctly + +To test whether your SSH key was added correctly, run the following command in +your terminal (replacing `gitlab.com` with your GitLab's instance domain): -1. Optionally you can test your setup by running `ssh -T git@example.com` - (replacing `example.com` with your GitLab domain) and verifying that you - receive a `Welcome to GitLab` message. +```bash +ssh -T git@gitlab.com +``` + +You should receive a _Welcome to GitLab, `@username`!_ message. + +If the welcome message doesn't appear, run SSH's verbose mode by replacing `-T` +with `-vvvT` to understand where the error is. ## Working with non-default SSH key pair paths If you used a non-default file path for your GitLab SSH key pair, you must configure your SSH client to find your GitLab private SSH key -for connections to your GitLab server (perhaps `gitlab.com`). +for connections to GitLab. -For your current terminal session you can do so using the following commands +Open a terminal and use the following commands (replacing `other_id_rsa` with your private SSH key): -**Git Bash on Windows / GNU/Linux / macOS:** - ```bash eval $(ssh-agent -s) ssh-add ~/.ssh/other_id_rsa ``` -To retain these settings you'll need to save them to a configuration file. -For OpenSSH clients this is configured in the `~/.ssh/config` file for some -operating systems. +To retain these settings, you'll need to save them to a configuration file. +For OpenSSH clients this is configured in the `~/.ssh/config` file. In this +file you can set up configurations for multiple hosts, like GitLab.com, your +own GitLab instance, GitHub, Bitbucket, etc. + Below are two example host configurations using their own SSH key: -``` -# GitLab.com server +```conf +# GitLab.com Host gitlab.com -RSAAuthentication yes -IdentityFile ~/.ssh/config/private-key-filename-01 + Preferredauthentications publickey + IdentityFile ~/.ssh/gitlab_com_rsa -# Private GitLab server +# Private GitLab instance Host gitlab.company.com -RSAAuthentication yes -IdentityFile ~/.ssh/config/private-key-filename + Preferredauthentications publickey + IdentityFile ~/.ssh/example_com_rsa ``` -Due to the wide variety of SSH clients and their very large number of -configuration options, further explanation of these topics is beyond the scope -of this document. - -Public SSH keys need to be unique, as they will bind to your account. -Your SSH key is the only identifier you'll have when pushing code via SSH. -That's why it needs to uniquely map to a single user. +Public SSH keys need to be unique to GitLab, as they will bind to your account. +Your SSH key is the only identifier you'll have when pushing code via SSH, +that's why it needs to uniquely map to a single user. ## Deploy keys @@ -240,8 +301,6 @@ not implicitly give any access just by setting them up. How to add your SSH key to Eclipse: https://wiki.eclipse.org/EGit/User_Guide#Eclipse_SSH_Configuration -[winputty]: https://the.earth.li/~sgtatham/putty/0.67/htmldoc/Chapter8.html#pubkey-puttygen - ## SSH on the GitLab server GitLab integrates with the system-installed SSH daemon, designating a user diff --git a/doc/university/training/user_training.md b/doc/university/training/user_training.md index dccb6cbf071..f3a4d766522 100644 --- a/doc/university/training/user_training.md +++ b/doc/university/training/user_training.md @@ -6,91 +6,90 @@ comments: false --- -# Agenda +## Agenda -1. Brief history of Git -1. GitLab walkthrough -1. Configure your environment -1. Workshop +1. Brief history of Git. +1. GitLab walkthrough. +1. Configure your environment. +1. Workshop. --- -# Git introduction +## Git introduction -https://git-scm.com/about +<https://git-scm.com/about> -- Distributed version control - - Does not rely on connection to a central server - - Many copies of the complete history -- Powerful branching and merging -- Adapts to nearly any workflow -- Fast, reliable and stable file format +- Distributed version control. + - Does not rely on connection to a central server. + - Many copies of the complete history. +- Powerful branching and merging. +- Adapts to nearly any workflow. +- Fast, reliable and stable file format. --- -# Help! +## Help! Use the tools at your disposal when you get stuck. -- Use '`git help <command>`' command -- Use Google -- Read documentation at https://git-scm.com +- Use '`git help <command>`' command. +- Use Google. +- Read documentation at <https://git-scm.com>. --- -# GitLab Walkthrough +## GitLab Walkthrough ![fit](logo.png) --- -# Configure your environment +## Configure your environment - Windows: Install 'Git for Windows' -> https://git-for-windows.github.io +> <https://git-for-windows.github.io> - Mac: Type '`git`' in the Terminal application. > If it's not installed, it will prompt you to install it. -- Debian: '`sudo apt-get install git-all`' -or Red Hat '`sudo yum install git-all`' +- Debian: '`sudo apt-get install git-all`' or Red Hat '`sudo yum install git-all`' --- -# Git Workshop +## Git Workshop -## Overview +### Overview -1. Configure Git -1. Configure SSH Key -1. Create a project -1. Committing -1. Feature branching -1. Merge requests -1. Feedback and Collaboration +1. Configure Git. +1. Configure SSH Key. +1. Create a project. +1. Committing. +1. Feature branching. +1. Merge requests. +1. Feedback and Collaboration. --- -# Configure Git +## Configure Git -One-time configuration of the Git client +One-time configuration of the Git client: -```bash +```sh git config --global user.name "Your Name" git config --global user.email you@example.com ``` --- -# Configure SSH Key +## Configure SSH Key -```bash +```sh ssh-keygen -t rsa -b 4096 -C "you@computer-name" ``` -```bash +```sh # You will be prompted for the following information. Press enter to accept the defaults. Defaults appear in parentheses. Generating public/private rsa key pair. Enter file in which to save the key (/Users/you/.ssh/id_rsa): @@ -102,31 +101,30 @@ The key fingerprint is: 39:fc:ce:94:f4:09:13:95:64:9a:65:c1:de:05:4d:01 you@computer-name ``` -Copy your public key and add it to your GitLab profile +Copy your public key and add it to your GitLab profile: -```bash +```sh cat ~/.ssh/id_rsa.pub ``` -```bash +```sh ssh-rsa AAAAB3NzaC1yc2EAAAADAQEL17Ufacg8cDhlQMS5NhV8z3GHZdhCrZbl4gz you@example.com ``` --- -# Create a project +## Create a project -- Create a project in your user namespace - - Choose to import from 'Any Repo by URL' and use - https://gitlab.com/gitlab-org/training-examples.git +- Create a project in your user namespace. + - Choose to import from 'Any Repo by URL' and use <https://gitlab.com/gitlab-org/training-examples.git>. - Create a '`development`' or '`workspace`' directory in your home directory. -- Clone the '`training-examples`' project +- Clone the '`training-examples`' project. --- -# Commands +## Commands (project) -``` +```sh mkdir ~/development cd ~/development @@ -141,37 +139,37 @@ cd training-examples --- -# Git concepts +## Git concepts -**Untracked files** +### Untracked files New files that Git has not been told to track previously. -**Working area** +### Working area Files that have been modified but are not committed. -**Staging area** +### Staging area Modified files that have been marked to go in the next commit. --- -# Committing +## Committing -1. Edit '`edit_this_file.rb`' in '`training-examples`' -1. See it listed as a changed file (working area) -1. View the differences -1. Stage the file -1. Commit -1. Push the commit to the remote -1. View the git log +1. Edit '`edit_this_file.rb`' in '`training-examples`'. +1. See it listed as a changed file (working area). +1. View the differences. +1. Stage the file. +1. Commit. +1. Push the commit to the remote. +1. View the git log. --- -# Commands +## Commands (committing) -``` +```sh # Edit `edit_this_file.rb` git status git diff @@ -183,29 +181,29 @@ git log --- -# Feature branching +## Feature branching -- Efficient parallel workflow for teams -- Develop each feature in a branch -- Keeps changes isolated -- Consider a 1-to-1 link to issues -- Push branches to the server frequently - - Hint: This is a cheap backup for your work-in-progress code +- Efficient parallel workflow for teams. +- Develop each feature in a branch. +- Keeps changes isolated. +- Consider a 1-to-1 link to issues. +- Push branches to the server frequently. + - Hint: This is a cheap backup for your work-in-progress code. --- -# Feature branching +## Feature branching steps -1. Create a new feature branch called 'squash_some_bugs' +1. Create a new feature branch called 'squash_some_bugs'. 1. Edit '`bugs.rb`' and remove all the bugs. -1. Commit -1. Push +1. Commit. +1. Push. --- -# Commands +## Commands (feature branching) -``` +```sh git checkout -b squash_some_bugs # Edit `bugs.rb` git status @@ -216,51 +214,50 @@ git push origin squash_some_bugs --- -# Merge requests +## Merge requests -- When you want feedback create a merge request -- Target is the ‘default’ branch (usually master) -- Assign or mention the person you would like to review -- Add 'WIP' to the title if it's a work in progress -- When accepting, always delete the branch -- Anyone can comment, not just the assignee -- Push corrections to the same branch +- When you want feedback create a merge request. +- Target is the ‘default’ branch (usually master). +- Assign or mention the person you would like to review. +- Add 'WIP' to the title if it's a work in progress. +- When accepting, always delete the branch. +- Anyone can comment, not just the assignee. +- Push corrections to the same branch. --- -# Merge requests +## Merge requests steps -**Create your first merge request** +Create your first merge request: -1. Use the blue button in the activity feed -1. View the diff (changes) and leave a comment -1. Push a new commit to the same branch -1. Review the changes again and notice the update +1. Use the blue button in the activity feed. +1. View the diff (changes) and leave a comment. +1. Push a new commit to the same branch. +1. Review the changes again and notice the update. --- -# Feedback and Collaboration +## Feedback and Collaboration -- Merge requests are a time for feedback and collaboration -- Giving feedback is hard -- Be as kind as possible -- Receiving feedback is hard -- Be as receptive as possible -- Feedback is about the best code, not the person. You are not your code +- Merge requests are a time for feedback and collaboration. +- Giving feedback is hard. +- Be as kind as possible. +- Receiving feedback is hard. +- Be as receptive as possible. +- Feedback is about the best code, not the person. You are not your code. --- -# Feedback and Collaboration +## Feedback and Collaboration resources Review the Thoughtbot code-review guide for suggestions to follow when reviewing merge requests: -[https://github.com/thoughtbot/guides/tree/master/code-review](https://github.com/thoughtbot/guides/tree/master/code-review) +<https://github.com/thoughtbot/guides/tree/master/code-review>. -See GitLab merge requests for examples: -[https://gitlab.com/gitlab-org/gitlab-ce/merge_requests](https://gitlab.com/gitlab-org/gitlab-ce/merge_requests) +See GitLab merge requests for examples: <https://gitlab.com/gitlab-org/gitlab-ce/merge_requests>. --- -# Explore GitLab projects +## Explore GitLab projects ![fit](logo.png) @@ -274,31 +271,29 @@ See GitLab merge requests for examples: --- -# Tags +## Tags -- Useful for marking deployments and releases -- Annotated tags are an unchangeable part of Git history -- Soft/lightweight tags can be set and removed at will -- Many projects combine an annotated release tag with a stable branch -- Consider setting deployment/release tags automatically +- Useful for marking deployments and releases. +- Annotated tags are an unchangeable part of Git history. +- Soft/lightweight tags can be set and removed at will. +- Many projects combine an annotated release tag with a stable branch. +- Consider setting deployment/release tags automatically. --- -# Tags - -- Create a lightweight tag -- Create an annotated tag -- Push the tags to the remote repository +## Tags steps -**Additional resources** +1. Create a lightweight tag. +1. Create an annotated tag. +1. Push the tags to the remote repository. -[http://git-scm.com/book/en/Git-Basics-Tagging](http://git-scm.com/book/en/Git-Basics-Tagging) +Additional resources: <http://git-scm.com/book/en/Git-Basics-Tagging>. --- -# Commands +## Commands (tags) -``` +```sh git checkout master # Lightweight tag @@ -313,31 +308,31 @@ git push origin --tags --- -# Merge conflicts +## Merge conflicts -- Happen often -- Learning to fix conflicts is hard -- Practice makes perfect +- Happen often. +- Learning to fix conflicts is hard. +- Practice makes perfect. - Force push after fixing conflicts. Be careful! --- -# Merge conflicts +## Merge conflicts steps 1. Checkout a new branch and edit `conflicts.rb`. Add 'Line4' and 'Line5'. -1. Commit and push +1. Commit and push. 1. Checkout master and edit `conflicts.rb`. Add 'Line6' and 'Line7' below 'Line3'. -1. Commit and push to master -1. Create a merge request +1. Commit and push to master. +1. Create a merge request. --- -# Merge conflicts +## Merge conflicts commands After creating a merge request you should notice that conflicts exist. Resolve the conflicts locally by rebasing. -``` +```sh git rebase master # Fix conflicts by editing the files. @@ -350,7 +345,7 @@ git push origin <branch> -f --- -# Rebase with squash +## Rebase with squash You may end up with a commit log that looks like this: @@ -368,11 +363,11 @@ Squash these in to meaningful commits using an interactive rebase. --- -# Rebase with squash +## Rebase with squash commands Squash the commits on the same branch we used for the merge conflicts step. -``` +```sh git rebase -i master ``` @@ -380,17 +375,17 @@ In the editor, leave the first commit as 'pick' and set others to 'fixup'. --- -# Questions? +## Questions? ![fit](logo.png) Thank you for your hard work! -**Additional Resources** +## Additional Resources -GitLab Documentation [http://docs.gitlab.com](http://docs.gitlab.com/) -GUI Clients [http://git-scm.com/downloads/guis](http://git-scm.com/downloads/guis) -Pro git book [http://git-scm.com/book](http://git-scm.com/book) -Platzi Course [https://courses.platzi.com/courses/git-gitlab/](https://courses.platzi.com/courses/git-gitlab/) -Code School tutorial [http://try.github.io/](http://try.github.io/) -Contact Us at `subscribers@gitlab.com` +- GitLab Documentation: <http://docs.gitlab.com/>. +- GUI Clients: <http://git-scm.com/downloads/guis>. +- Pro git book: <http://git-scm.com/book>. +- Platzi Course: <https://courses.platzi.com/courses/git-gitlab/>. +- Code School tutorial: <http://try.github.io/>. +- Contact us at `subscribers@gitlab.com`. diff --git a/doc/user/markdown.md b/doc/user/markdown.md index f9bdaea185b..96a509c4b21 100644 --- a/doc/user/markdown.md +++ b/doc/user/markdown.md @@ -1,17 +1,8 @@ # Markdown -## GitLab Flavored Markdown (GFM) - -> **Note:** -> Not all of the GitLab-specific extensions to Markdown that are described in -> this document currently work on our documentation website. -> -> For the best result, we encourage you to check this document out as rendered -> by GitLab: [markdown.md] - -_GitLab uses (as of 11.1) the [CommonMark Ruby Library][commonmarker] for Markdown processing of all new issues, merge requests, comments, and other Markdown content in the GitLab system. As of 11.3, wiki pages and Markdown files (`.md`) in the repositories are also processed with CommonMark. Older content in issues/comments are still processed using the [Redcarpet Ruby library][redcarpet]._ +This markdown guide is valid for GitLab's system markdown entries and files. -_Where there are significant differences, we will try to call them out in this document._ +## GitLab Flavored Markdown (GFM) GitLab uses "GitLab Flavored Markdown" (GFM). It extends the [CommonMark specification][commonmark-spec] (which is based on standard Markdown) in a few significant ways to add some useful functionality. It was inspired by [GitHub Flavored Markdown](https://help.github.com/articles/basic-writing-and-formatting-syntax/). @@ -26,11 +17,28 @@ You can use GFM in the following areas: - markdown documents inside the repository You can also use other rich text files in GitLab. You might have to install a -dependency to do so. Please see the [github-markup gem readme](https://github.com/gitlabhq/markup#markups) for more information. +dependency to do so. Please see the [`github-markup` gem readme](https://github.com/gitlabhq/markup#markups) for more information. + +> **Notes:** +> +> For the best result, we encourage you to check this document out as rendered +> by GitLab itself: [markdown.md] +> +> As of 11.1, GitLab uses the [CommonMark Ruby Library][commonmarker] for Markdown +processing of all new issues, merge requests, comments, and other Markdown content +in the GitLab system. As of 11.3, wiki pages and Markdown files (`.md`) in the +repositories are also processed with CommonMark. Older content in issues/comments +are still processed using the [Redcarpet Ruby library][redcarpet]. +> +> _Where there are significant differences, we will try to call them out in this document._ ### Transitioning to CommonMark -You may have Markdown documents in your repository that were written using some of the nuances of RedCarpet's version of Markdown. Since CommonMark uses a slightly stricter syntax, these documents may now display a little strangely since we've transitioned to CommonMark. Numbered lists with nested lists in particular can be displayed incorrectly. +You may have Markdown documents in your repository that were written using some +of the nuances of RedCarpet's version of Markdown. Since CommonMark uses a +slightly stricter syntax, these documents may now display a little strangely +since we've transitioned to CommonMark. Numbered lists with nested lists in +particular can be displayed incorrectly. It is usually quite easy to fix. In the case of a nested list such as this: @@ -50,11 +58,18 @@ simply add a space to each nested item: In the documentation below, we try to highlight some of the differences. -If you have a need to view a document using RedCarpet, you can add the token `legacy_render=1` to the end of the url, like this: +If you have a need to view a document using RedCarpet, you can add the token +`legacy_render=1` to the end of the url, like this: https://gitlab.com/gitlab-org/gitlab-ce/blob/master/doc/user/markdown.md?legacy_render=1 -If you have a large volume of Markdown files, it can be tedious to determine if they will be displayed correctly or not. You can use the [diff_redcarpet_cmark](https://gitlab.com/digitalmoksha/diff_redcarpet_cmark) tool (not an officially supported product) to generate a list of files and differences between how RedCarpet and CommonMark render the files. It can give you a great idea if anything needs to be changed - many times nothing will need to changed. +If you have a large volume of Markdown files, it can be tedious to determine +if they will be displayed correctly or not. You can use the +[diff_redcarpet_cmark](https://gitlab.com/digitalmoksha/diff_redcarpet_cmark) +tool (not an officially supported product) to generate a list of files and +differences between how RedCarpet and CommonMark render the files. It can give +you a great idea if anything needs to be changed - many times nothing will need +to changed. ### Newlines @@ -63,7 +78,8 @@ https://gitlab.com/gitlab-org/gitlab-ce/blob/master/doc/user/markdown.md#newline GFM honors the markdown specification in how [paragraphs and line breaks are handled][commonmark-spec]. -A paragraph is simply one or more consecutive lines of text, separated by one or more blank lines. +A paragraph is simply one or more consecutive lines of text, separated by one or +more blank lines. Line-breaks, or soft returns, are rendered if you end a line with two or more spaces: <!-- (Do *NOT* remove the two ending whitespaces in the following line.) --> @@ -85,7 +101,9 @@ Sugar is sweet > If this is not rendered correctly, see https://gitlab.com/gitlab-org/gitlab-ce/blob/master/doc/user/markdown.md#multiple-underscores-in-words -It is not reasonable to italicize just _part_ of a word, especially when you're dealing with code and names that often appear with multiple underscores. Therefore, GFM ignores multiple underscores in words: +It is not reasonable to italicize just _part_ of a word, especially when you're +dealing with code and names that often appear with multiple underscores. +Therefore, GFM ignores multiple underscores in words: perform_complicated_task @@ -124,7 +142,7 @@ https://gitlab.com/gitlab-org/gitlab-ce/blob/master/doc/user/markdown.md#multili On top of standard Markdown [blockquotes](#blockquotes), which require prepending `>` to quoted lines, GFM supports multiline blockquotes fenced by <code>>>></code>: -```no-highlight +``` >>> If you paste a message from somewhere else @@ -158,7 +176,7 @@ Blocks of code are either fenced by lines with three back-ticks <code>```</code> or are indented with four spaces. Only the fenced code blocks support syntax highlighting: -```no-highlight +``` Inline `code` has `back-ticks around` it. ``` @@ -248,21 +266,23 @@ However the wrapping tags cannot be mixed as such: > If this is not rendered correctly, see https://gitlab.com/gitlab-org/gitlab-ce/blob/master/doc/user/markdown.md#emoji - Sometimes you want to :monkey: around a bit and add some :star2: to your :speech_balloon:. Well we have a gift for you: +``` +Sometimes you want to :monkey: around a bit and add some :star2: to your :speech_balloon:. Well we have a gift for you: - :zap: You can use emoji anywhere GFM is supported. :v: +:zap: You can use emoji anywhere GFM is supported. :v: - You can use it to point out a :bug: or warn about :speak_no_evil: patches. And if someone improves your really :snail: code, send them some :birthday:. People will :heart: you for that. +You can use it to point out a :bug: or warn about :speak_no_evil: patches. And if someone improves your really :snail: code, send them some :birthday:. People will :heart: you for that. - If you are new to this, don't be :fearful:. You can easily join the emoji :family:. All you need to do is to look up one of the supported codes. +If you are new to this, don't be :fearful:. You can easily join the emoji :family:. All you need to do is to look up one of the supported codes. - Consult the [Emoji Cheat Sheet](https://www.emojicopy.com) for a list of all supported emoji codes. :thumbsup: +Consult the [Emoji Cheat Sheet](https://www.emojicopy.com) for a list of all supported emoji codes. :thumbsup: - Most emoji are natively supported on macOS, Windows, iOS, Android and will fallback to image-based emoji where there is lack of support. +Most emoji are natively supported on macOS, Windows, iOS, Android and will fallback to image-based emoji where there is lack of support. - On Linux, you can download [Noto Color Emoji](https://www.google.com/get/noto/help/emoji/) to get full native emoji support. +On Linux, you can download [Noto Color Emoji](https://www.google.com/get/noto/help/emoji/) to get full native emoji support. - Ubuntu 18.04 (like many modern Linux distros) has this font installed by default. +Ubuntu 18.04 (like many modern Linux distros) has this font installed by default. +``` Sometimes you want to <img src="https://gitlab.com/gitlab-org/gitlab-ce/raw/master/app/assets/images/emoji/monkey.png" width="20px" height="20px"> around a bit and add some <img src="https://gitlab.com/gitlab-org/gitlab-ce/raw/master/app/assets/images/emoji/star2.png" width="20px" height="20px"> to your <img src="https://gitlab.com/gitlab-org/gitlab-ce/raw/master/app/assets/images/emoji/speech_balloon.png" width="20px" height="20px">. Well we have a gift for you: @@ -281,7 +301,6 @@ On Linux, you can download [Noto Color Emoji](https://www.google.com/get/noto/he Ubuntu 18.04 (like many modern Linux distros) has this font installed by default. - ### Special GitLab References GFM recognizes special references. @@ -343,7 +362,7 @@ https://gitlab.com/gitlab-org/gitlab-ce/blob/master/doc/user/markdown.md#task-li You can add task lists to issues, merge requests and comments. To create a task list, add a specially-formatted Markdown list, like so: -```no-highlight +``` - [x] Completed task - [ ] Incomplete task - [ ] Sub-task 1 @@ -355,7 +374,7 @@ You can add task lists to issues, merge requests and comments. To create a task Tasks formatted as ordered lists are supported as well: -```no-highlight +``` 1. [x] Completed task 1. [ ] Incomplete task 1. [ ] Sub-task 1 @@ -412,7 +431,7 @@ This math is inline ![alt text](img/math_inline_sup_render_gfm.png). This is on a separate line -<div align="center"><img src="./img/math_inline_sup_render_gfm.png" ></div> +<img src="./img/math_inline_sup_render_gfm.png" > _Be advised that KaTeX only supports a [subset][katex-subset] of LaTeX._ @@ -440,7 +459,7 @@ Examples: `HSL(540,70%,50%)` `HSLA(540,70%,50%,0.7)` -Become: +Becomes: ![alt color-inline-colorchip-render-gfm](img/color_inline_colorchip_render_gfm.png) @@ -482,7 +501,7 @@ For details see the [Mermaid official page][mermaid]. ### Headers -```no-highlight +``` # H1 ## H2 ### H3 @@ -540,7 +559,7 @@ Note that the Emoji processing happens before the header IDs are generated, so t Examples: -```no-highlight +``` Emphasis, aka italics, with *asterisks* or _underscores_. Strong emphasis, aka bold, with **asterisks** or __underscores__. @@ -550,7 +569,7 @@ Combined emphasis with **asterisks and _underscores_**. Strikethrough uses two tildes. ~~Scratch this.~~ ``` -Become: +Becomes: Emphasis, aka italics, with *asterisks* or _underscores_. @@ -564,7 +583,7 @@ Strikethrough uses two tildes. ~~Scratch this.~~ Examples: -```no-highlight +``` 1. First ordered list item 2. Another item * Unordered sub-list. @@ -577,7 +596,7 @@ Examples: + Or pluses ``` -Become: +Becomes: 1. First ordered list item 2. Another item @@ -595,7 +614,7 @@ each subsequent paragraph should be indented to the same level as the start of t Example: -```no-highlight +``` 1. First ordered list item Second paragraph of first item. @@ -616,7 +635,7 @@ the paragraph will appear outside the list, instead of properly indented under t Example: -```no-highlight +``` 1. First ordered list item Paragraph of first item. @@ -676,7 +695,7 @@ Examples: [logo]: img/markdown_logo.png -Become: +Becomes: Here's our logo: @@ -694,7 +713,7 @@ Reference-style: Examples: -```no-highlight +``` > Blockquotes are very handy in email to emulate reply text. > This line is part of the same quote. @@ -703,7 +722,7 @@ Quote break. > This is a very long line that will still be quoted properly when it wraps. Oh boy let's keep writing to make sure this is long enough to actually wrap for everyone. Oh, you can *put* **Markdown** into a blockquote. ``` -Become: +Becomes: > Blockquotes are very handy in email to emulate reply text. > This line is part of the same quote. @@ -720,7 +739,7 @@ See the documentation for HTML::Pipeline's [SanitizationFilter](http://www.rubyd Examples: -```no-highlight +``` <dl> <dt>Definition list</dt> <dd>Is something people use sometimes.</dd> @@ -730,7 +749,7 @@ Examples: </dl> ``` -Become: +Becomes: <dl> <dt>Definition list</dt> @@ -788,7 +807,7 @@ ___ Underscores ``` -Become: +Becomes: Three or more... @@ -826,7 +845,7 @@ This line is *on its own line*, because the previous line ends with two spaces. spaces. ``` -Become: +Becomes: Here's a line for us to start with. diff --git a/doc/user/permissions.md b/doc/user/permissions.md index 4359592905d..1fd230a41aa 100644 --- a/doc/user/permissions.md +++ b/doc/user/permissions.md @@ -95,6 +95,7 @@ The following table depicts the various user permission levels in a project. | Manage GitLab Pages | | | | ✓ | ✓ | | Manage GitLab Pages domains and certificates | | | | ✓ | ✓ | | Remove GitLab Pages | | | | | ✓ | +| View GitLab Pages protected by [access control](../administration/pages/index.md#access-control) | ✓ | ✓ | ✓ | ✓ | ✓ | | Manage clusters | | | | ✓ | ✓ | | Manage license policy **[ULTIMATE]** | | | | ✓ | ✓ | | Edit comments (posted by any user) | | | | ✓ | ✓ | @@ -206,7 +207,7 @@ They will, like usual users, receive a role in the project or group with all the abilities that are mentioned in the table above. They cannot however create groups or projects, and they have the same access as logged out users in all other cases. - + An administrator can flag a user as external [through the API](../api/users.md) or by checking the checkbox on the admin panel. As an administrator, navigate to **Admin > Users** to create a new user or edit an existing one. There, you @@ -217,7 +218,7 @@ by an administrator under **Admin > Application Settings**. ### Default internal users -The "Internal users" field allows specifying an e-mail address regex pattern to identify default internal users. +The "Internal users" field allows specifying an e-mail address regex pattern to identify default internal users. New users whose email address matches the regex pattern will be set to internal by default rather than an external collaborator. diff --git a/doc/user/project/import/index.md b/doc/user/project/import/index.md index 4ea35a30bbf..2f5efbe84d9 100644 --- a/doc/user/project/import/index.md +++ b/doc/user/project/import/index.md @@ -1,6 +1,7 @@ # Migrating projects to a GitLab instance -1. [From Bitbucket.org](bitbucket.md) +1. [From Bitbucket Cloud (aka bitbucket.org)](bitbucket.md) +1. [From Bitbucket Server (aka Stash)](bitbucket_server.md) 1. [From ClearCase](clearcase.md) 1. [From CVS](cvs.md) 1. [From FogBugz](fogbugz.md) diff --git a/doc/user/project/repository/index.md b/doc/user/project/repository/index.md index beff4b89424..6d822d3f7f2 100644 --- a/doc/user/project/repository/index.md +++ b/doc/user/project/repository/index.md @@ -53,6 +53,32 @@ To get started with the command line, please read through the Use GitLab's [file finder](../../../workflow/file_finder.md) to search for files in a repository. +### Repository README and index files + +When a `README` or `index` file is present in a repository, its contents will be +automatically pre-rendered by GitLab without opening it. + +They can either be plain text or have an extension of a supported markup language: + +- Asciidoc: `README.adoc` or `index.adoc` +- Markdown: `README.md` or `index.md` +- reStructuredText: `README.rst` or `index.rst` +- Text: `README.txt` or `index.txt` + +Some things to note about precedence: + +1. When both a `README` and an `index` file are present, the `README` will always + take precedence. +1. When more than one file is present with different extensions, they are + ordered alphabetically, with the exception of a file without an extension + which will always be last in precedence. For example, `README.adoc` will take + precedence over `README.md`, and `README.rst` will take precedence over + `README`. + +NOTE: **Note:** +`index` files without an extension will not automatically pre-render. You'll +have to explicitly open them to see their contents. + ### Jupyter Notebook files > [Introduced](https://gitlab.com/gitlab-org/gitlab-ce/issues/2508) in GitLab 9.1 @@ -165,7 +191,7 @@ minutes. ![Repository Languages bar](img/repository_languages.png) -Not all files are detected, among others; documentation, +Not all files are detected, among others; documentation, vendored code, and most markup languages are excluded. This behaviour can be adjusted by overriding the default. For example, to enable `.proto` files to be detected, add the following to `.gitattributes` in the root of your repository. diff --git a/doc/workflow/notifications.md b/doc/workflow/notifications.md index 9e41038e02e..c590ac4b0ba 100644 --- a/doc/workflow/notifications.md +++ b/doc/workflow/notifications.md @@ -92,12 +92,16 @@ In most of the below cases, the notification will be sent to: | Reassign issue | The above, plus the old assignee | | Reopen issue | | | Due issue | Participants and Custom notification level with this event selected | +| Change milestone issue | Subscribers, participants mentioned, and Custom notification level with this event selected | +| Remove milestone issue | Subscribers, participants mentioned, and Custom notification level with this event selected | | New merge request | | | Push to merge request | Participants and Custom notification level with this event selected | | Reassign merge request | The above, plus the old assignee | | Close merge request | | | Reopen merge request | | | Merge merge request | | +| Change milestone merge request | Subscribers, participants mentioned, and Custom notification level with this event selected | +| Remove milestone merge request | Subscribers, participants mentioned, and Custom notification level with this event selected | | New comment | The above, plus anyone mentioned by `@username` in the comment, with notification level "Mention" or higher | | Failed pipeline | The author of the pipeline | | Successful pipeline | The author of the pipeline, if they have the custom notification setting for successful pipelines set | diff --git a/lib/api/issues.rb b/lib/api/issues.rb index e37083165f5..7909f9c7a00 100644 --- a/lib/api/issues.rb +++ b/lib/api/issues.rb @@ -294,6 +294,30 @@ module API end # rubocop: enable CodeReuse/ActiveRecord + desc 'List merge requests that are related to the issue' do + success Entities::MergeRequestBasic + end + params do + requires :issue_iid, type: Integer, desc: 'The internal ID of a project issue' + end + get ':id/issues/:issue_iid/related_merge_requests' do + issue = find_project_issue(params[:issue_iid]) + + merge_request_iids = ::Issues::ReferencedMergeRequestsService.new(user_project, current_user) + .execute(issue) + .flatten + .map(&:iid) + + merge_requests = + if merge_request_iids.present? + MergeRequestsFinder.new(current_user, project_id: user_project.id, iids: merge_request_iids).execute + else + MergeRequest.none + end + + present paginate(merge_requests), with: Entities::MergeRequestBasic, current_user: current_user, project: user_project + end + desc 'List merge requests closing issue' do success Entities::MergeRequestBasic end diff --git a/lib/api/users.rb b/lib/api/users.rb index 47382b09207..2a56506f3a5 100644 --- a/lib/api/users.rb +++ b/lib/api/users.rb @@ -512,11 +512,9 @@ module API PersonalAccessTokensFinder.new({ user: user, impersonation: true }.merge(options)) end - # rubocop: disable CodeReuse/ActiveRecord def find_impersonation_token - finder.find_by(id: declared_params[:impersonation_token_id]) || not_found!('Impersonation Token') + finder.find_by_id(declared_params[:impersonation_token_id]) || not_found!('Impersonation Token') end - # rubocop: enable CodeReuse/ActiveRecord end before { authenticated_as_admin! } diff --git a/lib/banzai/filter/issuable_state_filter.rb b/lib/banzai/filter/issuable_state_filter.rb index d7fe012883d..8e2358694d4 100644 --- a/lib/banzai/filter/issuable_state_filter.rb +++ b/lib/banzai/filter/issuable_state_filter.rb @@ -18,7 +18,7 @@ module Banzai issuables = extractor.extract([doc]) issuables.each do |node, issuable| - next if !can_read_cross_project? && issuable.project != project + next if !can_read_cross_project? && cross_reference?(issuable) if VISIBLE_STATES.include?(issuable.state) && issuable_reference?(node.inner_html, issuable) node.content += " (#{issuable.state})" @@ -31,7 +31,14 @@ module Banzai private def issuable_reference?(text, issuable) - text == issuable.reference_link_text(project || group) + CGI.unescapeHTML(text) == issuable.reference_link_text(project || group) + end + + def cross_reference?(issuable) + return true if issuable.project != project + return true if issuable.respond_to?(:group) && issuable.group != group + + false end def can_read_cross_project? diff --git a/lib/banzai/issuable_extractor.rb b/lib/banzai/issuable_extractor.rb index 0a05d46db4c..341dbb74fe0 100644 --- a/lib/banzai/issuable_extractor.rb +++ b/lib/banzai/issuable_extractor.rb @@ -9,13 +9,11 @@ module Banzai # so we can avoid N+1 queries problem class IssuableExtractor - QUERY = %q( - descendant-or-self::a[contains(concat(" ", @class, " "), " gfm ")] - [@data-reference-type="issue" or @data-reference-type="merge_request"] - ).freeze - attr_reader :context + ISSUE_REFERENCE_TYPE = '@data-reference-type="issue"'.freeze + MERGE_REQUEST_REFERENCE_TYPE = '@data-reference-type="merge_request"'.freeze + # context - An instance of Banzai::RenderContext. def initialize(context) @context = context @@ -24,21 +22,38 @@ module Banzai # Returns Hash in the form { node => issuable_instance } def extract(documents) nodes = documents.flat_map do |document| - document.xpath(QUERY) + document.xpath(query) end - issue_parser = Banzai::ReferenceParser::IssueParser.new(context) + # The project or group for the issuable might be pending for deletion! + # Filter them out because we don't care about them. + issuables_for_nodes(nodes).select { |node, issuable| issuable.project || issuable.group } + end + + private - merge_request_parser = + def issuables_for_nodes(nodes) + parsers.each_with_object({}) do |parser, result| + result.merge!(parser.records_for_nodes(nodes)) + end + end + + def parsers + [ + Banzai::ReferenceParser::IssueParser.new(context), Banzai::ReferenceParser::MergeRequestParser.new(context) + ] + end - issuables_for_nodes = issue_parser.records_for_nodes(nodes).merge( - merge_request_parser.records_for_nodes(nodes) + def query + %Q( + descendant-or-self::a[contains(concat(" ", @class, " "), " gfm ")] + [#{reference_types.join(' or ')}] ) + end - # The project for the issue/MR might be pending for deletion! - # Filter them out because we don't care about them. - issuables_for_nodes.select { |node, issuable| issuable.project } + def reference_types + [ISSUE_REFERENCE_TYPE, MERGE_REQUEST_REFERENCE_TYPE] end end end diff --git a/lib/extracts_path.rb b/lib/extracts_path.rb index a340a276640..655278da711 100644 --- a/lib/extracts_path.rb +++ b/lib/extracts_path.rb @@ -153,7 +153,7 @@ module ExtractsPath private - # overriden in subclasses, do not remove + # overridden in subclasses, do not remove def get_id id = [params[:id] || params[:ref]] id << "/" + params[:path] unless params[:path].blank? diff --git a/lib/gitlab/auth/o_auth/auth_hash.rb b/lib/gitlab/auth/o_auth/auth_hash.rb index 4a5f9d2839d..36fc8061d92 100644 --- a/lib/gitlab/auth/o_auth/auth_hash.rb +++ b/lib/gitlab/auth/o_auth/auth_hash.rb @@ -80,7 +80,7 @@ module Gitlab end # Get the first part of the email address (before @) - # In addtion in removes illegal characters + # In addition in removes illegal characters def generate_username(email) email.match(/^[^@]*/)[0].mb_chars.normalize(:kd).gsub(/[^\x00-\x7F]/, '').to_s end diff --git a/lib/gitlab/background_migration/populate_cluster_kubernetes_namespace_table.rb b/lib/gitlab/background_migration/populate_cluster_kubernetes_namespace_table.rb new file mode 100644 index 00000000000..35bfc381180 --- /dev/null +++ b/lib/gitlab/background_migration/populate_cluster_kubernetes_namespace_table.rb @@ -0,0 +1,82 @@ +# frozen_string_literal: true +# +# rubocop:disable Style/Documentation + +module Gitlab + module BackgroundMigration + class PopulateClusterKubernetesNamespaceTable + include Gitlab::Database::MigrationHelpers + + BATCH_SIZE = 1_000 + + module Migratable + class KubernetesNamespace < ActiveRecord::Base + self.table_name = 'clusters_kubernetes_namespaces' + end + + class ClusterProject < ActiveRecord::Base + include EachBatch + + self.table_name = 'cluster_projects' + + belongs_to :project + + def self.with_no_kubernetes_namespace + where.not(id: Migratable::KubernetesNamespace.select(:cluster_project_id)) + end + + def namespace + slug = "#{project.path}-#{project.id}".downcase + slug.gsub(/[^-a-z0-9]/, '-').gsub(/^-+/, '') + end + + def service_account + "#{namespace}-service-account" + end + end + + class Project < ActiveRecord::Base + self.table_name = 'projects' + end + end + + def perform + cluster_projects_with_no_kubernetes_namespace.each_batch(of: BATCH_SIZE) do |cluster_projects_batch, index| + sql_values = sql_values_for(cluster_projects_batch) + + insert_into_cluster_kubernetes_namespace(sql_values) + end + end + + private + + def cluster_projects_with_no_kubernetes_namespace + Migratable::ClusterProject.with_no_kubernetes_namespace + end + + def sql_values_for(cluster_projects) + cluster_projects.map do |cluster_project| + values_for_cluster_project(cluster_project) + end + end + + def values_for_cluster_project(cluster_project) + { + cluster_project_id: cluster_project.id, + cluster_id: cluster_project.cluster_id, + project_id: cluster_project.project_id, + namespace: cluster_project.namespace, + service_account_name: cluster_project.service_account, + created_at: 'NOW()', + updated_at: 'NOW()' + } + end + + def insert_into_cluster_kubernetes_namespace(rows) + Gitlab::Database.bulk_insert(Migratable::KubernetesNamespace.table_name, + rows, + disable_quote: [:created_at, :updated_at]) + end + end + end +end diff --git a/lib/gitlab/background_migration/set_confidential_note_events_on_services.rb b/lib/gitlab/background_migration/set_confidential_note_events_on_services.rb index e5e8837221e..bc434b0cb64 100644 --- a/lib/gitlab/background_migration/set_confidential_note_events_on_services.rb +++ b/lib/gitlab/background_migration/set_confidential_note_events_on_services.rb @@ -3,8 +3,8 @@ module Gitlab module BackgroundMigration - # Ensures services which previously recieved all notes events continue - # to recieve confidential ones. + # Ensures services which previously received all notes events continue + # to receive confidential ones. class SetConfidentialNoteEventsOnServices class Service < ActiveRecord::Base self.table_name = 'services' diff --git a/lib/gitlab/background_migration/set_confidential_note_events_on_webhooks.rb b/lib/gitlab/background_migration/set_confidential_note_events_on_webhooks.rb index 171c8ef21b7..28d8d2c640b 100644 --- a/lib/gitlab/background_migration/set_confidential_note_events_on_webhooks.rb +++ b/lib/gitlab/background_migration/set_confidential_note_events_on_webhooks.rb @@ -3,8 +3,8 @@ module Gitlab module BackgroundMigration - # Ensures hooks which previously recieved all notes events continue - # to recieve confidential ones. + # Ensures hooks which previously received all notes events continue + # to receive confidential ones. class SetConfidentialNoteEventsOnWebhooks class WebHook < ActiveRecord::Base self.table_name = 'web_hooks' diff --git a/lib/gitlab/ci/pipeline/chain/create.rb b/lib/gitlab/ci/pipeline/chain/create.rb index c882241ef6a..aa627bdb009 100644 --- a/lib/gitlab/ci/pipeline/chain/create.rb +++ b/lib/gitlab/ci/pipeline/chain/create.rb @@ -7,26 +7,11 @@ module Gitlab class Create < Chain::Base include Chain::Helpers - # rubocop: disable CodeReuse/ActiveRecord def perform! - ::Ci::Pipeline.transaction do - pipeline.save! - - ## - # Create environments before the pipeline starts. - # - pipeline.builds.each do |build| - if build.has_environment? - project.environments.find_or_create_by( - name: build.expanded_environment_name - ) - end - end - end + pipeline.save! rescue ActiveRecord::RecordInvalid => e error("Failed to persist the pipeline: #{e}") end - # rubocop: enable CodeReuse/ActiveRecord def break? !pipeline.persisted? diff --git a/lib/gitlab/ci/status/build/action.rb b/lib/gitlab/ci/status/build/action.rb index 6c9125647ad..45d9ba41e92 100644 --- a/lib/gitlab/ci/status/build/action.rb +++ b/lib/gitlab/ci/status/build/action.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + module Gitlab module Ci module Status diff --git a/lib/gitlab/ci/status/build/cancelable.rb b/lib/gitlab/ci/status/build/cancelable.rb index 024047d4983..43fb5cdbbe6 100644 --- a/lib/gitlab/ci/status/build/cancelable.rb +++ b/lib/gitlab/ci/status/build/cancelable.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + module Gitlab module Ci module Status diff --git a/lib/gitlab/ci/status/build/canceled.rb b/lib/gitlab/ci/status/build/canceled.rb index c83e2734a73..0518b9e673d 100644 --- a/lib/gitlab/ci/status/build/canceled.rb +++ b/lib/gitlab/ci/status/build/canceled.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + module Gitlab module Ci module Status diff --git a/lib/gitlab/ci/status/build/common.rb b/lib/gitlab/ci/status/build/common.rb index c1fc70ac266..6a75ec5c37f 100644 --- a/lib/gitlab/ci/status/build/common.rb +++ b/lib/gitlab/ci/status/build/common.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + module Gitlab module Ci module Status diff --git a/lib/gitlab/ci/status/build/created.rb b/lib/gitlab/ci/status/build/created.rb index 5be8e9de425..780fea23123 100644 --- a/lib/gitlab/ci/status/build/created.rb +++ b/lib/gitlab/ci/status/build/created.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + module Gitlab module Ci module Status diff --git a/lib/gitlab/ci/status/build/erased.rb b/lib/gitlab/ci/status/build/erased.rb index 495227c2ffb..d74cfc1ee77 100644 --- a/lib/gitlab/ci/status/build/erased.rb +++ b/lib/gitlab/ci/status/build/erased.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + module Gitlab module Ci module Status diff --git a/lib/gitlab/ci/status/build/factory.rb b/lib/gitlab/ci/status/build/factory.rb index 4a74d6d6ed1..6e4bfe23f2b 100644 --- a/lib/gitlab/ci/status/build/factory.rb +++ b/lib/gitlab/ci/status/build/factory.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + module Gitlab module Ci module Status diff --git a/lib/gitlab/ci/status/build/failed.rb b/lib/gitlab/ci/status/build/failed.rb index 4babc23a495..d40454df737 100644 --- a/lib/gitlab/ci/status/build/failed.rb +++ b/lib/gitlab/ci/status/build/failed.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + module Gitlab module Ci module Status @@ -12,7 +14,8 @@ module Gitlab missing_dependency_failure: 'missing dependency failure', runner_unsupported: 'unsupported runner', stale_schedule: 'stale schedule', - job_execution_timeout: 'job execution timeout' + job_execution_timeout: 'job execution timeout', + archived_failure: 'archived failure' }.freeze private_constant :REASONS diff --git a/lib/gitlab/ci/status/build/failed_allowed.rb b/lib/gitlab/ci/status/build/failed_allowed.rb index ca0046fb1f7..d7570fdd3e2 100644 --- a/lib/gitlab/ci/status/build/failed_allowed.rb +++ b/lib/gitlab/ci/status/build/failed_allowed.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + module Gitlab module Ci module Status diff --git a/lib/gitlab/ci/status/build/manual.rb b/lib/gitlab/ci/status/build/manual.rb index 042da6392d3..d01b09f1398 100644 --- a/lib/gitlab/ci/status/build/manual.rb +++ b/lib/gitlab/ci/status/build/manual.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + module Gitlab module Ci module Status diff --git a/lib/gitlab/ci/status/build/pending.rb b/lib/gitlab/ci/status/build/pending.rb index 9dd9a27ad57..95f668295dd 100644 --- a/lib/gitlab/ci/status/build/pending.rb +++ b/lib/gitlab/ci/status/build/pending.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + module Gitlab module Ci module Status diff --git a/lib/gitlab/ci/status/build/play.rb b/lib/gitlab/ci/status/build/play.rb index a8b9ebf0803..c66b8ca5654 100644 --- a/lib/gitlab/ci/status/build/play.rb +++ b/lib/gitlab/ci/status/build/play.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + module Gitlab module Ci module Status diff --git a/lib/gitlab/ci/status/build/retried.rb b/lib/gitlab/ci/status/build/retried.rb index 6e190e4ee3c..b489dc68733 100644 --- a/lib/gitlab/ci/status/build/retried.rb +++ b/lib/gitlab/ci/status/build/retried.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + module Gitlab module Ci module Status diff --git a/lib/gitlab/ci/status/build/retryable.rb b/lib/gitlab/ci/status/build/retryable.rb index 5aeb8e51480..eb6b3f21604 100644 --- a/lib/gitlab/ci/status/build/retryable.rb +++ b/lib/gitlab/ci/status/build/retryable.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + module Gitlab module Ci module Status diff --git a/lib/gitlab/ci/status/build/scheduled.rb b/lib/gitlab/ci/status/build/scheduled.rb index 62ad9083616..f443dbee120 100644 --- a/lib/gitlab/ci/status/build/scheduled.rb +++ b/lib/gitlab/ci/status/build/scheduled.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + module Gitlab module Ci module Status diff --git a/lib/gitlab/ci/status/build/skipped.rb b/lib/gitlab/ci/status/build/skipped.rb index 3e678d0baee..4fe2f7b3114 100644 --- a/lib/gitlab/ci/status/build/skipped.rb +++ b/lib/gitlab/ci/status/build/skipped.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + module Gitlab module Ci module Status diff --git a/lib/gitlab/ci/status/build/stop.rb b/lib/gitlab/ci/status/build/stop.rb index dea838bfa39..a620e7ad126 100644 --- a/lib/gitlab/ci/status/build/stop.rb +++ b/lib/gitlab/ci/status/build/stop.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + module Gitlab module Ci module Status diff --git a/lib/gitlab/ci/status/build/unschedule.rb b/lib/gitlab/ci/status/build/unschedule.rb index e1b7b83428c..9110839cb55 100644 --- a/lib/gitlab/ci/status/build/unschedule.rb +++ b/lib/gitlab/ci/status/build/unschedule.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + module Gitlab module Ci module Status diff --git a/lib/gitlab/ci/status/canceled.rb b/lib/gitlab/ci/status/canceled.rb index e6195a60d4f..07f37732023 100644 --- a/lib/gitlab/ci/status/canceled.rb +++ b/lib/gitlab/ci/status/canceled.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + module Gitlab module Ci module Status diff --git a/lib/gitlab/ci/status/core.rb b/lib/gitlab/ci/status/core.rb index 9d6a2f51c11..ea773ee9944 100644 --- a/lib/gitlab/ci/status/core.rb +++ b/lib/gitlab/ci/status/core.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + module Gitlab module Ci module Status diff --git a/lib/gitlab/ci/status/created.rb b/lib/gitlab/ci/status/created.rb index 846f00b83dd..fface4bb97b 100644 --- a/lib/gitlab/ci/status/created.rb +++ b/lib/gitlab/ci/status/created.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + module Gitlab module Ci module Status diff --git a/lib/gitlab/ci/status/extended.rb b/lib/gitlab/ci/status/extended.rb index 1e8101f8949..b72a28ed0b6 100644 --- a/lib/gitlab/ci/status/extended.rb +++ b/lib/gitlab/ci/status/extended.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + module Gitlab module Ci module Status diff --git a/lib/gitlab/ci/status/external/common.rb b/lib/gitlab/ci/status/external/common.rb index 9307545b5b1..4169f5b3210 100644 --- a/lib/gitlab/ci/status/external/common.rb +++ b/lib/gitlab/ci/status/external/common.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + module Gitlab module Ci module Status diff --git a/lib/gitlab/ci/status/external/factory.rb b/lib/gitlab/ci/status/external/factory.rb index 07b15bd8d97..91fafb940a8 100644 --- a/lib/gitlab/ci/status/external/factory.rb +++ b/lib/gitlab/ci/status/external/factory.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + module Gitlab module Ci module Status diff --git a/lib/gitlab/ci/status/factory.rb b/lib/gitlab/ci/status/factory.rb index 15836c699c7..3446644eff8 100644 --- a/lib/gitlab/ci/status/factory.rb +++ b/lib/gitlab/ci/status/factory.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + module Gitlab module Ci module Status diff --git a/lib/gitlab/ci/status/failed.rb b/lib/gitlab/ci/status/failed.rb index 27ce85bd3ed..770ed7d4d5a 100644 --- a/lib/gitlab/ci/status/failed.rb +++ b/lib/gitlab/ci/status/failed.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + module Gitlab module Ci module Status diff --git a/lib/gitlab/ci/status/group/common.rb b/lib/gitlab/ci/status/group/common.rb index cfd4329a923..0b5ea0712ca 100644 --- a/lib/gitlab/ci/status/group/common.rb +++ b/lib/gitlab/ci/status/group/common.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + module Gitlab module Ci module Status diff --git a/lib/gitlab/ci/status/group/factory.rb b/lib/gitlab/ci/status/group/factory.rb index d118116cfc3..ee785856fdd 100644 --- a/lib/gitlab/ci/status/group/factory.rb +++ b/lib/gitlab/ci/status/group/factory.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + module Gitlab module Ci module Status diff --git a/lib/gitlab/ci/status/manual.rb b/lib/gitlab/ci/status/manual.rb index fc387e2fd25..50c92add400 100644 --- a/lib/gitlab/ci/status/manual.rb +++ b/lib/gitlab/ci/status/manual.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + module Gitlab module Ci module Status diff --git a/lib/gitlab/ci/status/pending.rb b/lib/gitlab/ci/status/pending.rb index 6780780db32..cea7e6ed938 100644 --- a/lib/gitlab/ci/status/pending.rb +++ b/lib/gitlab/ci/status/pending.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + module Gitlab module Ci module Status diff --git a/lib/gitlab/ci/status/pipeline/blocked.rb b/lib/gitlab/ci/status/pipeline/blocked.rb index bf7e484ee9b..ed13a439be0 100644 --- a/lib/gitlab/ci/status/pipeline/blocked.rb +++ b/lib/gitlab/ci/status/pipeline/blocked.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + module Gitlab module Ci module Status diff --git a/lib/gitlab/ci/status/pipeline/common.rb b/lib/gitlab/ci/status/pipeline/common.rb index 61bb07beb0f..7b34a2ea858 100644 --- a/lib/gitlab/ci/status/pipeline/common.rb +++ b/lib/gitlab/ci/status/pipeline/common.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + module Gitlab module Ci module Status diff --git a/lib/gitlab/ci/status/pipeline/delayed.rb b/lib/gitlab/ci/status/pipeline/delayed.rb index 12736861c89..e61acdcd167 100644 --- a/lib/gitlab/ci/status/pipeline/delayed.rb +++ b/lib/gitlab/ci/status/pipeline/delayed.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + module Gitlab module Ci module Status diff --git a/lib/gitlab/ci/status/pipeline/factory.rb b/lib/gitlab/ci/status/pipeline/factory.rb index 0adf83fa197..5d1a8bbd924 100644 --- a/lib/gitlab/ci/status/pipeline/factory.rb +++ b/lib/gitlab/ci/status/pipeline/factory.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + module Gitlab module Ci module Status diff --git a/lib/gitlab/ci/status/running.rb b/lib/gitlab/ci/status/running.rb index ee13905e46d..ac7dd74cdce 100644 --- a/lib/gitlab/ci/status/running.rb +++ b/lib/gitlab/ci/status/running.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + module Gitlab module Ci module Status diff --git a/lib/gitlab/ci/status/scheduled.rb b/lib/gitlab/ci/status/scheduled.rb index 3adcfa36af2..16ad1da89e3 100644 --- a/lib/gitlab/ci/status/scheduled.rb +++ b/lib/gitlab/ci/status/scheduled.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + module Gitlab module Ci module Status diff --git a/lib/gitlab/ci/status/skipped.rb b/lib/gitlab/ci/status/skipped.rb index 0dbdc4de426..aaec1e1d201 100644 --- a/lib/gitlab/ci/status/skipped.rb +++ b/lib/gitlab/ci/status/skipped.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + module Gitlab module Ci module Status diff --git a/lib/gitlab/ci/status/stage/common.rb b/lib/gitlab/ci/status/stage/common.rb index f60a7662075..f12daaa9676 100644 --- a/lib/gitlab/ci/status/stage/common.rb +++ b/lib/gitlab/ci/status/stage/common.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + module Gitlab module Ci module Status diff --git a/lib/gitlab/ci/status/stage/factory.rb b/lib/gitlab/ci/status/stage/factory.rb index 4c37f084d07..58f4642510b 100644 --- a/lib/gitlab/ci/status/stage/factory.rb +++ b/lib/gitlab/ci/status/stage/factory.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + module Gitlab module Ci module Status diff --git a/lib/gitlab/ci/status/success.rb b/lib/gitlab/ci/status/success.rb index 731013ec017..020f2c5b89f 100644 --- a/lib/gitlab/ci/status/success.rb +++ b/lib/gitlab/ci/status/success.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + module Gitlab module Ci module Status diff --git a/lib/gitlab/ci/status/success_warning.rb b/lib/gitlab/ci/status/success_warning.rb index 32b4cf43e48..6632cd9b143 100644 --- a/lib/gitlab/ci/status/success_warning.rb +++ b/lib/gitlab/ci/status/success_warning.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + module Gitlab module Ci module Status diff --git a/lib/gitlab/ci/templates/Auto-DevOps.gitlab-ci.yml b/lib/gitlab/ci/templates/Auto-DevOps.gitlab-ci.yml index db48b187e5e..c759bb7098e 100644 --- a/lib/gitlab/ci/templates/Auto-DevOps.gitlab-ci.yml +++ b/lib/gitlab/ci/templates/Auto-DevOps.gitlab-ci.yml @@ -49,7 +49,7 @@ variables: POSTGRES_ENABLED: "true" POSTGRES_DB: $CI_ENVIRONMENT_SLUG - KUBERNETES_VERSION: 1.8.6 + KUBERNETES_VERSION: 1.10.9 HELM_VERSION: 2.11.0 DOCKER_DRIVER: overlay2 @@ -116,12 +116,9 @@ code_quality: license_management: stage: test - image: docker:stable + image: "registry.gitlab.com/gitlab-org/security-products/license-management:$CI_SERVER_VERSION_MAJOR-$CI_SERVER_VERSION_MINOR-stable" allow_failure: true - services: - - docker:stable-dind script: - - setup_docker - license_management artifacts: paths: [gl-license-management-report.json] @@ -525,11 +522,7 @@ rollout 100%: } function license_management() { - # Extract "MAJOR.MINOR" from CI_SERVER_VERSION and generate "MAJOR-MINOR-stable" - LICENSE_MANAGEMENT_VERSION=$(echo "$CI_SERVER_VERSION" | sed 's/^\([0-9]*\)\.\([0-9]*\).*/\1-\2-stable/') - - docker run --volume "$PWD:/code" \ - "registry.gitlab.com/gitlab-org/security-products/license-management:$LICENSE_MANAGEMENT_VERSION" analyze /code + /run.sh analyze . } function sast() { @@ -823,7 +816,7 @@ rollout 100%: function initialize_tiller() { echo "Checking Tiller..." - export HELM_HOST=":44134" + export HELM_HOST="localhost:44134" tiller -listen ${HELM_HOST} -alsologtostderr > /dev/null 2>&1 & echo "Tiller is listening on ${HELM_HOST}" diff --git a/lib/gitlab/ci/templates/Maven.gitlab-ci.yml b/lib/gitlab/ci/templates/Maven.gitlab-ci.yml index d61ff239e13..492b3d03db2 100644 --- a/lib/gitlab/ci/templates/Maven.gitlab-ci.yml +++ b/lib/gitlab/ci/templates/Maven.gitlab-ci.yml @@ -15,7 +15,7 @@ # * Publishes the documentation for `master` branch. variables: - # This will supress any download for dependencies and plugins or upload messages which would clutter the console log. + # This will suppress any download for dependencies and plugins or upload messages which would clutter the console log. # `showDateTime` will show the passed time in milliseconds. You need to specify `--batch-mode` to make this work. MAVEN_OPTS: "-Dhttps.protocols=TLSv1.2 -Dmaven.repo.local=$CI_PROJECT_DIR/.m2/repository -Dorg.slf4j.simpleLogger.log.org.apache.maven.cli.transfer.Slf4jMavenTransferListener=WARN -Dorg.slf4j.simpleLogger.showDateTime=true -Djava.awt.headless=true" # As of Maven 3.3.0 instead of this you may define these options in `.mvn/maven.config` so the same config is used diff --git a/lib/gitlab/ci/trace/chunked_io.rb b/lib/gitlab/ci/trace/chunked_io.rb index 2147f62a84a..e9b3199d56e 100644 --- a/lib/gitlab/ci/trace/chunked_io.rb +++ b/lib/gitlab/ci/trace/chunked_io.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + ## # This class is compatible with IO class (https://ruby-doc.org/core-2.3.1/IO.html) # source: https://gitlab.com/snippets/1685610 @@ -66,8 +68,8 @@ module Gitlab end end - def read(length = nil, outbuf = "") - out = "" + def read(length = nil, outbuf = nil) + out = [] length ||= size - tell @@ -83,17 +85,18 @@ module Gitlab length -= chunk_data.bytesize end + out = out.join + # If outbuf is passed, we put the output into the buffer. This supports IO.copy_stream functionality if outbuf - outbuf.slice!(0, outbuf.bytesize) - outbuf << out + outbuf.replace(out) end out end def readline - out = "" + out = [] until eof? data = chunk_slice_from_offset @@ -109,7 +112,7 @@ module Gitlab end end - out + out.join end def write(data) diff --git a/lib/gitlab/ci/trace/section_parser.rb b/lib/gitlab/ci/trace/section_parser.rb index c09089d6475..f33f8cc56c1 100644 --- a/lib/gitlab/ci/trace/section_parser.rb +++ b/lib/gitlab/ci/trace/section_parser.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + module Gitlab module Ci class Trace diff --git a/lib/gitlab/ci/trace/stream.rb b/lib/gitlab/ci/trace/stream.rb index a71040e5e56..bd40fdf59b1 100644 --- a/lib/gitlab/ci/trace/stream.rb +++ b/lib/gitlab/ci/trace/stream.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + module Gitlab module Ci class Trace @@ -129,8 +131,7 @@ module Gitlab debris = '' until (buf = read_backward(BUFFER_SIZE)).empty? - buf += debris - debris, *lines = buf.each_line.to_a + debris, *lines = (buf + debris).each_line.to_a lines.reverse_each do |line| yield(line.force_encoding(Encoding.default_external)) end diff --git a/lib/gitlab/ci/variables/collection.rb b/lib/gitlab/ci/variables/collection.rb index ad30b3f427c..a7b4e0348c2 100644 --- a/lib/gitlab/ci/variables/collection.rb +++ b/lib/gitlab/ci/variables/collection.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + module Gitlab module Ci module Variables diff --git a/lib/gitlab/ci/variables/collection/item.rb b/lib/gitlab/ci/variables/collection/item.rb index 7da6d09d440..fdf852e8788 100644 --- a/lib/gitlab/ci/variables/collection/item.rb +++ b/lib/gitlab/ci/variables/collection/item.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + module Gitlab module Ci module Variables diff --git a/lib/gitlab/cluster/puma_worker_killer_initializer.rb b/lib/gitlab/cluster/puma_worker_killer_initializer.rb index 331c39f7d6b..4ed9a9a02ab 100644 --- a/lib/gitlab/cluster/puma_worker_killer_initializer.rb +++ b/lib/gitlab/cluster/puma_worker_killer_initializer.rb @@ -11,7 +11,11 @@ module Gitlab # Importantly RAM is for _all_workers (ie, the cluster), # not each worker as is the case with GITLAB_UNICORN_MEMORY_MAX worker_count = puma_options[:workers] || 1 - config.ram = worker_count * puma_per_worker_max_memory_mb + # The Puma Worker Killer checks the total RAM used by both the master + # and worker processes. Bump the limits to N+1 instead of N workers + # to account for this: + # https://github.com/schneems/puma_worker_killer/blob/v0.1.0/lib/puma_worker_killer/puma_memory.rb#L57 + config.ram = (worker_count + 1) * puma_per_worker_max_memory_mb config.frequency = 20 # seconds diff --git a/lib/gitlab/database/migration_helpers.rb b/lib/gitlab/database/migration_helpers.rb index a17f27a3147..f98d6dbd46f 100644 --- a/lib/gitlab/database/migration_helpers.rb +++ b/lib/gitlab/database/migration_helpers.rb @@ -879,7 +879,7 @@ module Gitlab columns(table).find { |column| column.name == name } end - # This will replace the first occurance of a string in a column with + # This will replace the first occurrence of a string in a column with # the replacement # On postgresql we can use `regexp_replace` for that. # On mysql we find the location of the pattern, and overwrite it diff --git a/lib/gitlab/diff/file_collection/base.rb b/lib/gitlab/diff/file_collection/base.rb index b79ff771a2b..2ad6fe8449d 100644 --- a/lib/gitlab/diff/file_collection/base.rb +++ b/lib/gitlab/diff/file_collection/base.rb @@ -17,7 +17,6 @@ module Gitlab @diffable = diffable @include_stats = diff_options.delete(:include_stats) - @diffs = diffable.raw_diffs(diff_options) @project = project @diff_options = diff_options @diff_refs = diff_refs @@ -25,8 +24,12 @@ module Gitlab @repository = project.repository end + def diffs + @diffs ||= diffable.raw_diffs(diff_options) + end + def diff_files - @diff_files ||= @diffs.decorate! { |diff| decorate_diff!(diff) } + @diff_files ||= diffs.decorate! { |diff| decorate_diff!(diff) } end def diff_file_with_old_path(old_path) diff --git a/lib/gitlab/diff/position_tracer.rb b/lib/gitlab/diff/position_tracer.rb index b68a1636814..8457e0c4cb6 100644 --- a/lib/gitlab/diff/position_tracer.rb +++ b/lib/gitlab/diff/position_tracer.rb @@ -24,7 +24,7 @@ module Gitlab # head of `feature` was commit B, resulting in the original diff A->B. # Since creation, `master` was updated to C. # Now `feature` is being updated to D, and the newly generated MR diff is C->D. - # It is possible that C and D are direct decendants of A and B respectively, + # It is possible that C and D are direct descendants of A and B respectively, # but this isn't necessarily the case as rebases and merges come into play. # # Suppose we have a diff note on the original diff A->B. Now that the MR diff --git a/lib/gitlab/file_detector.rb b/lib/gitlab/file_detector.rb index 4d89ee5a669..d6338b09e3d 100644 --- a/lib/gitlab/file_detector.rb +++ b/lib/gitlab/file_detector.rb @@ -8,7 +8,7 @@ module Gitlab module FileDetector PATTERNS = { # Project files - readme: %r{\Areadme[^/]*\z}i, + readme: %r{\A(readme|index)[^/]*\z}i, changelog: %r{\A(changelog|history|changes|news)[^/]*\z}i, license: %r{\A((un)?licen[sc]e|copying)(\.[^/]+)?\z}i, contributing: %r{\Acontributing[^/]*\z}i, diff --git a/lib/gitlab/git/repository.rb b/lib/gitlab/git/repository.rb index fcc92341c40..20cd257bb98 100644 --- a/lib/gitlab/git/repository.rb +++ b/lib/gitlab/git/repository.rb @@ -720,6 +720,26 @@ module Gitlab end end + # Fetch remote for repository + # + # remote - remote name + # ssh_auth - SSH known_hosts data and a private key to use for public-key authentication + # forced - should we use --force flag? + # no_tags - should we use --no-tags flag? + # prune - should we use --prune flag? + def fetch_remote(remote, ssh_auth: nil, forced: false, no_tags: false, prune: true) + wrapped_gitaly_errors do + gitaly_repository_client.fetch_remote( + remote, + ssh_auth: ssh_auth, + forced: forced, + no_tags: no_tags, + prune: prune, + timeout: GITLAB_PROJECTS_TIMEOUT + ) + end + end + def blob_at(sha, path) Gitlab::Git::Blob.find(self, sha, path) unless Gitlab::Git.blank_ref?(sha) end diff --git a/lib/gitlab/gitaly_client/repository_service.rb b/lib/gitlab/gitaly_client/repository_service.rb index 2956ed4b911..d7b36946b65 100644 --- a/lib/gitlab/gitaly_client/repository_service.rb +++ b/lib/gitlab/gitaly_client/repository_service.rb @@ -349,7 +349,7 @@ module Gitlab f.write(message.data) end end - # If the file is empty means that we recieved an empty stream, we delete the file + # If the file is empty means that we received an empty stream, we delete the file FileUtils.rm(save_path) if File.zero?(save_path) end diff --git a/lib/gitlab/import/merge_request_helpers.rb b/lib/gitlab/import/merge_request_helpers.rb index 97dc1a987c4..9215067d973 100644 --- a/lib/gitlab/import/merge_request_helpers.rb +++ b/lib/gitlab/import/merge_request_helpers.rb @@ -22,7 +22,7 @@ module Gitlab # additional work that is strictly necessary. merge_request_id = insert_and_return_id(attributes, project.merge_requests) - merge_request = project.merge_requests.find(merge_request_id) + merge_request = project.merge_requests.reload.find(merge_request_id) # We use .insert_and_return_id which effectively disables all callbacks. # Trigger iid logic here to make sure we track internal id values consistently. diff --git a/lib/gitlab/import_export/import_export.yml b/lib/gitlab/import_export/import_export.yml index 2bed470514b..9790818ecaf 100644 --- a/lib/gitlab/import_export/import_export.yml +++ b/lib/gitlab/import_export/import_export.yml @@ -92,6 +92,7 @@ excluded_attributes: - :path - :namespace_id - :creator_id + - :pool_repository_id - :import_url - :import_status - :avatar diff --git a/lib/gitlab/import_export/project_tree_restorer.rb b/lib/gitlab/import_export/project_tree_restorer.rb index 3d693d23c99..99581eb0416 100644 --- a/lib/gitlab/import_export/project_tree_restorer.rb +++ b/lib/gitlab/import_export/project_tree_restorer.rb @@ -154,7 +154,7 @@ module Gitlab Project.transaction do process_sub_relation(relation, relation_item) - # For every subrelation that hangs from Project, save the associated records alltogether + # For every subrelation that hangs from Project, save the associated records altogether # This effectively batches all records per subrelation item, only keeping those in memory # We have to keep in mind that more batch granularity << Memory, but >> Slowness if save diff --git a/lib/gitlab/kubernetes/helm.rb b/lib/gitlab/kubernetes/helm.rb index 4a1bdf34c3e..1cd4f9e17b7 100644 --- a/lib/gitlab/kubernetes/helm.rb +++ b/lib/gitlab/kubernetes/helm.rb @@ -2,6 +2,7 @@ module Gitlab module Kubernetes module Helm HELM_VERSION = '2.7.2'.freeze + KUBECTL_VERSION = '1.11.0'.freeze NAMESPACE = 'gitlab-managed-apps'.freeze SERVICE_ACCOUNT = 'tiller'.freeze CLUSTER_ROLE_BINDING = 'tiller-admin'.freeze diff --git a/lib/gitlab/kubernetes/helm/base_command.rb b/lib/gitlab/kubernetes/helm/base_command.rb index 6752f2cff43..008cba9d33c 100644 --- a/lib/gitlab/kubernetes/helm/base_command.rb +++ b/lib/gitlab/kubernetes/helm/base_command.rb @@ -11,12 +11,6 @@ module Gitlab def generate_script <<~HEREDOC set -eo pipefail - ALPINE_VERSION=$(cat /etc/alpine-release | cut -d '.' -f 1,2) - echo http://mirror.clarkson.edu/alpine/v$ALPINE_VERSION/main >> /etc/apk/repositories - echo http://mirror1.hs-esslingen.de/pub/Mirrors/alpine/v$ALPINE_VERSION/main >> /etc/apk/repositories - apk add -U wget ca-certificates openssl >/dev/null - wget -q -O - https://kubernetes-helm.storage.googleapis.com/helm-v#{Gitlab::Kubernetes::Helm::HELM_VERSION}-linux-amd64.tar.gz | tar zxC /tmp >/dev/null - mv /tmp/linux-amd64/helm /usr/bin/ HEREDOC end diff --git a/lib/gitlab/kubernetes/helm/pod.rb b/lib/gitlab/kubernetes/helm/pod.rb index 95192b11c0d..e9c621d96f0 100644 --- a/lib/gitlab/kubernetes/helm/pod.rb +++ b/lib/gitlab/kubernetes/helm/pod.rb @@ -25,7 +25,7 @@ module Gitlab def container_specification { name: 'helm', - image: 'alpine:3.6', + image: "registry.gitlab.com/gitlab-org/cluster-integration/helm-install-image/releases/#{Gitlab::Kubernetes::Helm::HELM_VERSION}-kube-#{Gitlab::Kubernetes::Helm::KUBECTL_VERSION}", env: generate_pod_env(command), command: %w(/bin/sh), args: %w(-c $(COMMAND_SCRIPT)) diff --git a/lib/gitlab/kubernetes/role_binding.rb b/lib/gitlab/kubernetes/role_binding.rb index 4f3ee040bf2..cb0cb42d007 100644 --- a/lib/gitlab/kubernetes/role_binding.rb +++ b/lib/gitlab/kubernetes/role_binding.rb @@ -3,9 +3,8 @@ module Gitlab module Kubernetes class RoleBinding - attr_reader :role_name, :namespace, :service_account_name - - def initialize(role_name:, namespace:, service_account_name:) + def initialize(name:, role_name:, namespace:, service_account_name:) + @name = name @role_name = role_name @namespace = namespace @service_account_name = service_account_name @@ -21,14 +20,16 @@ module Gitlab private + attr_reader :name, :role_name, :namespace, :service_account_name + def metadata - { name: "gitlab-#{namespace}", namespace: namespace } + { name: name, namespace: namespace } end def role_ref { apiGroup: 'rbac.authorization.k8s.io', - kind: 'Role', + kind: 'ClusterRole', name: role_name } end diff --git a/lib/gitlab/markup_helper.rb b/lib/gitlab/markup_helper.rb index 142b7d1a472..d419fa66e57 100644 --- a/lib/gitlab/markup_helper.rb +++ b/lib/gitlab/markup_helper.rb @@ -4,10 +4,11 @@ module Gitlab module MarkupHelper extend self - MARKDOWN_EXTENSIONS = %w(mdown mkd mkdn md markdown).freeze - ASCIIDOC_EXTENSIONS = %w(adoc ad asciidoc).freeze - OTHER_EXTENSIONS = %w(textile rdoc org creole wiki mediawiki rst).freeze + MARKDOWN_EXTENSIONS = %w[mdown mkd mkdn md markdown].freeze + ASCIIDOC_EXTENSIONS = %w[adoc ad asciidoc].freeze + OTHER_EXTENSIONS = %w[textile rdoc org creole wiki mediawiki rst].freeze EXTENSIONS = MARKDOWN_EXTENSIONS + ASCIIDOC_EXTENSIONS + OTHER_EXTENSIONS + PLAIN_FILENAMES = %w[readme index].freeze # Public: Determines if a given filename is compatible with GitHub::Markup. # @@ -43,7 +44,7 @@ module Gitlab # # Returns boolean def plain?(filename) - extension(filename) == 'txt' || filename.casecmp('readme').zero? + extension(filename) == 'txt' || plain_filename?(filename) end def previewable?(filename) @@ -55,5 +56,9 @@ module Gitlab def extension(filename) File.extname(filename).downcase.delete('.') end + + def plain_filename?(filename) + PLAIN_FILENAMES.include?(filename.downcase) + end end end diff --git a/lib/gitlab/path_regex.rb b/lib/gitlab/path_regex.rb index 44025650de0..fa68dead80b 100644 --- a/lib/gitlab/path_regex.rb +++ b/lib/gitlab/path_regex.rb @@ -236,7 +236,7 @@ module Gitlab def single_line_regexp(regex) # Turns a multiline extended regexp into a single line one, - # beacuse `rake routes` breaks on multiline regexes. + # because `rake routes` breaks on multiline regexes. Regexp.new(regex.source.gsub(/\(\?#.+?\)/, '').gsub(/\s*/, ''), regex.options ^ Regexp::EXTENDED).freeze end end diff --git a/lib/gitlab/proxy_http_connection_adapter.rb b/lib/gitlab/proxy_http_connection_adapter.rb index 82213098672..a64cb47e77e 100644 --- a/lib/gitlab/proxy_http_connection_adapter.rb +++ b/lib/gitlab/proxy_http_connection_adapter.rb @@ -4,7 +4,7 @@ # of the global setting allow_local_requests_from_hooks_and_services this adapter # will allow/block connection to internal IPs and/or urls. # -# This functionality can be overriden by providing the setting the option +# This functionality can be overridden by providing the setting the option # allow_local_requests = true in the request. For example: # Gitlab::HTTP.get('http://www.gitlab.com', allow_local_requests: true) # diff --git a/lib/gitlab/shell.rb b/lib/gitlab/shell.rb index 16c1edb2f11..c6a6fb9b5ce 100644 --- a/lib/gitlab/shell.rb +++ b/lib/gitlab/shell.rb @@ -108,23 +108,6 @@ module Gitlab success end - # Fetch remote for repository - # - # repository - an instance of Git::Repository - # remote - remote name - # ssh_auth - SSH known_hosts data and a private key to use for public-key authentication - # forced - should we use --force flag? - # no_tags - should we use --no-tags flag? - # - # Ex. - # fetch_remote(my_repo, "upstream") - # - def fetch_remote(repository, remote, ssh_auth: nil, forced: false, no_tags: false, prune: true) - wrapped_gitaly_errors do - repository.gitaly_repository_client.fetch_remote(remote, ssh_auth: ssh_auth, forced: forced, no_tags: no_tags, timeout: git_timeout, prune: prune) - end - end - # Move repository reroutes to mv_directory which is an alias for # mv_namespace. Given the underlying implementation is a move action, # indescriminate of what the folders might be. diff --git a/lib/gitlab/slash_commands/issue_new.rb b/lib/gitlab/slash_commands/issue_new.rb index 25f965e843d..6396b828dc7 100644 --- a/lib/gitlab/slash_commands/issue_new.rb +++ b/lib/gitlab/slash_commands/issue_new.rb @@ -3,7 +3,7 @@ module Gitlab class IssueNew < IssueCommand def self.match(text) # we can not match \n with the dot by passing the m modifier as than - # the title and description are not seperated + # the title and description are not separated /\Aissue\s+(new|create)\s+(?<title>[^\n]*)\n*(?<description>(.|\n)*)/.match(text) end diff --git a/lib/gitlab/user_extractor.rb b/lib/gitlab/user_extractor.rb index bd0d24e4369..874599688bb 100644 --- a/lib/gitlab/user_extractor.rb +++ b/lib/gitlab/user_extractor.rb @@ -1,7 +1,7 @@ # frozen_string_literal: true # This class extracts all users found in a piece of text by the username or the -# email adress +# email address module Gitlab class UserExtractor @@ -14,13 +14,11 @@ module Gitlab @text = text end - # rubocop: disable CodeReuse/ActiveRecord def users return User.none unless @text.present? @users ||= User.from_union(union_relations) end - # rubocop: enable CodeReuse/ActiveRecord def usernames matches[:usernames] diff --git a/lib/google_api/auth.rb b/lib/google_api/auth.rb index e724e58e9ca..56f056fd869 100644 --- a/lib/google_api/auth.rb +++ b/lib/google_api/auth.rb @@ -16,7 +16,7 @@ module GoogleApi client.auth_code.authorize_url( redirect_uri: redirect_uri, scope: scope, - state: state # This is used for arbitary redirection + state: state # This is used for arbitrary redirection ) end diff --git a/lib/tasks/gitlab/check.rake b/lib/tasks/gitlab/check.rake index e5b5f3548e4..663bebfe71a 100644 --- a/lib/tasks/gitlab/check.rake +++ b/lib/tasks/gitlab/check.rake @@ -1,6 +1,7 @@ namespace :gitlab do desc 'GitLab | Check the configuration of GitLab and its environment' task check: %w{gitlab:gitlab_shell:check + gitlab:gitaly:check gitlab:sidekiq:check gitlab:incoming_email:check gitlab:ldap:check @@ -44,13 +45,7 @@ namespace :gitlab do start_checking "GitLab Shell" check_gitlab_shell - Gitlab::GitalyClient::StorageSettings.allow_disk_access do - check_repo_base_exists - check_repo_base_is_not_symlink - check_repo_base_user_and_group - check_repo_base_permissions - check_repos_hooks_directory_is_link - end + check_repos_hooks_directory_is_link check_gitlab_shell_self_test finished_checking "GitLab Shell" @@ -59,116 +54,6 @@ namespace :gitlab do # Checks ######################## - def check_repo_base_exists - puts "Repo base directory exists?" - - Gitlab.config.repositories.storages.each do |name, repository_storage| - repo_base_path = repository_storage.legacy_disk_path - print "#{name}... " - - if File.exist?(repo_base_path) - puts "yes".color(:green) - else - puts "no".color(:red) - puts "#{repo_base_path} is missing".color(:red) - try_fixing_it( - "This should have been created when setting up GitLab Shell.", - "Make sure it's set correctly in config/gitlab.yml", - "Make sure GitLab Shell is installed correctly." - ) - for_more_information( - see_installation_guide_section "GitLab Shell" - ) - fix_and_rerun - end - end - end - - def check_repo_base_is_not_symlink - puts "Repo storage directories are symlinks?" - - Gitlab.config.repositories.storages.each do |name, repository_storage| - repo_base_path = repository_storage.legacy_disk_path - print "#{name}... " - - unless File.exist?(repo_base_path) - puts "can't check because of previous errors".color(:magenta) - break - end - - unless File.symlink?(repo_base_path) - puts "no".color(:green) - else - puts "yes".color(:red) - try_fixing_it( - "Make sure it's set to the real directory in config/gitlab.yml" - ) - fix_and_rerun - end - end - end - - def check_repo_base_permissions - puts "Repo paths access is drwxrws---?" - - Gitlab.config.repositories.storages.each do |name, repository_storage| - repo_base_path = repository_storage.legacy_disk_path - print "#{name}... " - - unless File.exist?(repo_base_path) - puts "can't check because of previous errors".color(:magenta) - break - end - - if File.stat(repo_base_path).mode.to_s(8).ends_with?("2770") - puts "yes".color(:green) - else - puts "no".color(:red) - try_fixing_it( - "sudo chmod -R ug+rwX,o-rwx #{repo_base_path}", - "sudo chmod -R ug-s #{repo_base_path}", - "sudo find #{repo_base_path} -type d -print0 | sudo xargs -0 chmod g+s" - ) - for_more_information( - see_installation_guide_section "GitLab Shell" - ) - fix_and_rerun - end - end - end - - def check_repo_base_user_and_group - gitlab_shell_ssh_user = Gitlab.config.gitlab_shell.ssh_user - puts "Repo paths owned by #{gitlab_shell_ssh_user}:root, or #{gitlab_shell_ssh_user}:#{Gitlab.config.gitlab_shell.owner_group}?" - - Gitlab.config.repositories.storages.each do |name, repository_storage| - repo_base_path = repository_storage.legacy_disk_path - print "#{name}... " - - unless File.exist?(repo_base_path) - puts "can't check because of previous errors".color(:magenta) - break - end - - user_id = uid_for(gitlab_shell_ssh_user) - root_group_id = gid_for('root') - group_ids = [root_group_id, gid_for(Gitlab.config.gitlab_shell.owner_group)] - if File.stat(repo_base_path).uid == user_id && group_ids.include?(File.stat(repo_base_path).gid) - puts "yes".color(:green) - else - puts "no".color(:red) - puts " User id for #{gitlab_shell_ssh_user}: #{user_id}. Groupd id for root: #{root_group_id}".color(:blue) - try_fixing_it( - "sudo chown -R #{gitlab_shell_ssh_user}:root #{repo_base_path}" - ) - for_more_information( - see_installation_guide_section "GitLab Shell" - ) - fix_and_rerun - end - end - end - def check_repos_hooks_directory_is_link print "hooks directories in repos are links: ... " @@ -247,6 +132,26 @@ namespace :gitlab do end end + namespace :gitaly do + desc 'GitLab | Check the health of Gitaly' + task check: :gitlab_environment do + warn_user_is_not_gitlab + start_checking 'Gitaly' + + Gitlab::HealthChecks::GitalyCheck.readiness.each do |result| + print "#{result.labels[:shard]} ... " + + if result.success + puts 'OK'.color(:green) + else + puts "FAIL: #{result.message}".color(:red) + end + end + + finished_checking 'Gitaly' + end + end + namespace :sidekiq do desc "GitLab | Check the configuration of Sidekiq" task check: :gitlab_environment do diff --git a/locale/gitlab.pot b/locale/gitlab.pot index 324e5315821..3e35a5dbdf4 100644 --- a/locale/gitlab.pot +++ b/locale/gitlab.pot @@ -1583,7 +1583,7 @@ msgstr "" msgid "ClusterIntegration|Prometheus is an open-source monitoring system with %{gitlabIntegrationLink} to monitor deployed applications." msgstr "" -msgid "ClusterIntegration|RBAC-enabled cluster (experimental)" +msgid "ClusterIntegration|RBAC-enabled cluster" msgstr "" msgid "ClusterIntegration|Read our %{link_to_help_page} on Kubernetes cluster integration." @@ -1655,9 +1655,6 @@ msgstr "" msgid "ClusterIntegration|The IP address is in the process of being assigned. Please check your Kubernetes cluster or Quotas on Google Kubernetes Engine if it takes a long time." msgstr "" -msgid "ClusterIntegration|The default cluster configuration grants access to many functionalities needed to successfully build and deploy a containerised application." -msgstr "" - msgid "ClusterIntegration|This account must have permissions to create a Kubernetes cluster in the %{link_to_container_project} specified below" msgstr "" @@ -2172,7 +2169,7 @@ msgstr "" msgid "Define a custom pattern with cron syntax" msgstr "" -msgid "DelayedJobs|Are you sure you want to run %{jobName} immediately? This job will run automatically after it's timer finishes." +msgid "DelayedJobs|Are you sure you want to run %{jobName} immediately? Otherwise this job will run automatically after it's timer finishes." msgstr "" msgid "DelayedJobs|Are you sure you want to run %{job_name} immediately? This job will run automatically after it's timer finishes." @@ -4165,6 +4162,9 @@ msgstr "" msgid "Notes|Show comments only" msgstr "" +msgid "Notes|Show history only" +msgstr "" + msgid "Notification events" msgstr "" @@ -4641,9 +4641,6 @@ msgstr "" msgid "Profiles| You are going to change the username %{currentUsernameBold} to %{newUsernameBold}. Profile and projects will be redirected to the %{newUsername} namespace but this redirect will expire once the %{currentUsername} namespace is registered by another user or group. Please update your Git repository remotes as soon as possible." msgstr "" -msgid "Profiles|%{author_name} made a private contribution" -msgstr "" - msgid "Profiles|Account scheduled for removal." msgstr "" @@ -4704,6 +4701,9 @@ msgstr "" msgid "Profiles|Invalid username" msgstr "" +msgid "Profiles|Made a private contribution" +msgstr "" + msgid "Profiles|Main settings" msgstr "" @@ -5847,6 +5847,9 @@ msgstr "" msgid "Status" msgstr "" +msgid "Stop environment" +msgstr "" + msgid "Stop impersonation" msgstr "" @@ -5856,6 +5859,9 @@ msgstr "" msgid "Stopped" msgstr "" +msgid "Stopping this environment is currently not possible as a deployment is in progress" +msgstr "" + msgid "Storage" msgstr "" diff --git a/package.json b/package.json index 608d4e58dd3..2d6479fea3f 100644 --- a/package.json +++ b/package.json @@ -24,8 +24,8 @@ "@babel/plugin-syntax-dynamic-import": "^7.0.0", "@babel/plugin-syntax-import-meta": "^7.0.0", "@babel/preset-env": "^7.1.0", - "@gitlab-org/gitlab-svgs": "^1.33.0", "@gitlab-org/gitlab-ui": "^1.10.0", + "@gitlab/svgs": "^1.35.0", "autosize": "^4.0.0", "axios": "^0.17.1", "babel-loader": "^8.0.4", @@ -36,42 +36,40 @@ module QA ## # GitLab QA fabrication mechanisms # - module Factory - autoload :ApiFabricator, 'qa/factory/api_fabricator' - autoload :Base, 'qa/factory/base' - - module Resource - autoload :Sandbox, 'qa/factory/resource/sandbox' - autoload :Group, 'qa/factory/resource/group' - autoload :Issue, 'qa/factory/resource/issue' - autoload :Project, 'qa/factory/resource/project' - autoload :Label, 'qa/factory/resource/label' - autoload :MergeRequest, 'qa/factory/resource/merge_request' - autoload :ProjectImportedFromGithub, 'qa/factory/resource/project_imported_from_github' - autoload :MergeRequestFromFork, 'qa/factory/resource/merge_request_from_fork' - autoload :DeployKey, 'qa/factory/resource/deploy_key' - autoload :DeployToken, 'qa/factory/resource/deploy_token' - autoload :Branch, 'qa/factory/resource/branch' - autoload :CiVariable, 'qa/factory/resource/ci_variable' - autoload :Runner, 'qa/factory/resource/runner' - autoload :PersonalAccessToken, 'qa/factory/resource/personal_access_token' - autoload :KubernetesCluster, 'qa/factory/resource/kubernetes_cluster' - autoload :User, 'qa/factory/resource/user' - autoload :ProjectMilestone, 'qa/factory/resource/project_milestone' - autoload :Wiki, 'qa/factory/resource/wiki' - autoload :File, 'qa/factory/resource/file' - autoload :Fork, 'qa/factory/resource/fork' - autoload :SSHKey, 'qa/factory/resource/ssh_key' - end + module Resource + autoload :ApiFabricator, 'qa/resource/api_fabricator' + autoload :Base, 'qa/resource/base' + + autoload :Sandbox, 'qa/resource/sandbox' + autoload :Group, 'qa/resource/group' + autoload :Issue, 'qa/resource/issue' + autoload :Project, 'qa/resource/project' + autoload :Label, 'qa/resource/label' + autoload :MergeRequest, 'qa/resource/merge_request' + autoload :ProjectImportedFromGithub, 'qa/resource/project_imported_from_github' + autoload :MergeRequestFromFork, 'qa/resource/merge_request_from_fork' + autoload :DeployKey, 'qa/resource/deploy_key' + autoload :DeployToken, 'qa/resource/deploy_token' + autoload :Branch, 'qa/resource/branch' + autoload :CiVariable, 'qa/resource/ci_variable' + autoload :Runner, 'qa/resource/runner' + autoload :PersonalAccessToken, 'qa/resource/personal_access_token' + autoload :KubernetesCluster, 'qa/resource/kubernetes_cluster' + autoload :User, 'qa/resource/user' + autoload :ProjectMilestone, 'qa/resource/project_milestone' + autoload :Wiki, 'qa/resource/wiki' + autoload :File, 'qa/resource/file' + autoload :Fork, 'qa/resource/fork' + autoload :SSHKey, 'qa/resource/ssh_key' module Repository - autoload :Push, 'qa/factory/repository/push' - autoload :ProjectPush, 'qa/factory/repository/project_push' - autoload :WikiPush, 'qa/factory/repository/wiki_push' + autoload :Push, 'qa/resource/repository/push' + autoload :ProjectPush, 'qa/resource/repository/project_push' + autoload :WikiPush, 'qa/resource/repository/wiki_push' end module Settings - autoload :HashedStorage, 'qa/factory/settings/hashed_storage' + autoload :HashedStorage, 'qa/resource/settings/hashed_storage' end end diff --git a/qa/qa/factory/README.md b/qa/qa/factory/README.md deleted file mode 100644 index 42077f60611..00000000000 --- a/qa/qa/factory/README.md +++ /dev/null @@ -1,410 +0,0 @@ -# Factory objects in GitLab QA - -In GitLab QA we are using factories to create resources. - -Factories implementation are primarily done using Browser UI steps, but can also -be done via the API. - -## Why do we need that? - -We need factory objects because we need to reduce duplication when creating -resources for our QA tests. - -## How to properly implement a factory object? - -All factories should inherit from [`Factory::Base`](./base.rb). - -There is only one mandatory method to implement to define a factory. This is the -`#fabricate!` method, which is used to build a resource via the browser UI. -Note that you should only use [Page objects](../page/README.md) to interact with -a Web page in this method. - -Here is an imaginary example: - -```ruby -module QA - module Factory - module Resource - class Shirt < Factory::Base - attr_accessor :name - - def fabricate! - Page::Dashboard::Index.perform do |dashboard_index| - dashboard_index.go_to_new_shirt - end - - Page::Shirt::New.perform do |shirt_new| - shirt_new.set_name(name) - shirt_new.create_shirt! - end - end - end - end - end -end -``` - -### Define API implementation - -A factory may also implement the three following methods to be able to create a -resource via the public GitLab API: - -- `#api_get_path`: The `GET` path to fetch an existing resource. -- `#api_post_path`: The `POST` path to create a new resource. -- `#api_post_body`: The `POST` body (as a Ruby hash) to create a new resource. - -Let's take the `Shirt` factory example, and add these three API methods: - -```ruby -module QA - module Factory - module Resource - class Shirt < Factory::Base - attr_accessor :name - - def fabricate! - # ... same as before - end - - def api_get_path - "/shirt/#{name}" - end - - def api_post_path - "/shirts" - end - - def api_post_body - { - name: name - } - end - end - end - end -end -``` - -The [`Project` factory](./resource/project.rb) is a good real example of Browser -UI and API implementations. - -#### Resource attributes - -A resource may need another resource to exist first. For instance, a project -needs a group to be created in. - -To define a resource attribute, you can use the `attribute` method with a -block using the other factory to fabricate the resource. - -That will allow access to the other resource from your resource object's -methods. You would usually use it in `#fabricate!`, `#api_get_path`, -`#api_post_path`, `#api_post_body`. - -Let's take the `Shirt` factory, and add a `project` attribute to it: - -```ruby -module QA - module Factory - module Resource - class Shirt < Factory::Base - attr_accessor :name - - attribute :project do - Factory::Resource::Project.fabricate! do |resource| - resource.name = 'project-to-create-a-shirt' - end - end - - def fabricate! - project.visit! - - Page::Project::Show.perform do |project_show| - project_show.go_to_new_shirt - end - - Page::Shirt::New.perform do |shirt_new| - shirt_new.set_name(name) - shirt_new.create_shirt! - end - end - - def api_get_path - "/project/#{project.path}/shirt/#{name}" - end - - def api_post_path - "/project/#{project.path}/shirts" - end - - def api_post_body - { - name: name - } - end - end - end - end -end -``` - -**Note that all the attributes are lazily constructed. This means if you want -a specific attribute to be fabricated first, you'll need to call the -attribute method first even if you're not using it.** - -#### Product data attributes - -Once created, you may want to populate a resource with attributes that can be -found in the Web page, or in the API response. -For instance, once you create a project, you may want to store its repository -SSH URL as an attribute. - -Again we could use the `attribute` method with a block, using a page object -to retrieve the data on the page. - -Let's take the `Shirt` factory, and define a `:brand` attribute: - -```ruby -module QA - module Factory - module Resource - class Shirt < Factory::Base - attr_accessor :name - - attribute :project do - Factory::Resource::Project.fabricate! do |resource| - resource.name = 'project-to-create-a-shirt' - end - end - - # Attribute populated from the Browser UI (using the block) - attribute :brand do - Page::Shirt::Show.perform do |shirt_show| - shirt_show.fetch_brand_from_page - end - end - - # ... same as before - end - end - end -end -``` - -**Note again that all the attributes are lazily constructed. This means if -you call `shirt.brand` after moving to the other page, it'll not properly -retrieve the data because we're no longer on the expected page.** - -Consider this: - -```ruby -shirt = - QA::Factory::Resource::Shirt.fabricate! do |resource| - resource.name = "GitLab QA" - end - -shirt.project.visit! - -shirt.brand # => FAIL! -``` - -The above example will fail because now we're on the project page, trying to -construct the brand data from the shirt page, however we moved to the project -page already. There are two ways to solve this, one is that we could try to -retrieve the brand before visiting the project again: - -```ruby -shirt = - QA::Factory::Resource::Shirt.fabricate! do |resource| - resource.name = "GitLab QA" - end - -shirt.brand # => OK! - -shirt.project.visit! - -shirt.brand # => OK! -``` - -The attribute will be stored in the instance therefore all the following calls -will be fine, using the data previously constructed. If we think that this -might be too brittle, we could eagerly construct the data right before -ending fabrication: - -```ruby -module QA - module Factory - module Resource - class Shirt < Factory::Base - # ... same as before - - def fabricate! - project.visit! - - Page::Project::Show.perform do |project_show| - project_show.go_to_new_shirt - end - - Page::Shirt::New.perform do |shirt_new| - shirt_new.set_name(name) - shirt_new.create_shirt! - end - - populate(:brand) # Eagerly construct the data - end - end - end - end -end -``` - -The `populate` method will iterate through its arguments and call each -attribute respectively. Here `populate(:brand)` has the same effect as -just `brand`. Using the populate method makes the intention clearer. - -With this, it will make sure we construct the data right after we create the -shirt. The drawback is that this will always construct the data when the resource is fabricated even if we don't need to use the data. - -Alternatively, we could just make sure we're on the right page before -constructing the brand data: - -```ruby -module QA - module Factory - module Resource - class Shirt < Factory::Base - attr_accessor :name - - attribute :project do - Factory::Resource::Project.fabricate! do |resource| - resource.name = 'project-to-create-a-shirt' - end - end - - # Attribute populated from the Browser UI (using the block) - attribute :brand do - back_url = current_url - visit! - - Page::Shirt::Show.perform do |shirt_show| - shirt_show.fetch_brand_from_page - end - - visit(back_url) - end - - # ... same as before - end - end - end -end -``` - -This will make sure it's on the shirt page before constructing brand, and -move back to the previous page to avoid breaking the state. - -#### Define an attribute based on an API response - -Sometimes, you want to define a resource attribute based on the API response -from its `GET` or `POST` request. For instance, if the creation of a shirt via -the API returns - -```ruby -{ - brand: 'a-brand-new-brand', - style: 't-shirt', - materials: [[:cotton, 80], [:polyamide, 20]] -} -``` - -you may want to store `style` as-is in the resource, and fetch the first value -of the first `materials` item in a `main_fabric` attribute. - -Let's take the `Shirt` factory, and define a `:style` and a `:main_fabric` -attributes: - -```ruby -module QA - module Factory - module Resource - class Shirt < Factory::Base - # ... same as before - - # Attribute from the Shirt factory if present, - # or fetched from the API response if present, - # or a QA::Factory::Base::NoValueError is raised otherwise - attribute :style - - # If the attribute from the Shirt factory is not present, - # and if the API does not contain this field, this block will be - # used to construct the value based on the API response. - attribute :main_fabric do - api_response.&dig(:materials, 0, 0) - end - - # ... same as before - end - end - end -end -``` - -**Notes on attributes precedence:** - -- factory instance variables have the highest precedence -- attributes from the API response take precedence over attributes from the - block (usually from Browser UI) -- attributes without a value will raise a `QA::Factory::Base::NoValueError` error - -## Creating resources in your tests - -To create a resource in your tests, you can call the `.fabricate!` method on the -factory class. -Note that if the factory supports API fabrication, this will use this -fabrication by default. - -Here is an example that will use the API fabrication method under the hood since -it's supported by the `Shirt` factory: - -```ruby -my_shirt = Factory::Resource::Shirt.fabricate! do |shirt| - shirt.name = 'my-shirt' -end - -expect(page).to have_text(my_shirt.name) # => "my-shirt" from the factory's attribute -expect(page).to have_text(my_shirt.brand) # => "a-brand-new-brand" from the API response -expect(page).to have_text(my_shirt.style) # => "t-shirt" from the API response -expect(page).to have_text(my_shirt.main_fabric) # => "cotton" from the API response via the block -``` - -If you explicitly want to use the Browser UI fabrication method, you can call -the `.fabricate_via_browser_ui!` method instead: - -```ruby -my_shirt = Factory::Resource::Shirt.fabricate_via_browser_ui! do |shirt| - shirt.name = 'my-shirt' -end - -expect(page).to have_text(my_shirt.name) # => "my-shirt" from the factory's attribute -expect(page).to have_text(my_shirt.brand) # => the brand name fetched from the `Page::Shirt::Show` page via the block -expect(page).to have_text(my_shirt.style) # => QA::Factory::Base::NoValueError will be raised because no API response nor a block is provided -expect(page).to have_text(my_shirt.main_fabric) # => QA::Factory::Base::NoValueError will be raised because no API response and the block didn't provide a value (because it's also based on the API response) -``` - -You can also explicitly use the API fabrication method, by calling the -`.fabricate_via_api!` method: - -```ruby -my_shirt = Factory::Resource::Shirt.fabricate_via_api! do |shirt| - shirt.name = 'my-shirt' -end -``` - -In this case, the result will be similar to calling `Factory::Resource::Shirt.fabricate!`. - -## Where to ask for help? - -If you need more information, ask for help on `#quality` channel on Slack -(internal, GitLab Team only). - -If you are not a Team Member, and you still need help to contribute, please -open an issue in GitLab CE issue tracker with the `~QA` label. diff --git a/qa/qa/factory/resource/branch.rb b/qa/qa/factory/resource/branch.rb deleted file mode 100644 index b05d1e252ec..00000000000 --- a/qa/qa/factory/resource/branch.rb +++ /dev/null @@ -1,77 +0,0 @@ -module QA - module Factory - module Resource - class Branch < Factory::Base - attr_accessor :project, :branch_name, - :allow_to_push, :allow_to_merge, :protected - - attribute :project do - Factory::Resource::Project.fabricate! do |resource| - resource.name = 'protected-branch-project' - end - end - - def initialize - @branch_name = 'test/branch' - @allow_to_push = true - @allow_to_merge = true - @protected = false - end - - def fabricate! - project.visit! - - Factory::Repository::ProjectPush.fabricate! do |resource| - resource.project = project - resource.file_name = 'kick-off.txt' - resource.commit_message = 'First commit' - end - - branch = Factory::Repository::ProjectPush.fabricate! do |resource| - resource.project = project - resource.file_name = 'README.md' - resource.commit_message = 'Add readme' - resource.branch_name = 'master' - resource.new_branch = false - resource.remote_branch = @branch_name - end - - Page::Project::Show.perform do |page| - page.wait { page.has_content?(branch_name) } - end - - # The upcoming process will make it access the Protected Branches page, - # select the already created branch and protect it according - # to `allow_to_push` variable. - return branch unless @protected - - Page::Project::Menu.perform(&:click_repository_settings) - - Page::Project::Settings::Repository.perform do |setting| - setting.expand_protected_branches do |page| - page.select_branch(branch_name) - - if allow_to_push - page.allow_devs_and_maintainers_to_push - else - page.allow_no_one_to_push - end - - if allow_to_merge - page.allow_devs_and_maintainers_to_merge - else - page.allow_no_one_to_merge - end - - page.wait(reload: false) do - !page.first('.btn-success').disabled? - end - - page.protect_branch - end - end - end - end - end - end -end diff --git a/qa/qa/factory/resource/ci_variable.rb b/qa/qa/factory/resource/ci_variable.rb deleted file mode 100644 index a0aefc61f9f..00000000000 --- a/qa/qa/factory/resource/ci_variable.rb +++ /dev/null @@ -1,30 +0,0 @@ -module QA - module Factory - module Resource - class CiVariable < Factory::Base - attr_accessor :key, :value - - attribute :project do - Factory::Resource::Project.fabricate! do |resource| - resource.name = 'project-with-ci-variables' - resource.description = 'project for adding CI variable test' - end - end - - def fabricate! - project.visit! - - Page::Project::Menu.perform(&:click_ci_cd_settings) - - Page::Project::Settings::CICD.perform do |setting| - setting.expand_ci_variables do |page| - page.fill_variable(key, value) - - page.save_variables - end - end - end - end - end - end -end diff --git a/qa/qa/factory/resource/deploy_key.rb b/qa/qa/factory/resource/deploy_key.rb deleted file mode 100644 index aea99c9f80d..00000000000 --- a/qa/qa/factory/resource/deploy_key.rb +++ /dev/null @@ -1,43 +0,0 @@ -module QA - module Factory - module Resource - class DeployKey < Factory::Base - attr_accessor :title, :key - - attribute :fingerprint do - Page::Project::Settings::Repository.perform do |setting| - setting.expand_deploy_keys do |key| - key_offset = key.key_titles.index do |key_title| - key_title.text == title - end - - key.key_fingerprints[key_offset].text - end - end - end - - attribute :project do - Factory::Resource::Project.fabricate! do |resource| - resource.name = 'project-to-deploy' - resource.description = 'project for adding deploy key test' - end - end - - def fabricate! - project.visit! - - Page::Project::Menu.perform(&:click_repository_settings) - - Page::Project::Settings::Repository.perform do |setting| - setting.expand_deploy_keys do |page| - page.fill_key_title(title) - page.fill_key_value(key) - - page.add_key - end - end - end - end - end - end -end diff --git a/qa/qa/factory/resource/deploy_token.rb b/qa/qa/factory/resource/deploy_token.rb deleted file mode 100644 index 68e98f0aa01..00000000000 --- a/qa/qa/factory/resource/deploy_token.rb +++ /dev/null @@ -1,50 +0,0 @@ -module QA - module Factory - module Resource - class DeployToken < Factory::Base - attr_accessor :name, :expires_at - - attribute :username do - Page::Project::Settings::Repository.perform do |page| - page.expand_deploy_tokens do |token| - token.token_username - end - end - end - - attribute :password do - Page::Project::Settings::Repository.perform do |page| - page.expand_deploy_tokens do |token| - token.token_password - end - end - end - - attribute :project do - Factory::Resource::Project.fabricate! do |resource| - resource.name = 'project-to-deploy' - resource.description = 'project for adding deploy token test' - end - end - - def fabricate! - project.visit! - - Page::Project::Menu.act do - click_repository_settings - end - - Page::Project::Settings::Repository.perform do |setting| - setting.expand_deploy_tokens do |page| - page.fill_token_name(name) - page.fill_token_expires_at(expires_at) - page.fill_scopes(read_repository: true, read_registry: false) - - page.add_token - end - end - end - end - end - end -end diff --git a/qa/qa/factory/resource/file.rb b/qa/qa/factory/resource/file.rb deleted file mode 100644 index 1148876c2d3..00000000000 --- a/qa/qa/factory/resource/file.rb +++ /dev/null @@ -1,38 +0,0 @@ -# frozen_string_literal: true - -module QA - module Factory - module Resource - class File < Factory::Base - attr_accessor :name, - :content, - :commit_message - - attribute :project do - Factory::Resource::Project.fabricate! do |resource| - resource.name = 'project-with-new-file' - end - end - - def initialize - @name = 'QA Test - File name' - @content = 'QA Test - File content' - @commit_message = 'QA Test - Commit message' - end - - def fabricate! - project.visit! - - Page::Project::Show.perform(&:create_new_file!) - - Page::File::Form.perform do |page| - page.add_name(@name) - page.add_content(@content) - page.add_commit_message(@commit_message) - page.commit_changes - end - end - end - end - end -end diff --git a/qa/qa/factory/resource/fork.rb b/qa/qa/factory/resource/fork.rb deleted file mode 100644 index b1e874af893..00000000000 --- a/qa/qa/factory/resource/fork.rb +++ /dev/null @@ -1,70 +0,0 @@ -module QA - module Factory - module Resource - class Fork < Factory::Base - attribute :push do - Factory::Repository::ProjectPush.fabricate! - end - - attribute :user do - Factory::Resource::User.fabricate! do |resource| - if Runtime::Env.forker? - resource.username = Runtime::Env.forker_username - resource.password = Runtime::Env.forker_password - end - end - end - - def visit_project_with_retry - # The user intermittently fails to stay signed in after visiting the - # project page. The new user is registered and then signs in and a - # screenshot shows that signing in was successful. Then the project - # page is visited but a screenshot shows the user is no longer signed - # in. It's difficult to reproduce locally but GDK logs don't seem to - # show anything unexpected. This method attempts to work around the - # problem and capture data to help troubleshoot. - - Capybara::Screenshot.screenshot_and_save_page - - start = Time.now - - while Time.now - start < 20 - push.project.visit! - - puts "Visited project page" - Capybara::Screenshot.screenshot_and_save_page - - return if Page::Main::Menu.act { has_personal_area?(wait: 0) } - - puts "Not signed in. Attempting to sign in again." - Capybara::Screenshot.screenshot_and_save_page - - Runtime::Browser.visit(:gitlab, Page::Main::Login) - - Page::Main::Login.perform do |login| - login.sign_in_using_credentials(user) - end - end - - raise "Failed to load project page and stay logged in" - end - - def fabricate! - populate(:push, :user) - - visit_project_with_retry - - Page::Project::Show.perform(&:fork_project) - - Page::Project::Fork::New.perform do |fork_new| - fork_new.choose_namespace(user.name) - end - - Page::Layout::Banner.perform do |page| - page.has_notice?('The project was successfully forked.') - end - end - end - end - end -end diff --git a/qa/qa/factory/resource/group.rb b/qa/qa/factory/resource/group.rb deleted file mode 100644 index 45e49da86f9..00000000000 --- a/qa/qa/factory/resource/group.rb +++ /dev/null @@ -1,68 +0,0 @@ -module QA - module Factory - module Resource - class Group < Factory::Base - attr_accessor :path, :description - - attribute :sandbox do - Factory::Resource::Sandbox.fabricate! - end - - attribute :id - - def initialize - @path = Runtime::Namespace.name - @description = "QA test run at #{Runtime::Namespace.time}" - end - - def fabricate! - sandbox.visit! - - Page::Group::Show.perform do |group_show| - if group_show.has_subgroup?(path) - group_show.go_to_subgroup(path) - else - group_show.go_to_new_subgroup - - Page::Group::New.perform do |group_new| - group_new.set_path(path) - group_new.set_description(description) - group_new.set_visibility('Public') - group_new.create - end - - # Ensure that the group was actually created - group_show.wait(time: 1) do - group_show.has_text?(path) && - group_show.has_new_project_or_subgroup_dropdown? - end - end - end - end - - def fabricate_via_api! - resource_web_url(api_get) - rescue ResourceNotFoundError - super - end - - def api_get_path - "/groups/#{CGI.escape("#{sandbox.path}/#{path}")}" - end - - def api_post_path - '/groups' - end - - def api_post_body - { - parent_id: sandbox.id, - path: path, - name: path, - visibility: 'public' - } - end - end - end - end -end diff --git a/qa/qa/factory/resource/issue.rb b/qa/qa/factory/resource/issue.rb deleted file mode 100644 index 3a28e0d5aa6..00000000000 --- a/qa/qa/factory/resource/issue.rb +++ /dev/null @@ -1,30 +0,0 @@ -module QA - module Factory - module Resource - class Issue < Factory::Base - attr_writer :description - - attribute :project do - Factory::Resource::Project.fabricate! do |resource| - resource.name = 'project-for-issues' - resource.description = 'project for adding issues' - end - end - - attribute :title - - def fabricate! - project.visit! - - Page::Project::Show.perform(&:go_to_new_issue) - - Page::Project::Issue::New.perform do |page| - page.add_title(@title) - page.add_description(@description) - page.create_new_issue - end - end - end - end - end -end diff --git a/qa/qa/factory/resource/kubernetes_cluster.rb b/qa/qa/factory/resource/kubernetes_cluster.rb deleted file mode 100644 index aac6864f42f..00000000000 --- a/qa/qa/factory/resource/kubernetes_cluster.rb +++ /dev/null @@ -1,57 +0,0 @@ -require 'securerandom' - -module QA - module Factory - module Resource - class KubernetesCluster < Factory::Base - attr_writer :project, :cluster, - :install_helm_tiller, :install_ingress, :install_prometheus, :install_runner - - attribute :ingress_ip do - Page::Project::Operations::Kubernetes::Show.perform(&:ingress_ip) - end - - def fabricate! - @project.visit! - - Page::Project::Menu.perform( - &:click_operations_kubernetes) - - Page::Project::Operations::Kubernetes::Index.perform( - &:add_kubernetes_cluster) - - Page::Project::Operations::Kubernetes::Add.perform( - &:add_existing_cluster) - - Page::Project::Operations::Kubernetes::AddExisting.perform do |page| - page.set_cluster_name(@cluster.cluster_name) - page.set_api_url(@cluster.api_url) - page.set_ca_certificate(@cluster.ca_certificate) - page.set_token(@cluster.token) - page.check_rbac! if @cluster.rbac - page.add_cluster! - end - - if @install_helm_tiller - Page::Project::Operations::Kubernetes::Show.perform do |page| - # We must wait a few seconds for permissions to be set up correctly for new cluster - sleep 10 - - # Helm must be installed before everything else - page.install!(:helm) - page.await_installed(:helm) - - page.install!(:ingress) if @install_ingress - page.install!(:prometheus) if @install_prometheus - page.install!(:runner) if @install_runner - - page.await_installed(:ingress) if @install_ingress - page.await_installed(:prometheus) if @install_prometheus - page.await_installed(:runner) if @install_runner - end - end - end - end - end - end -end diff --git a/qa/qa/factory/resource/label.rb b/qa/qa/factory/resource/label.rb deleted file mode 100644 index 32bc519b48c..00000000000 --- a/qa/qa/factory/resource/label.rb +++ /dev/null @@ -1,39 +0,0 @@ -require 'securerandom' - -module QA - module Factory - module Resource - class Label < Factory::Base - attr_accessor :description, :color - - attribute :title - - attribute :project do - Factory::Resource::Project.fabricate! do |resource| - resource.name = 'project-with-label' - end - end - - def initialize - @title = "qa-test-#{SecureRandom.hex(8)}" - @description = 'This is a test label' - @color = '#0033CC' - end - - def fabricate! - project.visit! - - Page::Project::Menu.perform(&:go_to_labels) - Page::Label::Index.perform(&:go_to_new_label) - - Page::Label::New.perform do |page| - page.fill_title(@title) - page.fill_description(@description) - page.fill_color(@color) - page.create_label - end - end - end - end - end -end diff --git a/qa/qa/factory/resource/merge_request.rb b/qa/qa/factory/resource/merge_request.rb deleted file mode 100644 index 4b7d2287f98..00000000000 --- a/qa/qa/factory/resource/merge_request.rb +++ /dev/null @@ -1,71 +0,0 @@ -require 'securerandom' - -module QA - module Factory - module Resource - class MergeRequest < Factory::Base - attr_accessor :title, - :description, - :source_branch, - :target_branch, - :assignee, - :milestone, - :labels - - attribute :project do - Factory::Resource::Project.fabricate! do |resource| - resource.name = 'project-with-merge-request' - end - end - - attribute :target do - project.visit! - - Factory::Repository::ProjectPush.fabricate! do |resource| - resource.project = project - resource.branch_name = 'master' - resource.remote_branch = target_branch - end - end - - attribute :source do - Factory::Repository::ProjectPush.fabricate! do |resource| - resource.project = project - resource.branch_name = target_branch - resource.remote_branch = source_branch - resource.new_branch = false - resource.file_name = "added_file.txt" - resource.file_content = "File Added" - end - end - - def initialize - @title = 'QA test - merge request' - @description = 'This is a test merge request' - @source_branch = "qa-test-feature-#{SecureRandom.hex(8)}" - @target_branch = "master" - @assignee = nil - @milestone = nil - @labels = [] - end - - def fabricate! - populate(:target, :source) - - project.visit! - Page::Project::Show.perform(&:new_merge_request) - Page::MergeRequest::New.perform do |page| - page.fill_title(@title) - page.fill_description(@description) - page.choose_milestone(@milestone) if @milestone - labels.each do |label| - page.select_label(label) - end - - page.create_merge_request - end - end - end - end - end -end diff --git a/qa/qa/factory/resource/merge_request_from_fork.rb b/qa/qa/factory/resource/merge_request_from_fork.rb deleted file mode 100644 index 1311bf625a6..00000000000 --- a/qa/qa/factory/resource/merge_request_from_fork.rb +++ /dev/null @@ -1,31 +0,0 @@ -module QA - module Factory - module Resource - class MergeRequestFromFork < MergeRequest - attr_accessor :fork_branch - - attribute :fork do - Factory::Resource::Fork.fabricate! - end - - attribute :push do - Factory::Repository::ProjectPush.fabricate! do |resource| - resource.project = fork - resource.branch_name = fork_branch - resource.file_name = 'file2.txt' - resource.user = fork.user - end - end - - def fabricate! - populate(:push) - - fork.visit! - - Page::Project::Show.perform(&:new_merge_request) - Page::MergeRequest::New.perform(&:create_merge_request) - end - end - end - end -end diff --git a/qa/qa/factory/resource/personal_access_token.rb b/qa/qa/factory/resource/personal_access_token.rb deleted file mode 100644 index ceb0f1c3d75..00000000000 --- a/qa/qa/factory/resource/personal_access_token.rb +++ /dev/null @@ -1,27 +0,0 @@ -module QA - module Factory - module Resource - ## - # Create a personal access token that can be used by the api - # - class PersonalAccessToken < Factory::Base - attr_accessor :name - - attribute :access_token do - Page::Profile::PersonalAccessTokens.perform(&:created_access_token) - end - - def fabricate! - Page::Main::Menu.perform(&:go_to_profile_settings) - Page::Profile::Menu.perform(&:click_access_tokens) - - Page::Profile::PersonalAccessTokens.perform do |page| - page.fill_token_name(name || 'api-test-token') - page.check_api - page.create_token - end - end - end - end - end -end diff --git a/qa/qa/factory/resource/project.rb b/qa/qa/factory/resource/project.rb deleted file mode 100644 index f691ae5a342..00000000000 --- a/qa/qa/factory/resource/project.rb +++ /dev/null @@ -1,78 +0,0 @@ -require 'securerandom' - -module QA - module Factory - module Resource - class Project < Factory::Base - attribute :name - attribute :description - - attribute :group do - Factory::Resource::Group.fabricate! - end - - attribute :repository_ssh_location do - Page::Project::Show.perform do |page| - page.choose_repository_clone_ssh - page.repository_location - end - end - - attribute :repository_http_location do - Page::Project::Show.perform do |page| - page.choose_repository_clone_http - page.repository_location - end - end - - def initialize - @description = 'My awesome project' - end - - def name=(raw_name) - @name = "#{raw_name}-#{SecureRandom.hex(8)}" - end - - def fabricate! - group.visit! - - Page::Group::Show.perform(&:go_to_new_project) - - Page::Project::New.perform do |page| - page.choose_test_namespace - page.choose_name(@name) - page.add_description(@description) - page.set_visibility('Public') - page.create_new_project - end - end - - def api_get_path - "/projects/#{name}" - end - - def api_post_path - '/projects' - end - - def api_post_body - { - namespace_id: group.id, - path: name, - name: name, - description: description, - visibility: 'public' - } - end - - private - - def transform_api_resource(resource) - resource[:repository_ssh_location] = Git::Location.new(resource[:ssh_url_to_repo]) - resource[:repository_http_location] = Git::Location.new(resource[:http_url_to_repo]) - resource - end - end - end - end -end diff --git a/qa/qa/factory/resource/project_imported_from_github.rb b/qa/qa/factory/resource/project_imported_from_github.rb deleted file mode 100644 index ce20641e6cc..00000000000 --- a/qa/qa/factory/resource/project_imported_from_github.rb +++ /dev/null @@ -1,36 +0,0 @@ -require 'securerandom' - -module QA - module Factory - module Resource - class ProjectImportedFromGithub < Resource::Project - attr_accessor :name - attr_writer :personal_access_token, :github_repository_path - - attribute :group do - Factory::Resource::Group.fabricate! - end - - def fabricate! - group.visit! - - Page::Group::Show.perform(&:go_to_new_project) - - Page::Project::New.perform do |page| - page.go_to_import_project - end - - Page::Project::New.perform do |page| - page.go_to_github_import - end - - Page::Project::Import::Github.perform do |page| - page.add_personal_access_token(@personal_access_token) - page.list_repos - page.import!(@github_repository_path, @name) - end - end - end - end - end -end diff --git a/qa/qa/factory/resource/project_milestone.rb b/qa/qa/factory/resource/project_milestone.rb deleted file mode 100644 index 383f534c12c..00000000000 --- a/qa/qa/factory/resource/project_milestone.rb +++ /dev/null @@ -1,36 +0,0 @@ -module QA - module Factory - module Resource - class ProjectMilestone < Factory::Base - attr_reader :title - attr_accessor :description - - attribute :project do - Factory::Resource::Project.fabricate! - end - - def title=(title) - @title = "#{title}-#{SecureRandom.hex(4)}" - @description = 'A milestone' - end - - def fabricate! - project.visit! - - Page::Project::Menu.perform do |page| - page.click_issues - page.click_milestones - end - - Page::Project::Milestone::Index.perform(&:click_new_milestone) - - Page::Project::Milestone::New.perform do |milestone_new| - milestone_new.set_title(@title) - milestone_new.set_description(@description) - milestone_new.create_new_milestone - end - end - end - end - end -end diff --git a/qa/qa/factory/resource/runner.rb b/qa/qa/factory/resource/runner.rb deleted file mode 100644 index 7108db1e55a..00000000000 --- a/qa/qa/factory/resource/runner.rb +++ /dev/null @@ -1,49 +0,0 @@ -require 'securerandom' - -module QA - module Factory - module Resource - class Runner < Factory::Base - attr_writer :name, :tags, :image - - attribute :project do - Factory::Resource::Project.fabricate! do |resource| - resource.name = 'project-with-ci-cd' - resource.description = 'Project with CI/CD Pipelines' - end - end - - def name - @name || "qa-runner-#{SecureRandom.hex(4)}" - end - - def tags - @tags || %w[qa e2e] - end - - def image - @image || 'gitlab/gitlab-runner:alpine' - end - - def fabricate! - project.visit! - - Page::Project::Menu.perform(&:click_ci_cd_settings) - - Service::Runner.new(name).tap do |runner| - Page::Project::Settings::CICD.perform do |settings| - settings.expand_runners_settings do |runners| - runner.pull - runner.token = runners.registration_token - runner.address = runners.coordinator_address - runner.tags = tags - runner.image = image - runner.register! - end - end - end - end - end - end - end -end diff --git a/qa/qa/factory/resource/sandbox.rb b/qa/qa/factory/resource/sandbox.rb deleted file mode 100644 index a125bac65dd..00000000000 --- a/qa/qa/factory/resource/sandbox.rb +++ /dev/null @@ -1,60 +0,0 @@ -module QA - module Factory - module Resource - ## - # Ensure we're in our sandbox namespace, either by navigating to it or by - # creating it if it doesn't yet exist. - # - class Sandbox < Factory::Base - attr_reader :path - - attribute :id - - def initialize - @path = Runtime::Namespace.sandbox_name - end - - def fabricate! - Page::Main::Menu.perform(&:go_to_groups) - - Page::Dashboard::Groups.perform do |page| - if page.has_group?(path) - page.go_to_group(path) - else - page.go_to_new_group - - Page::Group::New.perform do |group| - group.set_path(path) - group.set_description('GitLab QA Sandbox Group') - group.set_visibility('Public') - group.create - end - end - end - end - - def fabricate_via_api! - resource_web_url(api_get) - rescue ResourceNotFoundError - super - end - - def api_get_path - "/groups/#{path}" - end - - def api_post_path - '/groups' - end - - def api_post_body - { - path: path, - name: path, - visibility: 'public' - } - end - end - end - end -end diff --git a/qa/qa/factory/resource/ssh_key.rb b/qa/qa/factory/resource/ssh_key.rb deleted file mode 100644 index 6f952eda36f..00000000000 --- a/qa/qa/factory/resource/ssh_key.rb +++ /dev/null @@ -1,28 +0,0 @@ -# frozen_string_literal: true - -module QA - module Factory - module Resource - class SSHKey < Factory::Base - extend Forwardable - - attr_accessor :title - - def_delegators :key, :private_key, :public_key, :fingerprint - - def key - @key ||= Runtime::Key::RSA.new - end - - def fabricate! - Page::Main::Menu.perform(&:go_to_profile_settings) - Page::Profile::Menu.perform(&:click_ssh_keys) - - Page::Profile::SSHKeys.perform do |page| - page.add_key(public_key, title) - end - end - end - end - end -end diff --git a/qa/qa/factory/resource/user.rb b/qa/qa/factory/resource/user.rb deleted file mode 100644 index e361face1f0..00000000000 --- a/qa/qa/factory/resource/user.rb +++ /dev/null @@ -1,56 +0,0 @@ -require 'securerandom' - -module QA - module Factory - module Resource - class User < Factory::Base - attr_reader :unique_id - attr_writer :username, :password - - def initialize - @unique_id = SecureRandom.hex(8) - end - - def username - @username ||= "qa-user-#{unique_id}" - end - - def password - @password ||= 'password' - end - - def name - @name ||= username - end - - def email - @email ||= "#{username}@example.com" - end - - def credentials_given? - defined?(@username) && defined?(@password) - end - - def fabricate! - # Don't try to log-out if we're not logged-in - if Page::Main::Menu.perform { |p| p.has_personal_area?(wait: 0) } - Page::Main::Menu.perform { |main| main.sign_out } - end - - if credentials_given? - Page::Main::Login.perform do |login| - login.sign_in_using_credentials(self) - end - else - Page::Main::Login.perform do |login| - login.switch_to_register_tab - end - Page::Main::SignUp.perform do |signup| - signup.sign_up!(self) - end - end - end - end - end - end -end diff --git a/qa/qa/factory/resource/wiki.rb b/qa/qa/factory/resource/wiki.rb deleted file mode 100644 index 769f394e85c..00000000000 --- a/qa/qa/factory/resource/wiki.rb +++ /dev/null @@ -1,30 +0,0 @@ -module QA - module Factory - module Resource - class Wiki < Factory::Base - attr_accessor :title, :content, :message - - attribute :project do - Factory::Resource::Project.fabricate! do |resource| - resource.name = 'project-for-wikis' - resource.description = 'project for adding wikis' - end - end - - def fabricate! - project.visit! - - Page::Project::Menu.perform { |menu_side| menu_side.click_wiki } - - Page::Project::Wiki::New.perform do |wiki_new| - wiki_new.go_to_create_first_page - wiki_new.set_title(@title) - wiki_new.set_content(@content) - wiki_new.set_message(@message) - wiki_new.create_new_page - end - end - end - end - end -end diff --git a/qa/qa/page/main/login.rb b/qa/qa/page/main/login.rb index 94b9486b0d5..97ffe0e5716 100644 --- a/qa/qa/page/main/login.rb +++ b/qa/qa/page/main/login.rb @@ -65,7 +65,7 @@ module QA end def sign_in_using_admin_credentials - admin = QA::Factory::Resource::User.new.tap do |user| + admin = QA::Resource::User.new.tap do |user| user.username = QA::Runtime::User.admin_username user.password = QA::Runtime::User.admin_password end diff --git a/qa/qa/page/merge_request/show.rb b/qa/qa/page/merge_request/show.rb index 376606afb5d..2e69a89e386 100644 --- a/qa/qa/page/merge_request/show.rb +++ b/qa/qa/page/merge_request/show.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + module QA module Page module MergeRequest @@ -23,6 +25,32 @@ module QA element :squash_checkbox end + view 'app/views/projects/merge_requests/show.html.haml' do + element :notes_tab + element :diffs_tab + end + + view 'app/assets/javascripts/diffs/components/diff_line_gutter_content.vue' do + element :diff_comment + end + + view 'app/assets/javascripts/notes/components/comment_form.vue' do + element :note_dropdown + element :discussion_option + end + + view 'app/assets/javascripts/notes/components/note_form.vue' do + element :reply_input + end + + view 'app/assets/javascripts/notes/components/noteable_discussion.vue' do + element :discussion_reply + end + + view 'app/assets/javascripts/diffs/components/inline_diff_table_row.vue' do + element :new_diff_line + end + view 'app/views/shared/issuable/_sidebar.html.haml' do element :labels_block end @@ -106,6 +134,35 @@ module QA click_element :squash_checkbox end + + def go_to_discussions_tab + click_element :notes_tab + end + + def go_to_diffs_tab + click_element :diffs_tab + end + + def add_comment_to_diff(text) + wait(time: 5) do + page.has_text?("No newline at end of file") + end + all_elements(:new_diff_line).first.hover + click_element :diff_comment + fill_element :reply_input, text + end + + def start_discussion(text) + fill_element :comment_input, text + click_element :note_dropdown + click_element :discussion_option + click_element :comment_button + end + + def reply_to_discussion(reply_text) + all_elements(:discussion_reply).last.click + fill_element :reply_input, reply_text + end end end end diff --git a/qa/qa/page/project/operations/kubernetes/add.rb b/qa/qa/page/project/operations/kubernetes/add.rb index 18c16ca6db7..939f912ea85 100644 --- a/qa/qa/page/project/operations/kubernetes/add.rb +++ b/qa/qa/page/project/operations/kubernetes/add.rb @@ -4,7 +4,7 @@ module QA module Operations module Kubernetes class Add < Page::Base - view 'app/views/projects/clusters/new.html.haml' do + view 'app/views/clusters/clusters/new.html.haml' do element :add_existing_cluster_button, "Add existing cluster" # rubocop:disable QA/ElementWithPattern end diff --git a/qa/qa/page/project/operations/kubernetes/add_existing.rb b/qa/qa/page/project/operations/kubernetes/add_existing.rb index f8e026b4405..f3ab636ecc1 100644 --- a/qa/qa/page/project/operations/kubernetes/add_existing.rb +++ b/qa/qa/page/project/operations/kubernetes/add_existing.rb @@ -4,7 +4,7 @@ module QA module Operations module Kubernetes class AddExisting < Page::Base - view 'app/views/projects/clusters/user/_form.html.haml' do + view 'app/views/clusters/clusters/user/_form.html.haml' do element :cluster_name, 'text_field :name' # rubocop:disable QA/ElementWithPattern element :api_url, 'text_field :api_url' # rubocop:disable QA/ElementWithPattern element :ca_certificate, 'text_area :ca_cert' # rubocop:disable QA/ElementWithPattern diff --git a/qa/qa/page/project/operations/kubernetes/index.rb b/qa/qa/page/project/operations/kubernetes/index.rb index 312b459ac89..67a74af1cd2 100644 --- a/qa/qa/page/project/operations/kubernetes/index.rb +++ b/qa/qa/page/project/operations/kubernetes/index.rb @@ -4,7 +4,7 @@ module QA module Operations module Kubernetes class Index < Page::Base - view 'app/views/projects/clusters/_empty_state.html.haml' do + view 'app/views/clusters/clusters/_empty_state.html.haml' do element :add_kubernetes_cluster_button, "link_to s_('ClusterIntegration|Add Kubernetes cluster')" # rubocop:disable QA/ElementWithPattern end diff --git a/qa/qa/resource/README.md b/qa/qa/resource/README.md new file mode 100644 index 00000000000..4cdeb3f42a2 --- /dev/null +++ b/qa/qa/resource/README.md @@ -0,0 +1,392 @@ +# Resource class in GitLab QA + +Resources are primarily created using Browser UI steps, but can also +be created via the API. + +## How to properly implement a resource class? + +All resource classes should inherit from [`Resource::Base`](./base.rb). + +There is only one mandatory method to implement to define a resource class. +This is the `#fabricate!` method, which is used to build the resource via the +browser UI. Note that you should only use [Page objects](../page/README.md) to +interact with a Web page in this method. + +Here is an imaginary example: + +```ruby +module QA + module Resource + class Shirt < Base + attr_accessor :name + + def fabricate! + Page::Dashboard::Index.perform do |dashboard_index| + dashboard_index.go_to_new_shirt + end + + Page::Shirt::New.perform do |shirt_new| + shirt_new.set_name(name) + shirt_new.create_shirt! + end + end + end + end +end +``` + +### Define API implementation + +A resource class may also implement the three following methods to be able to +create the resource via the public GitLab API: + +- `#api_get_path`: The `GET` path to fetch an existing resource. +- `#api_post_path`: The `POST` path to create a new resource. +- `#api_post_body`: The `POST` body (as a Ruby hash) to create a new resource. + +Let's take the `Shirt` resource class, and add these three API methods: + +```ruby +module QA + module Resource + class Shirt < Base + attr_accessor :name + + def fabricate! + # ... same as before + end + + def api_get_path + "/shirt/#{name}" + end + + def api_post_path + "/shirts" + end + + def api_post_body + { + name: name + } + end + end + end +end +``` + +The [`Project` resource](./project.rb) is a good real example of Browser +UI and API implementations. + +#### Resource attributes + +A resource may need another resource to exist first. For instance, a project +needs a group to be created in. + +To define a resource attribute, you can use the `attribute` method with a +block using the other resource class to fabricate the resource. + +That will allow access to the other resource from your resource object's +methods. You would usually use it in `#fabricate!`, `#api_get_path`, +`#api_post_path`, `#api_post_body`. + +Let's take the `Shirt` resource class, and add a `project` attribute to it: + +```ruby +module QA + module Resource + class Shirt < Base + attr_accessor :name + + attribute :project do + Project.fabricate! do |resource| + resource.name = 'project-to-create-a-shirt' + end + end + + def fabricate! + project.visit! + + Page::Project::Show.perform do |project_show| + project_show.go_to_new_shirt + end + + Page::Shirt::New.perform do |shirt_new| + shirt_new.set_name(name) + shirt_new.create_shirt! + end + end + + def api_get_path + "/project/#{project.path}/shirt/#{name}" + end + + def api_post_path + "/project/#{project.path}/shirts" + end + + def api_post_body + { + name: name + } + end + end + end +end +``` + +**Note that all the attributes are lazily constructed. This means if you want +a specific attribute to be fabricated first, you'll need to call the +attribute method first even if you're not using it.** + +#### Product data attributes + +Once created, you may want to populate a resource with attributes that can be +found in the Web page, or in the API response. +For instance, once you create a project, you may want to store its repository +SSH URL as an attribute. + +Again we could use the `attribute` method with a block, using a page object +to retrieve the data on the page. + +Let's take the `Shirt` resource class, and define a `:brand` attribute: + +```ruby +module QA + module Resource + class Shirt < Base + attr_accessor :name + + attribute :project do + Project.fabricate! do |resource| + resource.name = 'project-to-create-a-shirt' + end + end + + # Attribute populated from the Browser UI (using the block) + attribute :brand do + Page::Shirt::Show.perform do |shirt_show| + shirt_show.fetch_brand_from_page + end + end + + # ... same as before + end + end +end +``` + +**Note again that all the attributes are lazily constructed. This means if +you call `shirt.brand` after moving to the other page, it'll not properly +retrieve the data because we're no longer on the expected page.** + +Consider this: + +```ruby +shirt = + QA::Resource::Shirt.fabricate! do |resource| + resource.name = "GitLab QA" + end + +shirt.project.visit! + +shirt.brand # => FAIL! +``` + +The above example will fail because now we're on the project page, trying to +construct the brand data from the shirt page, however we moved to the project +page already. There are two ways to solve this, one is that we could try to +retrieve the brand before visiting the project again: + +```ruby +shirt = + QA::Resource::Shirt.fabricate! do |resource| + resource.name = "GitLab QA" + end + +shirt.brand # => OK! + +shirt.project.visit! + +shirt.brand # => OK! +``` + +The attribute will be stored in the instance therefore all the following calls +will be fine, using the data previously constructed. If we think that this +might be too brittle, we could eagerly construct the data right before +ending fabrication: + +```ruby +module QA + module Resource + class Shirt < Base + # ... same as before + + def fabricate! + project.visit! + + Page::Project::Show.perform do |project_show| + project_show.go_to_new_shirt + end + + Page::Shirt::New.perform do |shirt_new| + shirt_new.set_name(name) + shirt_new.create_shirt! + end + + populate(:brand) # Eagerly construct the data + end + end + end +end +``` + +The `populate` method will iterate through its arguments and call each +attribute respectively. Here `populate(:brand)` has the same effect as +just `brand`. Using the populate method makes the intention clearer. + +With this, it will make sure we construct the data right after we create the +shirt. The drawback is that this will always construct the data when the +resource is fabricated even if we don't need to use the data. + +Alternatively, we could just make sure we're on the right page before +constructing the brand data: + +```ruby +module QA + module Resource + class Shirt < Base + attr_accessor :name + + attribute :project do + Project.fabricate! do |resource| + resource.name = 'project-to-create-a-shirt' + end + end + + # Attribute populated from the Browser UI (using the block) + attribute :brand do + back_url = current_url + visit! + + Page::Shirt::Show.perform do |shirt_show| + shirt_show.fetch_brand_from_page + end + + visit(back_url) + end + + # ... same as before + end + end +end +``` + +This will make sure it's on the shirt page before constructing brand, and +move back to the previous page to avoid breaking the state. + +#### Define an attribute based on an API response + +Sometimes, you want to define a resource attribute based on the API response +from its `GET` or `POST` request. For instance, if the creation of a shirt via +the API returns + +```ruby +{ + brand: 'a-brand-new-brand', + style: 't-shirt', + materials: [[:cotton, 80], [:polyamide, 20]] +} +``` + +you may want to store `style` as-is in the resource, and fetch the first value +of the first `materials` item in a `main_fabric` attribute. + +Let's take the `Shirt` resource class, and define a `:style` and a +`:main_fabric` attributes: + +```ruby +module QA + module Resource + class Shirt < Base + # ... same as before + + # @style from the instance if present, + # or fetched from the API response if present, + # or a QA::Resource::Base::NoValueError is raised otherwise + attribute :style + + # If @main_fabric is not present, + # and if the API does not contain this field, this block will be + # used to construct the value based on the API response, and + # store the result in @main_fabric + attribute :main_fabric do + api_response.&dig(:materials, 0, 0) + end + + # ... same as before + end + end +end +``` + +**Notes on attributes precedence:** + +- resource instance variables have the highest precedence +- attributes from the API response take precedence over attributes from the + block (usually from Browser UI) +- attributes without a value will raise a `QA::Resource::Base::NoValueError` error + +## Creating resources in your tests + +To create a resource in your tests, you can call the `.fabricate!` method on +the resource class. +Note that if the resource class supports API fabrication, this will use this +fabrication by default. + +Here is an example that will use the API fabrication method under the hood +since it's supported by the `Shirt` resource class: + +```ruby +my_shirt = Resource::Shirt.fabricate! do |shirt| + shirt.name = 'my-shirt' +end + +expect(page).to have_text(my_shirt.name) # => "my-shirt" from the resource's instance variable +expect(page).to have_text(my_shirt.brand) # => "a-brand-new-brand" from the API response +expect(page).to have_text(my_shirt.style) # => "t-shirt" from the API response +expect(page).to have_text(my_shirt.main_fabric) # => "cotton" from the API response via the block +``` + +If you explicitly want to use the Browser UI fabrication method, you can call +the `.fabricate_via_browser_ui!` method instead: + +```ruby +my_shirt = Resource::Shirt.fabricate_via_browser_ui! do |shirt| + shirt.name = 'my-shirt' +end + +expect(page).to have_text(my_shirt.name) # => "my-shirt" from the resource's instance variable +expect(page).to have_text(my_shirt.brand) # => the brand name fetched from the `Page::Shirt::Show` page via the block +expect(page).to have_text(my_shirt.style) # => QA::Resource::Base::NoValueError will be raised because no API response nor a block is provided +expect(page).to have_text(my_shirt.main_fabric) # => QA::Resource::Base::NoValueError will be raised because no API response and the block didn't provide a value (because it's also based on the API response) +``` + +You can also explicitly use the API fabrication method, by calling the +`.fabricate_via_api!` method: + +```ruby +my_shirt = Resource::Shirt.fabricate_via_api! do |shirt| + shirt.name = 'my-shirt' +end +``` + +In this case, the result will be similar to calling +`Resource::Shirt.fabricate!`. + +## Where to ask for help? + +If you need more information, ask for help on `#quality` channel on Slack +(internal, GitLab Team only). + +If you are not a Team Member, and you still need help to contribute, please +open an issue in GitLab CE issue tracker with the `~QA` label. diff --git a/qa/qa/factory/api_fabricator.rb b/qa/qa/resource/api_fabricator.rb index b1cfb6c9783..3762a94f312 100644 --- a/qa/qa/factory/api_fabricator.rb +++ b/qa/qa/resource/api_fabricator.rb @@ -5,7 +5,7 @@ require 'active_support/core_ext/object/deep_dup' require 'capybara/dsl' module QA - module Factory + module Resource module ApiFabricator include Airborne include Capybara::DSL @@ -27,7 +27,7 @@ module QA def fabricate_via_api! unless api_support? - raise NotImplementedError, "Factory #{self.class.name} does not support fabrication via the API!" + raise NotImplementedError, "Resource #{self.class.name} does not support fabrication via the API!" end resource_web_url(api_post) @@ -52,14 +52,18 @@ module QA end def api_get - url = Runtime::API::Request.new(api_client, api_get_path).url + process_api_response(parse_body(api_get_from(api_get_path))) + end + + def api_get_from(get_path) + url = Runtime::API::Request.new(api_client, get_path).url response = get(url) unless response.code == HTTP_STATUS_OK raise ResourceNotFoundError, "Resource at #{url} could not be found (#{response.code}): `#{response}`." end - process_api_response(parse_body(response)) + response end def api_post @@ -89,8 +93,8 @@ module QA self.api_resource = transform_api_resource(parsed_response.deep_dup) end - def transform_api_resource(resource) - resource + def transform_api_resource(api_resource) + api_resource end end end diff --git a/qa/qa/factory/base.rb b/qa/qa/resource/base.rb index e28a00c545b..f3eefb70520 100644 --- a/qa/qa/factory/base.rb +++ b/qa/qa/resource/base.rb @@ -4,7 +4,7 @@ require 'forwardable' require 'capybara/dsl' module QA - module Factory + module Resource class Base extend SingleForwardable include ApiFabricator @@ -58,11 +58,11 @@ module QA def self.fabricate_via_browser_ui!(*args, &prepare_block) options = args.extract_options! - factory = options.fetch(:factory) { new } + resource = options.fetch(:resource) { new } parents = options.fetch(:parents) { [] } - do_fabricate!(factory: factory, prepare_block: prepare_block, parents: parents) do - log_fabrication(:browser_ui, factory, parents, args) { factory.fabricate!(*args) } + do_fabricate!(resource: resource, prepare_block: prepare_block, parents: parents) do + log_fabrication(:browser_ui, resource, parents, args) { resource.fabricate!(*args) } current_url end @@ -70,29 +70,29 @@ module QA def self.fabricate_via_api!(*args, &prepare_block) options = args.extract_options! - factory = options.fetch(:factory) { new } + resource = options.fetch(:resource) { new } parents = options.fetch(:parents) { [] } - raise NotImplementedError unless factory.api_support? + raise NotImplementedError unless resource.api_support? - factory.eager_load_api_client! + resource.eager_load_api_client! - do_fabricate!(factory: factory, prepare_block: prepare_block, parents: parents) do - log_fabrication(:api, factory, parents, args) { factory.fabricate_via_api! } + do_fabricate!(resource: resource, prepare_block: prepare_block, parents: parents) do + log_fabrication(:api, resource, parents, args) { resource.fabricate_via_api! } end end - def self.do_fabricate!(factory:, prepare_block:, parents: []) - prepare_block.call(factory) if prepare_block + def self.do_fabricate!(resource:, prepare_block:, parents: []) + prepare_block.call(resource) if prepare_block resource_web_url = yield - factory.web_url = resource_web_url + resource.web_url = resource_web_url - factory + resource end private_class_method :do_fabricate! - def self.log_fabrication(method, factory, parents, args) + def self.log_fabrication(method, resource, parents, args) return yield unless Runtime::Env.debug? start = Time.now @@ -100,7 +100,7 @@ module QA msg = [prefix] msg << "Built a #{name}" msg << "as a dependency of #{parents.last}" if parents.any? - msg << "via #{method} with args #{args}" + msg << "via #{method}" yield.tap do msg << "in #{Time.now - start} seconds" @@ -111,7 +111,7 @@ module QA private_class_method :log_fabrication def self.evaluator - @evaluator ||= Factory::Base::DSL.new(self) + @evaluator ||= Base::DSL.new(self) end private_class_method :evaluator diff --git a/qa/qa/resource/branch.rb b/qa/qa/resource/branch.rb new file mode 100644 index 00000000000..bd52c4abe02 --- /dev/null +++ b/qa/qa/resource/branch.rb @@ -0,0 +1,77 @@ +# frozen_string_literal: true + +module QA + module Resource + class Branch < Base + attr_accessor :project, :branch_name, + :allow_to_push, :allow_to_merge, :protected + + attribute :project do + Project.fabricate! do |resource| + resource.name = 'protected-branch-project' + end + end + + def initialize + @branch_name = 'test/branch' + @allow_to_push = true + @allow_to_merge = true + @protected = false + end + + def fabricate! + project.visit! + + Repository::ProjectPush.fabricate! do |resource| + resource.project = project + resource.file_name = 'kick-off.txt' + resource.commit_message = 'First commit' + end + + branch = Repository::ProjectPush.fabricate! do |resource| + resource.project = project + resource.file_name = 'README.md' + resource.commit_message = 'Add readme' + resource.branch_name = 'master' + resource.new_branch = false + resource.remote_branch = @branch_name + end + + Page::Project::Show.perform do |page| + page.wait { page.has_content?(branch_name) } + end + + # The upcoming process will make it access the Protected Branches page, + # select the already created branch and protect it according + # to `allow_to_push` variable. + return branch unless @protected + + Page::Project::Menu.perform(&:click_repository_settings) + + Page::Project::Settings::Repository.perform do |setting| + setting.expand_protected_branches do |page| + page.select_branch(branch_name) + + if allow_to_push + page.allow_devs_and_maintainers_to_push + else + page.allow_no_one_to_push + end + + if allow_to_merge + page.allow_devs_and_maintainers_to_merge + else + page.allow_no_one_to_merge + end + + page.wait(reload: false) do + !page.first('.btn-success').disabled? + end + + page.protect_branch + end + end + end + end + end +end diff --git a/qa/qa/resource/ci_variable.rb b/qa/qa/resource/ci_variable.rb new file mode 100644 index 00000000000..0570c47d41c --- /dev/null +++ b/qa/qa/resource/ci_variable.rb @@ -0,0 +1,30 @@ +# frozen_string_literal: true + +module QA + module Resource + class CiVariable < Base + attr_accessor :key, :value + + attribute :project do + Project.fabricate! do |resource| + resource.name = 'project-with-ci-variables' + resource.description = 'project for adding CI variable test' + end + end + + def fabricate! + project.visit! + + Page::Project::Menu.perform(&:click_ci_cd_settings) + + Page::Project::Settings::CICD.perform do |setting| + setting.expand_ci_variables do |page| + page.fill_variable(key, value) + + page.save_variables + end + end + end + end + end +end diff --git a/qa/qa/resource/deploy_key.rb b/qa/qa/resource/deploy_key.rb new file mode 100644 index 00000000000..9ed8fb7726e --- /dev/null +++ b/qa/qa/resource/deploy_key.rb @@ -0,0 +1,43 @@ +# frozen_string_literal: true + +module QA + module Resource + class DeployKey < Base + attr_accessor :title, :key + + attribute :fingerprint do + Page::Project::Settings::Repository.perform do |setting| + setting.expand_deploy_keys do |key| + key_offset = key.key_titles.index do |key_title| + key_title.text == title + end + + key.key_fingerprints[key_offset].text + end + end + end + + attribute :project do + Project.fabricate! do |resource| + resource.name = 'project-to-deploy' + resource.description = 'project for adding deploy key test' + end + end + + def fabricate! + project.visit! + + Page::Project::Menu.perform(&:click_repository_settings) + + Page::Project::Settings::Repository.perform do |setting| + setting.expand_deploy_keys do |page| + page.fill_key_title(title) + page.fill_key_value(key) + + page.add_key + end + end + end + end + end +end diff --git a/qa/qa/resource/deploy_token.rb b/qa/qa/resource/deploy_token.rb new file mode 100644 index 00000000000..cee4422f6b4 --- /dev/null +++ b/qa/qa/resource/deploy_token.rb @@ -0,0 +1,50 @@ +# frozen_string_literal: true + +module QA + module Resource + class DeployToken < Base + attr_accessor :name, :expires_at + + attribute :username do + Page::Project::Settings::Repository.perform do |page| + page.expand_deploy_tokens do |token| + token.token_username + end + end + end + + attribute :password do + Page::Project::Settings::Repository.perform do |page| + page.expand_deploy_tokens do |token| + token.token_password + end + end + end + + attribute :project do + Project.fabricate! do |resource| + resource.name = 'project-to-deploy' + resource.description = 'project for adding deploy token test' + end + end + + def fabricate! + project.visit! + + Page::Project::Menu.act do + click_repository_settings + end + + Page::Project::Settings::Repository.perform do |setting| + setting.expand_deploy_tokens do |page| + page.fill_token_name(name) + page.fill_token_expires_at(expires_at) + page.fill_scopes(read_repository: true, read_registry: false) + + page.add_token + end + end + end + end + end +end diff --git a/qa/qa/resource/file.rb b/qa/qa/resource/file.rb new file mode 100644 index 00000000000..effc5a7940b --- /dev/null +++ b/qa/qa/resource/file.rb @@ -0,0 +1,36 @@ +# frozen_string_literal: true + +module QA + module Resource + class File < Base + attr_accessor :name, + :content, + :commit_message + + attribute :project do + Project.fabricate! do |resource| + resource.name = 'project-with-new-file' + end + end + + def initialize + @name = 'QA Test - File name' + @content = 'QA Test - File content' + @commit_message = 'QA Test - Commit message' + end + + def fabricate! + project.visit! + + Page::Project::Show.perform(&:create_new_file!) + + Page::File::Form.perform do |page| + page.add_name(@name) + page.add_content(@content) + page.add_commit_message(@commit_message) + page.commit_changes + end + end + end + end +end diff --git a/qa/qa/resource/fork.rb b/qa/qa/resource/fork.rb new file mode 100644 index 00000000000..9fd66f3a36a --- /dev/null +++ b/qa/qa/resource/fork.rb @@ -0,0 +1,43 @@ +# frozen_string_literal: true + +module QA + module Resource + class Fork < Base + attribute :push do + Repository::ProjectPush.fabricate! + end + + attribute :user do + User.fabricate! do |resource| + if Runtime::Env.forker? + resource.username = Runtime::Env.forker_username + resource.password = Runtime::Env.forker_password + end + end + end + + def fabricate! + populate(:push, :user) + + # Sign out as admin and sign is as the fork user + Page::Main::Menu.perform(&:sign_out) + Runtime::Browser.visit(:gitlab, Page::Main::Login) + Page::Main::Login.perform do |login| + login.sign_in_using_credentials(user) + end + + push.project.visit! + + Page::Project::Show.perform(&:fork_project) + + Page::Project::Fork::New.perform do |fork_new| + fork_new.choose_namespace(user.name) + end + + Page::Layout::Banner.perform do |page| + page.has_notice?('The project was successfully forked.') + end + end + end + end +end diff --git a/qa/qa/resource/group.rb b/qa/qa/resource/group.rb new file mode 100644 index 00000000000..dce15e4f10b --- /dev/null +++ b/qa/qa/resource/group.rb @@ -0,0 +1,68 @@ +# frozen_string_literal: true + +module QA + module Resource + class Group < Base + attr_accessor :path, :description + + attribute :sandbox do + Sandbox.fabricate! + end + + attribute :id + + def initialize + @path = Runtime::Namespace.name + @description = "QA test run at #{Runtime::Namespace.time}" + end + + def fabricate! + sandbox.visit! + + Page::Group::Show.perform do |group_show| + if group_show.has_subgroup?(path) + group_show.go_to_subgroup(path) + else + group_show.go_to_new_subgroup + + Page::Group::New.perform do |group_new| + group_new.set_path(path) + group_new.set_description(description) + group_new.set_visibility('Public') + group_new.create + end + + # Ensure that the group was actually created + group_show.wait(time: 1) do + group_show.has_text?(path) && + group_show.has_new_project_or_subgroup_dropdown? + end + end + end + end + + def fabricate_via_api! + resource_web_url(api_get) + rescue ResourceNotFoundError + super + end + + def api_get_path + "/groups/#{CGI.escape("#{sandbox.path}/#{path}")}" + end + + def api_post_path + '/groups' + end + + def api_post_body + { + parent_id: sandbox.id, + path: path, + name: path, + visibility: 'public' + } + end + end + end +end diff --git a/qa/qa/resource/issue.rb b/qa/qa/resource/issue.rb new file mode 100644 index 00000000000..2c2f27fe231 --- /dev/null +++ b/qa/qa/resource/issue.rb @@ -0,0 +1,30 @@ +# frozen_string_literal: true + +module QA + module Resource + class Issue < Base + attr_writer :description + + attribute :project do + Project.fabricate! do |resource| + resource.name = 'project-for-issues' + resource.description = 'project for adding issues' + end + end + + attribute :title + + def fabricate! + project.visit! + + Page::Project::Show.perform(&:go_to_new_issue) + + Page::Project::Issue::New.perform do |page| + page.add_title(@title) + page.add_description(@description) + page.create_new_issue + end + end + end + end +end diff --git a/qa/qa/resource/kubernetes_cluster.rb b/qa/qa/resource/kubernetes_cluster.rb new file mode 100644 index 00000000000..96c8843fb99 --- /dev/null +++ b/qa/qa/resource/kubernetes_cluster.rb @@ -0,0 +1,57 @@ +# frozen_string_literal: true + +require 'securerandom' + +module QA + module Resource + class KubernetesCluster < Base + attr_writer :project, :cluster, + :install_helm_tiller, :install_ingress, :install_prometheus, :install_runner + + attribute :ingress_ip do + Page::Project::Operations::Kubernetes::Show.perform(&:ingress_ip) + end + + def fabricate! + @project.visit! + + Page::Project::Menu.perform( + &:click_operations_kubernetes) + + Page::Project::Operations::Kubernetes::Index.perform( + &:add_kubernetes_cluster) + + Page::Project::Operations::Kubernetes::Add.perform( + &:add_existing_cluster) + + Page::Project::Operations::Kubernetes::AddExisting.perform do |page| + page.set_cluster_name(@cluster.cluster_name) + page.set_api_url(@cluster.api_url) + page.set_ca_certificate(@cluster.ca_certificate) + page.set_token(@cluster.token) + page.check_rbac! if @cluster.rbac + page.add_cluster! + end + + if @install_helm_tiller + Page::Project::Operations::Kubernetes::Show.perform do |page| + # We must wait a few seconds for permissions to be set up correctly for new cluster + sleep 10 + + # Helm must be installed before everything else + page.install!(:helm) + page.await_installed(:helm) + + page.install!(:ingress) if @install_ingress + page.install!(:prometheus) if @install_prometheus + page.install!(:runner) if @install_runner + + page.await_installed(:ingress) if @install_ingress + page.await_installed(:prometheus) if @install_prometheus + page.await_installed(:runner) if @install_runner + end + end + end + end + end +end diff --git a/qa/qa/resource/label.rb b/qa/qa/resource/label.rb new file mode 100644 index 00000000000..c0869cb1f2a --- /dev/null +++ b/qa/qa/resource/label.rb @@ -0,0 +1,39 @@ +# frozen_string_literal: true + +require 'securerandom' + +module QA + module Resource + class Label < Base + attr_accessor :description, :color + + attribute :title + + attribute :project do + Project.fabricate! do |resource| + resource.name = 'project-with-label' + end + end + + def initialize + @title = "qa-test-#{SecureRandom.hex(8)}" + @description = 'This is a test label' + @color = '#0033CC' + end + + def fabricate! + project.visit! + + Page::Project::Menu.perform(&:go_to_labels) + Page::Label::Index.perform(&:go_to_new_label) + + Page::Label::New.perform do |page| + page.fill_title(@title) + page.fill_description(@description) + page.fill_color(@color) + page.create_label + end + end + end + end +end diff --git a/qa/qa/resource/merge_request.rb b/qa/qa/resource/merge_request.rb new file mode 100644 index 00000000000..466a7942dc6 --- /dev/null +++ b/qa/qa/resource/merge_request.rb @@ -0,0 +1,71 @@ +# frozen_string_literal: true + +require 'securerandom' + +module QA + module Resource + class MergeRequest < Base + attr_accessor :title, + :description, + :source_branch, + :target_branch, + :assignee, + :milestone, + :labels + + attribute :project do + Project.fabricate! do |resource| + resource.name = 'project-with-merge-request' + end + end + + attribute :target do + project.visit! + + Repository::ProjectPush.fabricate! do |resource| + resource.project = project + resource.branch_name = 'master' + resource.remote_branch = target_branch + end + end + + attribute :source do + Repository::ProjectPush.fabricate! do |resource| + resource.project = project + resource.branch_name = target_branch + resource.remote_branch = source_branch + resource.new_branch = false + resource.file_name = "added_file.txt" + resource.file_content = "File Added" + end + end + + def initialize + @title = 'QA test - merge request' + @description = 'This is a test merge request' + @source_branch = "qa-test-feature-#{SecureRandom.hex(8)}" + @target_branch = "master" + @assignee = nil + @milestone = nil + @labels = [] + end + + def fabricate! + populate(:target, :source) + + project.visit! + Page::Project::Show.perform(&:new_merge_request) + Page::MergeRequest::New.perform do |page| + page.fill_title(@title) + page.fill_description(@description) + page.choose_milestone(@milestone) if @milestone + labels.each do |label| + page.select_label(label) + end + + page.create_merge_request + end + end + end + end +end diff --git a/qa/qa/resource/merge_request_from_fork.rb b/qa/qa/resource/merge_request_from_fork.rb new file mode 100644 index 00000000000..f91ae299d76 --- /dev/null +++ b/qa/qa/resource/merge_request_from_fork.rb @@ -0,0 +1,31 @@ +# frozen_string_literal: true + +module QA + module Resource + class MergeRequestFromFork < MergeRequest + attr_accessor :fork_branch + + attribute :fork do + Fork.fabricate! + end + + attribute :push do + Repository::ProjectPush.fabricate! do |resource| + resource.project = fork + resource.branch_name = fork_branch + resource.file_name = 'file2.txt' + resource.user = fork.user + end + end + + def fabricate! + populate(:push) + + fork.visit! + + Page::Project::Show.perform(&:new_merge_request) + Page::MergeRequest::New.perform(&:create_merge_request) + end + end + end +end diff --git a/qa/qa/resource/personal_access_token.rb b/qa/qa/resource/personal_access_token.rb new file mode 100644 index 00000000000..b8dd0a3562f --- /dev/null +++ b/qa/qa/resource/personal_access_token.rb @@ -0,0 +1,27 @@ +# frozen_string_literal: true + +module QA + module Resource + ## + # Create a personal access token that can be used by the api + # + class PersonalAccessToken < Base + attr_accessor :name + + attribute :access_token do + Page::Profile::PersonalAccessTokens.perform(&:created_access_token) + end + + def fabricate! + Page::Main::Menu.perform(&:go_to_profile_settings) + Page::Profile::Menu.perform(&:click_access_tokens) + + Page::Profile::PersonalAccessTokens.perform do |page| + page.fill_token_name(name || 'api-test-token') + page.check_api + page.create_token + end + end + end + end +end diff --git a/qa/qa/resource/project.rb b/qa/qa/resource/project.rb new file mode 100644 index 00000000000..7fdf69278f9 --- /dev/null +++ b/qa/qa/resource/project.rb @@ -0,0 +1,80 @@ +# frozen_string_literal: true + +require 'securerandom' + +module QA + module Resource + class Project < Base + attribute :name + attribute :description + + attribute :group do + Group.fabricate! + end + + attribute :repository_ssh_location do + Page::Project::Show.perform do |page| + page.choose_repository_clone_ssh + page.repository_location + end + end + + attribute :repository_http_location do + Page::Project::Show.perform do |page| + page.choose_repository_clone_http + page.repository_location + end + end + + def initialize + @description = 'My awesome project' + end + + def name=(raw_name) + @name = "#{raw_name}-#{SecureRandom.hex(8)}" + end + + def fabricate! + group.visit! + + Page::Group::Show.perform(&:go_to_new_project) + + Page::Project::New.perform do |page| + page.choose_test_namespace + page.choose_name(@name) + page.add_description(@description) + page.set_visibility('Public') + page.create_new_project + end + end + + def api_get_path + "/projects/#{name}" + end + + def api_post_path + '/projects' + end + + def api_post_body + { + namespace_id: group.id, + path: name, + name: name, + description: description, + visibility: 'public' + } + end + + private + + def transform_api_resource(api_resource) + api_resource[:repository_ssh_location] = + Git::Location.new(api_resource[:ssh_url_to_repo]) + api_resource[:repository_http_location] = + Git::Location.new(api_resource[:http_url_to_repo]) + api_resource + end + end + end +end diff --git a/qa/qa/resource/project_imported_from_github.rb b/qa/qa/resource/project_imported_from_github.rb new file mode 100644 index 00000000000..3f02fe885a9 --- /dev/null +++ b/qa/qa/resource/project_imported_from_github.rb @@ -0,0 +1,36 @@ +# frozen_string_literal: true + +require 'securerandom' + +module QA + module Resource + class ProjectImportedFromGithub < Project + attr_accessor :name + attr_writer :personal_access_token, :github_repository_path + + attribute :group do + Group.fabricate! + end + + def fabricate! + group.visit! + + Page::Group::Show.perform(&:go_to_new_project) + + Page::Project::New.perform do |page| + page.go_to_import_project + end + + Page::Project::New.perform do |page| + page.go_to_github_import + end + + Page::Project::Import::Github.perform do |page| + page.add_personal_access_token(@personal_access_token) + page.list_repos + page.import!(@github_repository_path, @name) + end + end + end + end +end diff --git a/qa/qa/resource/project_milestone.rb b/qa/qa/resource/project_milestone.rb new file mode 100644 index 00000000000..a4d6657caff --- /dev/null +++ b/qa/qa/resource/project_milestone.rb @@ -0,0 +1,36 @@ +# frozen_string_literal: true + +module QA + module Resource + class ProjectMilestone < Base + attr_reader :title + attr_accessor :description + + attribute :project do + Project.fabricate! + end + + def title=(title) + @title = "#{title}-#{SecureRandom.hex(4)}" + @description = 'A milestone' + end + + def fabricate! + project.visit! + + Page::Project::Menu.perform do |page| + page.click_issues + page.click_milestones + end + + Page::Project::Milestone::Index.perform(&:click_new_milestone) + + Page::Project::Milestone::New.perform do |milestone_new| + milestone_new.set_title(@title) + milestone_new.set_description(@description) + milestone_new.create_new_milestone + end + end + end + end +end diff --git a/qa/qa/factory/repository/project_push.rb b/qa/qa/resource/repository/project_push.rb index 272b7fc5818..c9fafe3419f 100644 --- a/qa/qa/factory/repository/project_push.rb +++ b/qa/qa/resource/repository/project_push.rb @@ -1,9 +1,11 @@ +# frozen_string_literal: true + module QA - module Factory + module Resource module Repository - class ProjectPush < Factory::Repository::Push + class ProjectPush < Repository::Push attribute :project do - Factory::Resource::Project.fabricate! do |resource| + Project.fabricate! do |resource| resource.name = 'project-with-code' resource.description = 'Project with repository' end diff --git a/qa/qa/factory/repository/push.rb b/qa/qa/resource/repository/push.rb index ffa755b9e88..c14d97ff7fb 100644 --- a/qa/qa/factory/repository/push.rb +++ b/qa/qa/resource/repository/push.rb @@ -1,9 +1,11 @@ +# frozen_string_literal: true + require 'pathname' module QA - module Factory + module Resource module Repository - class Push < Factory::Base + class Push < Base attr_accessor :file_name, :file_content, :commit_message, :branch_name, :new_branch, :output, :repository_http_uri, :repository_ssh_uri, :ssh_key, :user diff --git a/qa/qa/factory/repository/wiki_push.rb b/qa/qa/resource/repository/wiki_push.rb index 25b6ffe8323..f1c39d507fe 100644 --- a/qa/qa/factory/repository/wiki_push.rb +++ b/qa/qa/resource/repository/wiki_push.rb @@ -1,9 +1,11 @@ +# frozen_string_literal: true + module QA - module Factory + module Resource module Repository - class WikiPush < Factory::Repository::Push + class WikiPush < Repository::Push attribute :wiki do - Factory::Resource::Wiki.fabricate! do |resource| + Wiki.fabricate! do |resource| resource.title = 'Home' resource.content = '# My First Wiki Content' resource.message = 'Update home' diff --git a/qa/qa/resource/runner.rb b/qa/qa/resource/runner.rb new file mode 100644 index 00000000000..08ae3f22117 --- /dev/null +++ b/qa/qa/resource/runner.rb @@ -0,0 +1,49 @@ +# frozen_string_literal: true + +require 'securerandom' + +module QA + module Resource + class Runner < Base + attr_writer :name, :tags, :image + + attribute :project do + Project.fabricate! do |resource| + resource.name = 'project-with-ci-cd' + resource.description = 'Project with CI/CD Pipelines' + end + end + + def name + @name || "qa-runner-#{SecureRandom.hex(4)}" + end + + def tags + @tags || %w[qa e2e] + end + + def image + @image || 'gitlab/gitlab-runner:alpine' + end + + def fabricate! + project.visit! + + Page::Project::Menu.perform(&:click_ci_cd_settings) + + Service::Runner.new(name).tap do |runner| + Page::Project::Settings::CICD.perform do |settings| + settings.expand_runners_settings do |runners| + runner.pull + runner.token = runners.registration_token + runner.address = runners.coordinator_address + runner.tags = tags + runner.image = image + runner.register! + end + end + end + end + end + end +end diff --git a/qa/qa/resource/sandbox.rb b/qa/qa/resource/sandbox.rb new file mode 100644 index 00000000000..41ce857a8b8 --- /dev/null +++ b/qa/qa/resource/sandbox.rb @@ -0,0 +1,60 @@ +# frozen_string_literal: true + +module QA + module Resource + ## + # Ensure we're in our sandbox namespace, either by navigating to it or by + # creating it if it doesn't yet exist. + # + class Sandbox < Base + attr_reader :path + + attribute :id + + def initialize + @path = Runtime::Namespace.sandbox_name + end + + def fabricate! + Page::Main::Menu.perform(&:go_to_groups) + + Page::Dashboard::Groups.perform do |page| + if page.has_group?(path) + page.go_to_group(path) + else + page.go_to_new_group + + Page::Group::New.perform do |group| + group.set_path(path) + group.set_description('GitLab QA Sandbox Group') + group.set_visibility('Public') + group.create + end + end + end + end + + def fabricate_via_api! + resource_web_url(api_get) + rescue ResourceNotFoundError + super + end + + def api_get_path + "/groups/#{path}" + end + + def api_post_path + '/groups' + end + + def api_post_body + { + path: path, + name: path, + visibility: 'public' + } + end + end + end +end diff --git a/qa/qa/factory/settings/hashed_storage.rb b/qa/qa/resource/settings/hashed_storage.rb index 4e32382f910..40c06768ffe 100644 --- a/qa/qa/factory/settings/hashed_storage.rb +++ b/qa/qa/resource/settings/hashed_storage.rb @@ -1,7 +1,9 @@ +# frozen_string_literal: true + module QA - module Factory + module Resource module Settings - class HashedStorage < Factory::Base + class HashedStorage < Base def fabricate!(*traits) raise ArgumentError unless traits.include?(:enabled) diff --git a/qa/qa/resource/ssh_key.rb b/qa/qa/resource/ssh_key.rb new file mode 100644 index 00000000000..c6c97c8532f --- /dev/null +++ b/qa/qa/resource/ssh_key.rb @@ -0,0 +1,26 @@ +# frozen_string_literal: true + +module QA + module Resource + class SSHKey < Base + extend Forwardable + + attr_accessor :title + + def_delegators :key, :private_key, :public_key, :fingerprint + + def key + @key ||= Runtime::Key::RSA.new + end + + def fabricate! + Page::Main::Menu.perform(&:go_to_profile_settings) + Page::Profile::Menu.perform(&:click_ssh_keys) + + Page::Profile::SSHKeys.perform do |page| + page.add_key(public_key, title) + end + end + end + end +end diff --git a/qa/qa/resource/user.rb b/qa/qa/resource/user.rb new file mode 100644 index 00000000000..16f0b311fa9 --- /dev/null +++ b/qa/qa/resource/user.rb @@ -0,0 +1,92 @@ +# frozen_string_literal: true + +require 'securerandom' + +module QA + module Resource + class User < Base + attr_reader :unique_id + attr_writer :username, :password + + def initialize + @unique_id = SecureRandom.hex(8) + end + + def username + @username ||= "qa-user-#{unique_id}" + end + + def password + @password ||= 'password' + end + + def name + @name ||= username + end + + def email + @email ||= "#{username}@example.com" + end + + def credentials_given? + defined?(@username) && defined?(@password) + end + + def fabricate! + # Don't try to log-out if we're not logged-in + if Page::Main::Menu.perform { |p| p.has_personal_area?(wait: 0) } + Page::Main::Menu.perform { |main| main.sign_out } + end + + if credentials_given? + Page::Main::Login.perform do |login| + login.sign_in_using_credentials(self) + end + else + Page::Main::Login.perform do |login| + login.switch_to_register_tab + end + Page::Main::SignUp.perform do |signup| + signup.sign_up!(self) + end + end + end + + def fabricate_via_api! + resource_web_url(api_get) + rescue ResourceNotFoundError + super + end + + def api_get_path + "/users/#{fetch_id(username)}" + end + + def api_post_path + '/users' + end + + def api_post_body + { + email: email, + password: password, + username: username, + name: name, + skip_confirmation: true + } + end + + private + + def fetch_id(username) + users = parse_body(api_get_from("/users?username=#{username}")) + + unless users.size == 1 && users.first[:username] == username + raise ResourceNotFoundError, "Expected one user with username #{username} but found: `#{users}`." + end + + users.first[:id] + end + end + end +end diff --git a/qa/qa/resource/wiki.rb b/qa/qa/resource/wiki.rb new file mode 100644 index 00000000000..e942e9718a0 --- /dev/null +++ b/qa/qa/resource/wiki.rb @@ -0,0 +1,30 @@ +# frozen_string_literal: true + +module QA + module Resource + class Wiki < Base + attr_accessor :title, :content, :message + + attribute :project do + Project.fabricate! do |resource| + resource.name = 'project-for-wikis' + resource.description = 'project for adding wikis' + end + end + + def fabricate! + project.visit! + + Page::Project::Menu.perform { |menu_side| menu_side.click_wiki } + + Page::Project::Wiki::New.perform do |wiki_new| + wiki_new.go_to_create_first_page + wiki_new.set_title(@title) + wiki_new.set_content(@content) + wiki_new.set_message(@message) + wiki_new.create_new_page + end + end + end + end +end diff --git a/qa/qa/runtime/api/client.rb b/qa/qa/runtime/api/client.rb index 0545b500e4c..aff84c89f0e 100644 --- a/qa/qa/runtime/api/client.rb +++ b/qa/qa/runtime/api/client.rb @@ -32,7 +32,7 @@ module QA def do_create_personal_access_token Page::Main::Login.act { sign_in_using_credentials } - Factory::Resource::PersonalAccessToken.fabricate!.access_token + Resource::PersonalAccessToken.fabricate!.access_token end end end diff --git a/qa/qa/specs/features/browser_ui/1_manage/login/register_spec.rb b/qa/qa/specs/features/browser_ui/1_manage/login/register_spec.rb index 44071ec3e45..185837edacf 100644 --- a/qa/qa/specs/features/browser_ui/1_manage/login/register_spec.rb +++ b/qa/qa/specs/features/browser_ui/1_manage/login/register_spec.rb @@ -5,7 +5,7 @@ module QA it 'user registers and logs in' do Runtime::Browser.visit(:gitlab, Page::Main::Login) - Factory::Resource::User.fabricate! + Resource::User.fabricate_via_browser_ui! # TODO, since `Signed in successfully` message was removed # this is the only way to tell if user is signed in correctly. diff --git a/qa/qa/specs/features/browser_ui/1_manage/project/add_project_member_spec.rb b/qa/qa/specs/features/browser_ui/1_manage/project/add_project_member_spec.rb index 7bf26c22fa6..bef89d5be24 100644 --- a/qa/qa/specs/features/browser_ui/1_manage/project/add_project_member_spec.rb +++ b/qa/qa/specs/features/browser_ui/1_manage/project/add_project_member_spec.rb @@ -5,18 +5,16 @@ module QA describe 'Add project member' do it 'user adds project member' do Runtime::Browser.visit(:gitlab, Page::Main::Login) + Page::Main::Login.perform(&:sign_in_using_credentials) - user = Factory::Resource::User.fabricate! + user = Resource::User.fabricate! - Page::Main::Menu.perform { |main| main.sign_out } - Page::Main::Login.act { sign_in_using_credentials } - - project = Factory::Resource::Project.fabricate! do |resource| + project = Resource::Project.fabricate! do |resource| resource.name = 'add-member-project' end project.visit! - Page::Project::Menu.act { click_members_settings } + Page::Project::Menu.perform(&:click_members_settings) Page::Project::Settings::Members.perform do |page| page.add_member(user.username) end diff --git a/qa/qa/specs/features/browser_ui/1_manage/project/create_project_spec.rb b/qa/qa/specs/features/browser_ui/1_manage/project/create_project_spec.rb index a242f2158da..6632c2977ef 100644 --- a/qa/qa/specs/features/browser_ui/1_manage/project/create_project_spec.rb +++ b/qa/qa/specs/features/browser_ui/1_manage/project/create_project_spec.rb @@ -7,7 +7,7 @@ module QA Runtime::Browser.visit(:gitlab, Page::Main::Login) Page::Main::Login.act { sign_in_using_credentials } - created_project = Factory::Resource::Project.fabricate_via_browser_ui! do |project| + created_project = Resource::Project.fabricate_via_browser_ui! do |project| project.name = 'awesome-project' project.description = 'create awesome project test' end diff --git a/qa/qa/specs/features/browser_ui/1_manage/project/import_github_repo_spec.rb b/qa/qa/specs/features/browser_ui/1_manage/project/import_github_repo_spec.rb index a99b0522e73..3ce48de2c25 100644 --- a/qa/qa/specs/features/browser_ui/1_manage/project/import_github_repo_spec.rb +++ b/qa/qa/specs/features/browser_ui/1_manage/project/import_github_repo_spec.rb @@ -4,7 +4,7 @@ module QA context 'Manage', :orchestrated, :github do describe 'Project import from GitHub' do let(:imported_project) do - Factory::Resource::ProjectImportedFromGithub.fabricate! do |project| + Resource::ProjectImportedFromGithub.fabricate! do |project| project.name = 'imported-project' project.personal_access_token = Runtime::Env.github_access_token project.github_repository_path = 'gitlab-qa/test-project' diff --git a/qa/qa/specs/features/browser_ui/1_manage/project/view_project_activity_spec.rb b/qa/qa/specs/features/browser_ui/1_manage/project/view_project_activity_spec.rb index 768d40f3acf..275de3d332c 100644 --- a/qa/qa/specs/features/browser_ui/1_manage/project/view_project_activity_spec.rb +++ b/qa/qa/specs/features/browser_ui/1_manage/project/view_project_activity_spec.rb @@ -7,7 +7,7 @@ module QA Runtime::Browser.visit(:gitlab, Page::Main::Login) Page::Main::Login.act { sign_in_using_credentials } - Factory::Repository::ProjectPush.fabricate! do |push| + Resource::Repository::ProjectPush.fabricate! do |push| push.file_name = 'README.md' push.file_content = '# This is a test project' push.commit_message = 'Add README.md' diff --git a/qa/qa/specs/features/browser_ui/2_plan/issue/create_issue_spec.rb b/qa/qa/specs/features/browser_ui/2_plan/issue/create_issue_spec.rb index e67561b3a39..f5002c8032f 100644 --- a/qa/qa/specs/features/browser_ui/2_plan/issue/create_issue_spec.rb +++ b/qa/qa/specs/features/browser_ui/2_plan/issue/create_issue_spec.rb @@ -9,7 +9,7 @@ module QA Runtime::Browser.visit(:gitlab, Page::Main::Login) Page::Main::Login.act { sign_in_using_credentials } - Factory::Resource::Issue.fabricate! do |issue| + Resource::Issue.fabricate! do |issue| issue.title = issue_title end end diff --git a/qa/qa/specs/features/browser_ui/2_plan/issue/filter_issue_comments_spec.rb b/qa/qa/specs/features/browser_ui/2_plan/issue/filter_issue_comments_spec.rb index 24877d937d2..83603f1cda7 100644 --- a/qa/qa/specs/features/browser_ui/2_plan/issue/filter_issue_comments_spec.rb +++ b/qa/qa/specs/features/browser_ui/2_plan/issue/filter_issue_comments_spec.rb @@ -9,7 +9,7 @@ module QA Runtime::Browser.visit(:gitlab, Page::Main::Login) Page::Main::Login.act { sign_in_using_credentials } - Factory::Resource::Issue.fabricate! do |issue| + Resource::Issue.fabricate! do |issue| issue.title = issue_title end diff --git a/qa/qa/specs/features/browser_ui/3_create/merge_request/create_merge_request_spec.rb b/qa/qa/specs/features/browser_ui/3_create/merge_request/create_merge_request_spec.rb index 037ff5efbd4..d33947f41da 100644 --- a/qa/qa/specs/features/browser_ui/3_create/merge_request/create_merge_request_spec.rb +++ b/qa/qa/specs/features/browser_ui/3_create/merge_request/create_merge_request_spec.rb @@ -7,22 +7,22 @@ module QA Runtime::Browser.visit(:gitlab, Page::Main::Login) Page::Main::Login.act { sign_in_using_credentials } - current_project = Factory::Resource::Project.fabricate! do |project| + current_project = Resource::Project.fabricate! do |project| project.name = 'project-with-merge-request-and-milestone' end - current_milestone = Factory::Resource::ProjectMilestone.fabricate! do |milestone| + current_milestone = Resource::ProjectMilestone.fabricate! do |milestone| milestone.title = 'unique-milestone' milestone.project = current_project end - new_label = Factory::Resource::Label.fabricate! do |label| + new_label = Resource::Label.fabricate! do |label| label.project = current_project label.title = 'qa-mr-test-label' label.description = 'Merge Request label' end - Factory::Resource::MergeRequest.fabricate! do |merge_request| + Resource::MergeRequest.fabricate! do |merge_request| merge_request.title = 'This is a merge request with a milestone' merge_request.description = 'Great feature with milestone' merge_request.project = current_project @@ -49,11 +49,11 @@ module QA Runtime::Browser.visit(:gitlab, Page::Main::Login) Page::Main::Login.act { sign_in_using_credentials } - current_project = Factory::Resource::Project.fabricate! do |project| + current_project = Resource::Project.fabricate! do |project| project.name = 'project-with-merge-request' end - Factory::Resource::MergeRequest.fabricate! do |merge_request| + Resource::MergeRequest.fabricate! do |merge_request| merge_request.title = 'This is a merge request' merge_request.description = 'Great feature' merge_request.project = current_project diff --git a/qa/qa/specs/features/browser_ui/3_create/merge_request/merge_merge_request_from_fork_spec.rb b/qa/qa/specs/features/browser_ui/3_create/merge_request/merge_merge_request_from_fork_spec.rb index 058af8aebdd..6dcd74471fe 100644 --- a/qa/qa/specs/features/browser_ui/3_create/merge_request/merge_merge_request_from_fork_spec.rb +++ b/qa/qa/specs/features/browser_ui/3_create/merge_request/merge_merge_request_from_fork_spec.rb @@ -7,7 +7,7 @@ module QA Runtime::Browser.visit(:gitlab, Page::Main::Login) Page::Main::Login.act { sign_in_using_credentials } - merge_request = Factory::Resource::MergeRequestFromFork.fabricate! do |merge_request| + merge_request = Resource::MergeRequestFromFork.fabricate! do |merge_request| merge_request.fork_branch = 'feature-branch' end diff --git a/qa/qa/specs/features/browser_ui/3_create/merge_request/rebase_merge_request_spec.rb b/qa/qa/specs/features/browser_ui/3_create/merge_request/rebase_merge_request_spec.rb index 3bcf086d332..e2d639fd150 100644 --- a/qa/qa/specs/features/browser_ui/3_create/merge_request/rebase_merge_request_spec.rb +++ b/qa/qa/specs/features/browser_ui/3_create/merge_request/rebase_merge_request_spec.rb @@ -7,7 +7,7 @@ module QA Runtime::Browser.visit(:gitlab, Page::Main::Login) Page::Main::Login.act { sign_in_using_credentials } - project = Factory::Resource::Project.fabricate! do |project| + project = Resource::Project.fabricate! do |project| project.name = "only-fast-forward" end project.visit! @@ -15,12 +15,12 @@ module QA Page::Project::Menu.act { go_to_settings } Page::Project::Settings::MergeRequest.act { enable_ff_only } - merge_request = Factory::Resource::MergeRequest.fabricate! do |merge_request| + merge_request = Resource::MergeRequest.fabricate! do |merge_request| merge_request.project = project merge_request.title = 'Needs rebasing' end - Factory::Repository::ProjectPush.fabricate! do |push| + Resource::Repository::ProjectPush.fabricate! do |push| push.project = project push.file_name = "other.txt" push.file_content = "New file added!" diff --git a/qa/qa/specs/features/browser_ui/3_create/merge_request/squash_merge_request_spec.rb b/qa/qa/specs/features/browser_ui/3_create/merge_request/squash_merge_request_spec.rb index 724c48cd125..6ff7360c413 100644 --- a/qa/qa/specs/features/browser_ui/3_create/merge_request/squash_merge_request_spec.rb +++ b/qa/qa/specs/features/browser_ui/3_create/merge_request/squash_merge_request_spec.rb @@ -7,16 +7,16 @@ module QA Runtime::Browser.visit(:gitlab, Page::Main::Login) Page::Main::Login.act { sign_in_using_credentials } - project = Factory::Resource::Project.fabricate! do |project| + project = Resource::Project.fabricate! do |project| project.name = "squash-before-merge" end - merge_request = Factory::Resource::MergeRequest.fabricate! do |merge_request| + merge_request = Resource::MergeRequest.fabricate! do |merge_request| merge_request.project = project merge_request.title = 'Squashing commits' end - Factory::Repository::ProjectPush.fabricate! do |push| + Resource::Repository::ProjectPush.fabricate! do |push| push.project = project push.commit_message = 'to be squashed' push.branch_name = merge_request.source_branch diff --git a/qa/qa/specs/features/browser_ui/3_create/repository/add_file_template_spec.rb b/qa/qa/specs/features/browser_ui/3_create/repository/add_file_template_spec.rb index 7705e12b95e..297485dd81e 100644 --- a/qa/qa/specs/features/browser_ui/3_create/repository/add_file_template_spec.rb +++ b/qa/qa/specs/features/browser_ui/3_create/repository/add_file_template_spec.rb @@ -13,7 +13,7 @@ module QA before(:all) do login - @project = Factory::Resource::Project.fabricate! do |project| + @project = Resource::Project.fabricate! do |project| project.name = 'file-template-project' project.description = 'Add file templates via the Files view' end diff --git a/qa/qa/specs/features/browser_ui/3_create/repository/add_ssh_key_spec.rb b/qa/qa/specs/features/browser_ui/3_create/repository/add_ssh_key_spec.rb index df70b9608d9..94be66782c6 100644 --- a/qa/qa/specs/features/browser_ui/3_create/repository/add_ssh_key_spec.rb +++ b/qa/qa/specs/features/browser_ui/3_create/repository/add_ssh_key_spec.rb @@ -9,7 +9,7 @@ module QA Runtime::Browser.visit(:gitlab, Page::Main::Login) Page::Main::Login.act { sign_in_using_credentials } - key = Factory::Resource::SSHKey.fabricate! do |resource| + key = Resource::SSHKey.fabricate! do |resource| resource.title = key_title end diff --git a/qa/qa/specs/features/browser_ui/3_create/repository/clone_spec.rb b/qa/qa/specs/features/browser_ui/3_create/repository/clone_spec.rb index b18dee53cbc..6a0add56fe0 100644 --- a/qa/qa/specs/features/browser_ui/3_create/repository/clone_spec.rb +++ b/qa/qa/specs/features/browser_ui/3_create/repository/clone_spec.rb @@ -14,7 +14,7 @@ module QA Runtime::Browser.visit(:gitlab, Page::Main::Login) Page::Main::Login.act { sign_in_using_credentials } - project = Factory::Resource::Project.fabricate! do |scenario| + project = Resource::Project.fabricate! do |scenario| scenario.name = 'project-with-code' scenario.description = 'project for git clone tests' end diff --git a/qa/qa/specs/features/browser_ui/3_create/repository/create_edit_delete_file_via_web_spec.rb b/qa/qa/specs/features/browser_ui/3_create/repository/create_edit_delete_file_via_web_spec.rb index f65a1569fb0..46346d1b984 100644 --- a/qa/qa/specs/features/browser_ui/3_create/repository/create_edit_delete_file_via_web_spec.rb +++ b/qa/qa/specs/features/browser_ui/3_create/repository/create_edit_delete_file_via_web_spec.rb @@ -12,7 +12,7 @@ module QA file_content = 'QA Test - File content' commit_message_for_create = 'QA Test - Create new file' - Factory::Resource::File.fabricate! do |file| + Resource::File.fabricate! do |file| file.name = file_name file.content = file_content file.commit_message = commit_message_for_create diff --git a/qa/qa/specs/features/browser_ui/3_create/repository/push_http_private_token_spec.rb b/qa/qa/specs/features/browser_ui/3_create/repository/push_http_private_token_spec.rb index 8e4210482a2..a63b7dce8d6 100644 --- a/qa/qa/specs/features/browser_ui/3_create/repository/push_http_private_token_spec.rb +++ b/qa/qa/specs/features/browser_ui/3_create/repository/push_http_private_token_spec.rb @@ -7,14 +7,14 @@ module QA Runtime::Browser.visit(:gitlab, Page::Main::Login) Page::Main::Login.perform(&:sign_in_using_credentials) - access_token = Factory::Resource::PersonalAccessToken.fabricate!.access_token + access_token = Resource::PersonalAccessToken.fabricate!.access_token - user = Factory::Resource::User.new.tap do |user| + user = Resource::User.new.tap do |user| user.username = Runtime::User.username user.password = access_token end - push = Factory::Repository::ProjectPush.fabricate! do |push| + push = Resource::Repository::ProjectPush.fabricate! do |push| push.user = user push.file_name = 'README.md' push.file_content = '# This is a test project' diff --git a/qa/qa/specs/features/browser_ui/3_create/repository/push_over_http_spec.rb b/qa/qa/specs/features/browser_ui/3_create/repository/push_over_http_spec.rb index 2f63a07e0c3..92f596a44d9 100644 --- a/qa/qa/specs/features/browser_ui/3_create/repository/push_over_http_spec.rb +++ b/qa/qa/specs/features/browser_ui/3_create/repository/push_over_http_spec.rb @@ -7,7 +7,7 @@ module QA Runtime::Browser.visit(:gitlab, Page::Main::Login) Page::Main::Login.act { sign_in_using_credentials } - Factory::Repository::ProjectPush.fabricate! do |push| + Resource::Repository::ProjectPush.fabricate! do |push| push.file_name = 'README.md' push.file_content = '# This is a test project' push.commit_message = 'Add README.md' diff --git a/qa/qa/specs/features/browser_ui/3_create/repository/push_protected_branch_spec.rb b/qa/qa/specs/features/browser_ui/3_create/repository/push_protected_branch_spec.rb index ac71cf52b6f..73a3dc14a65 100644 --- a/qa/qa/specs/features/browser_ui/3_create/repository/push_protected_branch_spec.rb +++ b/qa/qa/specs/features/browser_ui/3_create/repository/push_protected_branch_spec.rb @@ -6,7 +6,7 @@ module QA let(:branch_name) { 'protected-branch' } let(:commit_message) { 'Protected push commit message' } let(:project) do - Factory::Resource::Project.fabricate! do |resource| + Resource::Project.fabricate! do |resource| resource.name = 'protected-branch-project' end end @@ -47,7 +47,7 @@ module QA end def create_protected_branch(allow_to_push:) - Factory::Resource::Branch.fabricate! do |resource| + Resource::Branch.fabricate! do |resource| resource.branch_name = branch_name resource.project = project resource.allow_to_push = allow_to_push @@ -56,7 +56,7 @@ module QA end def push_new_file(branch) - Factory::Repository::ProjectPush.fabricate! do |resource| + Resource::Repository::ProjectPush.fabricate! do |resource| resource.project = project resource.file_name = 'new_file.md' resource.file_content = '# This is a new file' diff --git a/qa/qa/specs/features/browser_ui/3_create/repository/use_ssh_key_spec.rb b/qa/qa/specs/features/browser_ui/3_create/repository/use_ssh_key_spec.rb index 36068ffba69..9c764424129 100644 --- a/qa/qa/specs/features/browser_ui/3_create/repository/use_ssh_key_spec.rb +++ b/qa/qa/specs/features/browser_ui/3_create/repository/use_ssh_key_spec.rb @@ -12,11 +12,11 @@ module QA Runtime::Browser.visit(:gitlab, Page::Main::Login) Page::Main::Login.act { sign_in_using_credentials } - key = Factory::Resource::SSHKey.fabricate! do |resource| + key = Resource::SSHKey.fabricate! do |resource| resource.title = key_title end - Factory::Repository::ProjectPush.fabricate! do |push| + Resource::Repository::ProjectPush.fabricate! do |push| push.ssh_key = key push.file_name = 'README.md' push.file_content = '# Test Use SSH Key' diff --git a/qa/qa/specs/features/browser_ui/3_create/web_ide/add_file_template_spec.rb b/qa/qa/specs/features/browser_ui/3_create/web_ide/add_file_template_spec.rb index 07dbf39a8a3..e7374377104 100644 --- a/qa/qa/specs/features/browser_ui/3_create/web_ide/add_file_template_spec.rb +++ b/qa/qa/specs/features/browser_ui/3_create/web_ide/add_file_template_spec.rb @@ -13,7 +13,7 @@ module QA before(:all) do login - @project = Factory::Resource::Project.fabricate! do |project| + @project = Resource::Project.fabricate! do |project| project.name = 'file-template-project' project.description = 'Add file templates via the Web IDE' end diff --git a/qa/qa/specs/features/browser_ui/3_create/wiki/create_edit_clone_push_wiki_spec.rb b/qa/qa/specs/features/browser_ui/3_create/wiki/create_edit_clone_push_wiki_spec.rb index 4126fd9fd3e..210271705d9 100644 --- a/qa/qa/specs/features/browser_ui/3_create/wiki/create_edit_clone_push_wiki_spec.rb +++ b/qa/qa/specs/features/browser_ui/3_create/wiki/create_edit_clone_push_wiki_spec.rb @@ -18,7 +18,7 @@ module QA end it 'user creates, edits, clones, and pushes to the wiki' do - wiki = Factory::Resource::Wiki.fabricate! do |resource| + wiki = Resource::Wiki.fabricate! do |resource| resource.title = 'Home' resource.content = '# My First Wiki Content' resource.message = 'Update home' @@ -34,7 +34,7 @@ module QA validate_content('My Second Wiki Content') - Factory::Repository::WikiPush.fabricate! do |push| + Resource::Repository::WikiPush.fabricate! do |push| push.wiki = wiki push.file_name = 'Home.md' push.file_content = '# My Third Wiki Content' diff --git a/qa/qa/specs/features/browser_ui/4_verify/ci_variable/add_ci_variable_spec.rb b/qa/qa/specs/features/browser_ui/4_verify/ci_variable/add_ci_variable_spec.rb index 58b272adcf1..0837b720df1 100644 --- a/qa/qa/specs/features/browser_ui/4_verify/ci_variable/add_ci_variable_spec.rb +++ b/qa/qa/specs/features/browser_ui/4_verify/ci_variable/add_ci_variable_spec.rb @@ -7,7 +7,7 @@ module QA Runtime::Browser.visit(:gitlab, Page::Main::Login) Page::Main::Login.act { sign_in_using_credentials } - Factory::Resource::CiVariable.fabricate! do |resource| + Resource::CiVariable.fabricate! do |resource| resource.key = 'VARIABLE_KEY' resource.value = 'some CI variable' end diff --git a/qa/qa/specs/features/browser_ui/4_verify/pipeline/create_and_process_pipeline_spec.rb b/qa/qa/specs/features/browser_ui/4_verify/pipeline/create_and_process_pipeline_spec.rb index d66bcce879b..25cbe41c684 100644 --- a/qa/qa/specs/features/browser_ui/4_verify/pipeline/create_and_process_pipeline_spec.rb +++ b/qa/qa/specs/features/browser_ui/4_verify/pipeline/create_and_process_pipeline_spec.rb @@ -13,18 +13,18 @@ module QA Runtime::Browser.visit(:gitlab, Page::Main::Login) Page::Main::Login.act { sign_in_using_credentials } - project = Factory::Resource::Project.fabricate! do |project| + project = Resource::Project.fabricate! do |project| project.name = 'project-with-pipelines' project.description = 'Project with CI/CD Pipelines.' end - Factory::Resource::Runner.fabricate! do |runner| + Resource::Runner.fabricate! do |runner| runner.project = project runner.name = executor runner.tags = %w[qa test] end - Factory::Repository::ProjectPush.fabricate! do |push| + Resource::Repository::ProjectPush.fabricate! do |push| push.project = project push.file_name = '.gitlab-ci.yml' push.commit_message = 'Add .gitlab-ci.yml' diff --git a/qa/qa/specs/features/browser_ui/4_verify/runner/register_runner_spec.rb b/qa/qa/specs/features/browser_ui/4_verify/runner/register_runner_spec.rb index 5d9aa00582f..3af7db751e7 100644 --- a/qa/qa/specs/features/browser_ui/4_verify/runner/register_runner_spec.rb +++ b/qa/qa/specs/features/browser_ui/4_verify/runner/register_runner_spec.rb @@ -13,7 +13,7 @@ module QA Runtime::Browser.visit(:gitlab, Page::Main::Login) Page::Main::Login.act { sign_in_using_credentials } - Factory::Resource::Runner.fabricate! do |runner| + Resource::Runner.fabricate! do |runner| runner.name = executor end diff --git a/qa/qa/specs/features/browser_ui/6_release/deploy_key/add_deploy_key_spec.rb b/qa/qa/specs/features/browser_ui/6_release/deploy_key/add_deploy_key_spec.rb index 64b98da8bf5..84757f25379 100644 --- a/qa/qa/specs/features/browser_ui/6_release/deploy_key/add_deploy_key_spec.rb +++ b/qa/qa/specs/features/browser_ui/6_release/deploy_key/add_deploy_key_spec.rb @@ -11,7 +11,7 @@ module QA deploy_key_title = 'deploy key title' deploy_key_value = key.public_key - deploy_key = Factory::Resource::DeployKey.fabricate! do |resource| + deploy_key = Resource::DeployKey.fabricate! do |resource| resource.title = deploy_key_title resource.key = deploy_key_value end diff --git a/qa/qa/specs/features/browser_ui/6_release/deploy_key/clone_using_deploy_key_spec.rb b/qa/qa/specs/features/browser_ui/6_release/deploy_key/clone_using_deploy_key_spec.rb index 604641e54b8..e2320c92343 100644 --- a/qa/qa/specs/features/browser_ui/6_release/deploy_key/clone_using_deploy_key_spec.rb +++ b/qa/qa/specs/features/browser_ui/6_release/deploy_key/clone_using_deploy_key_spec.rb @@ -15,13 +15,13 @@ module QA @runner_name = "qa-runner-#{Time.now.to_i}" - @project = Factory::Resource::Project.fabricate! do |resource| + @project = Resource::Project.fabricate! do |resource| resource.name = 'deploy-key-clone-project' end @repository_location = @project.repository_ssh_location - Factory::Resource::Runner.fabricate! do |resource| + Resource::Runner.fabricate! do |resource| resource.project = @project resource.name = @runner_name resource.tags = %w[qa docker] @@ -47,7 +47,7 @@ module QA login - Factory::Resource::DeployKey.fabricate! do |resource| + Resource::DeployKey.fabricate! do |resource| resource.project = @project resource.title = "deploy key #{key.name}(#{key.bits})" resource.key = key.public_key @@ -55,7 +55,7 @@ module QA deploy_key_name = "DEPLOY_KEY_#{key.name}_#{key.bits}" - Factory::Resource::CiVariable.fabricate! do |resource| + Resource::CiVariable.fabricate! do |resource| resource.project = @project resource.key = deploy_key_name resource.value = key.private_key @@ -78,7 +78,7 @@ module QA - docker YAML - Factory::Repository::ProjectPush.fabricate! do |resource| + Resource::Repository::ProjectPush.fabricate! do |resource| resource.project = @project resource.file_name = '.gitlab-ci.yml' resource.commit_message = 'Add .gitlab-ci.yml' diff --git a/qa/qa/specs/features/browser_ui/6_release/deploy_token/add_deploy_token_spec.rb b/qa/qa/specs/features/browser_ui/6_release/deploy_token/add_deploy_token_spec.rb index 263ba6a6800..9f34e4218c1 100644 --- a/qa/qa/specs/features/browser_ui/6_release/deploy_token/add_deploy_token_spec.rb +++ b/qa/qa/specs/features/browser_ui/6_release/deploy_token/add_deploy_token_spec.rb @@ -10,7 +10,7 @@ module QA deploy_token_name = 'deploy token name' deploy_token_expires_at = Date.today + 7 # 1 Week from now - deploy_token = Factory::Resource::DeployToken.fabricate! do |resource| + deploy_token = Resource::DeployToken.fabricate! do |resource| resource.name = deploy_token_name resource.expires_at = deploy_token_expires_at end diff --git a/qa/qa/specs/features/browser_ui/7_configure/auto_devops/create_project_with_auto_devops_spec.rb b/qa/qa/specs/features/browser_ui/7_configure/auto_devops/create_project_with_auto_devops_spec.rb index c2fce1e7df1..30ec0665973 100644 --- a/qa/qa/specs/features/browser_ui/7_configure/auto_devops/create_project_with_auto_devops_spec.rb +++ b/qa/qa/specs/features/browser_ui/7_configure/auto_devops/create_project_with_auto_devops_spec.rb @@ -15,21 +15,21 @@ module QA Runtime::Browser.visit(:gitlab, Page::Main::Login) Page::Main::Login.act { sign_in_using_credentials } - project = Factory::Resource::Project.fabricate! do |p| + project = Resource::Project.fabricate! do |p| p.name = 'project-with-autodevops' p.description = 'Project with Auto Devops' end # Disable code_quality check in Auto DevOps pipeline as it takes # too long and times out the test - Factory::Resource::CiVariable.fabricate! do |resource| + Resource::CiVariable.fabricate! do |resource| resource.project = project resource.key = 'CODE_QUALITY_DISABLED' resource.value = '1' end # Create Auto Devops compatible repo - Factory::Repository::ProjectPush.fabricate! do |push| + Resource::Repository::ProjectPush.fabricate! do |push| push.project = project push.directory = Pathname .new(__dir__) @@ -41,7 +41,7 @@ module QA # Create and connect K8s cluster @cluster = Service::KubernetesCluster.new(rbac: rbac).create! - kubernetes_cluster = Factory::Resource::KubernetesCluster.fabricate! do |cluster| + kubernetes_cluster = Resource::KubernetesCluster.fabricate! do |cluster| cluster.project = project cluster.cluster = @cluster cluster.install_helm_tiller = true diff --git a/qa/spec/factory/resource/user_spec.rb b/qa/spec/factory/resource/user_spec.rb new file mode 100644 index 00000000000..820c506b715 --- /dev/null +++ b/qa/spec/factory/resource/user_spec.rb @@ -0,0 +1,36 @@ +# frozen_string_literal: true + +describe QA::Resource::User do + describe "#fabricate_via_api!" do + Response = Struct.new(:code, :body) + + it 'fetches an existing user' do + existing_users = [ + { + id: '0', + name: 'name', + username: 'name', + web_url: '' + } + ] + users_response = Response.new('200', JSON.dump(existing_users)) + single_user_response = Response.new('200', JSON.dump(existing_users.first)) + + expect(subject).to receive(:api_get_from).with("/users?username=name").and_return(users_response) + expect(subject).to receive(:api_get_from).with("/users/0").and_return(single_user_response) + + subject.username = 'name' + subject.fabricate_via_api! + + expect(subject.api_response).to eq(existing_users.first) + end + + it 'tries to create a user if it does not exist' do + expect(subject).to receive(:api_get_from).with("/users?username=foo").and_return(Response.new('200', '[]')) + expect(subject).to receive(:api_post).and_return({ web_url: '' }) + + subject.username = 'foo' + subject.fabricate_via_api! + end + end +end diff --git a/qa/spec/factory/api_fabricator_spec.rb b/qa/spec/resource/api_fabricator_spec.rb index e5fbc064911..a5ed4422f6e 100644 --- a/qa/spec/factory/api_fabricator_spec.rb +++ b/qa/spec/resource/api_fabricator_spec.rb @@ -1,18 +1,18 @@ # frozen_string_literal: true -describe QA::Factory::ApiFabricator do - let(:factory_without_api_support) do +describe QA::Resource::ApiFabricator do + let(:resource_without_api_support) do Class.new do def self.name - 'FooBarFactory' + 'FooBarResource' end end end - let(:factory_with_api_support) do + let(:resource_with_api_support) do Class.new do def self.name - 'FooBarFactory' + 'FooBarResource' end def api_get_path @@ -33,22 +33,22 @@ describe QA::Factory::ApiFabricator do allow(subject).to receive(:current_url).and_return('') end - subject { factory.tap { |f| f.include(described_class) }.new } + subject { resource.tap { |f| f.include(described_class) }.new } describe '#api_support?' do let(:api_client) { spy('Runtime::API::Client') } let(:api_client_instance) { double('API Client') } - context 'when factory does not support fabrication via the API' do - let(:factory) { factory_without_api_support } + context 'when resource does not support fabrication via the API' do + let(:resource) { resource_without_api_support } it 'returns false' do expect(subject).not_to be_api_support end end - context 'when factory supports fabrication via the API' do - let(:factory) { factory_with_api_support } + context 'when resource supports fabrication via the API' do + let(:resource) { resource_with_api_support } it 'returns false' do expect(subject).to be_api_support @@ -67,20 +67,20 @@ describe QA::Factory::ApiFabricator do allow(api_client_instance).to receive(:personal_access_token).and_return('foo') end - context 'when factory does not support fabrication via the API' do - let(:factory) { factory_without_api_support } + context 'when resource does not support fabrication via the API' do + let(:resource) { resource_without_api_support } it 'raises a NotImplementedError exception' do - expect { subject.fabricate_via_api! }.to raise_error(NotImplementedError, "Factory FooBarFactory does not support fabrication via the API!") + expect { subject.fabricate_via_api! }.to raise_error(NotImplementedError, "Resource FooBarResource does not support fabrication via the API!") end end - context 'when factory supports fabrication via the API' do - let(:factory) { factory_with_api_support } + context 'when resource supports fabrication via the API' do + let(:resource) { resource_with_api_support } let(:api_request) { spy('Runtime::API::Request') } let(:resource_web_url) { 'http://example.org/api/v4/foo' } - let(:resource) { { id: 1, name: 'John Doe', web_url: resource_web_url } } - let(:raw_post) { double('Raw POST response', code: 201, body: resource.to_json) } + let(:response) { { id: 1, name: 'John Doe', web_url: resource_web_url } } + let(:raw_post) { double('Raw POST response', code: 201, body: response.to_json) } before do stub_const('QA::Runtime::API::Request', api_request) @@ -103,7 +103,7 @@ describe QA::Factory::ApiFabricator do it 'populates api_resource with the resource' do subject.fabricate_via_api! - expect(subject.api_resource).to eq(resource) + expect(subject.api_resource).to eq(response) end context 'when the POST fails' do @@ -114,17 +114,17 @@ describe QA::Factory::ApiFabricator do expect(api_request).to receive(:new).with(api_client_instance, subject.api_post_path).and_return(double(url: resource_web_url)) expect(subject).to receive(:post).with(resource_web_url, subject.api_post_body).and_return(raw_post) - expect { subject.fabricate_via_api! }.to raise_error(described_class::ResourceFabricationFailedError, "Fabrication of FooBarFactory using the API failed (400) with `#{raw_post}`.") + expect { subject.fabricate_via_api! }.to raise_error(described_class::ResourceFabricationFailedError, "Fabrication of FooBarResource using the API failed (400) with `#{raw_post}`.") expect(subject.api_resource).to be_nil end end end context '#transform_api_resource' do - let(:factory) do + let(:resource) do Class.new do def self.name - 'FooBarFactory' + 'FooBarResource' end def api_get_path @@ -146,12 +146,12 @@ describe QA::Factory::ApiFabricator do end end - let(:resource) { { existing: 'foo', web_url: resource_web_url } } + let(:response) { { existing: 'foo', web_url: resource_web_url } } let(:transformed_resource) { { existing: 'foo', new: 'foobar', web_url: resource_web_url } } it 'transforms the resource' do expect(subject).to receive(:post).with(resource_web_url, subject.api_post_body).and_return(raw_post) - expect(subject).to receive(:transform_api_resource).with(resource).and_return(transformed_resource) + expect(subject).to receive(:transform_api_resource).with(response).and_return(transformed_resource) subject.fabricate_via_api! end diff --git a/qa/spec/factory/base_spec.rb b/qa/spec/resource/base_spec.rb index e9584a27d63..dc9e16792d3 100644 --- a/qa/spec/factory/base_spec.rb +++ b/qa/spec/resource/base_spec.rb @@ -1,49 +1,49 @@ # frozen_string_literal: true -describe QA::Factory::Base do +describe QA::Resource::Base do include Support::StubENV - let(:factory) { spy('factory') } + let(:resource) { spy('resource') } let(:location) { 'http://location' } shared_context 'fabrication context' do subject do Class.new(described_class) do def self.name - 'MyFactory' + 'MyResource' end end end before do allow(subject).to receive(:current_url).and_return(location) - allow(subject).to receive(:new).and_return(factory) + allow(subject).to receive(:new).and_return(resource) end end shared_examples 'fabrication method' do |fabrication_method_called, actual_fabrication_method = nil| let(:fabrication_method_used) { actual_fabrication_method || fabrication_method_called } - it 'yields factory before calling factory method' do - expect(factory).to receive(:something!).ordered - expect(factory).to receive(fabrication_method_used).ordered.and_return(location) + it 'yields resource before calling resource method' do + expect(resource).to receive(:something!).ordered + expect(resource).to receive(fabrication_method_used).ordered.and_return(location) - subject.public_send(fabrication_method_called, factory: factory) do |factory| - factory.something! + subject.public_send(fabrication_method_called, resource: resource) do |resource| + resource.something! end end - it 'does not log the factory and build method when QA_DEBUG=false' do + it 'does not log the resource and build method when QA_DEBUG=false' do stub_env('QA_DEBUG', 'false') - expect(factory).to receive(fabrication_method_used).and_return(location) + expect(resource).to receive(fabrication_method_used).and_return(location) - expect { subject.public_send(fabrication_method_called, 'something', factory: factory) } + expect { subject.public_send(fabrication_method_called, 'something', resource: resource) } .not_to output.to_stdout end end describe '.fabricate!' do - context 'when factory does not support fabrication via the API' do + context 'when resource does not support fabrication via the API' do before do expect(described_class).to receive(:fabricate_via_api!).and_raise(NotImplementedError) end @@ -55,7 +55,7 @@ describe QA::Factory::Base do end end - context 'when factory supports fabrication via the API' do + context 'when resource supports fabrication via the API' do it 'calls .fabricate_via_browser_ui!' do expect(described_class).to receive(:fabricate_via_api!) @@ -69,20 +69,20 @@ describe QA::Factory::Base do it_behaves_like 'fabrication method', :fabricate_via_api! - it 'instantiates the factory, calls factory method returns the resource' do - expect(factory).to receive(:fabricate_via_api!).and_return(location) + it 'instantiates the resource, calls resource method returns the resource' do + expect(resource).to receive(:fabricate_via_api!).and_return(location) - result = subject.fabricate_via_api!(factory: factory, parents: []) + result = subject.fabricate_via_api!(resource: resource, parents: []) - expect(result).to eq(factory) + expect(result).to eq(resource) end - it 'logs the factory and build method when QA_DEBUG=true' do + it 'logs the resource and build method when QA_DEBUG=true' do stub_env('QA_DEBUG', 'true') - expect(factory).to receive(:fabricate_via_api!).and_return(location) + expect(resource).to receive(:fabricate_via_api!).and_return(location) - expect { subject.fabricate_via_api!(factory: factory, parents: []) } - .to output(/==> Built a MyFactory via api with args \[\] in [\d\w\.\-]+/) + expect { subject.fabricate_via_api!('something', resource: resource, parents: []) } + .to output(/==> Built a MyResource via api in [\d\.\-e]+ seconds+/) .to_stdout end end @@ -92,30 +92,30 @@ describe QA::Factory::Base do it_behaves_like 'fabrication method', :fabricate_via_browser_ui!, :fabricate! - it 'instantiates the factory and calls factory method' do - subject.fabricate_via_browser_ui!('something', factory: factory, parents: []) + it 'instantiates the resource and calls resource method' do + subject.fabricate_via_browser_ui!('something', resource: resource, parents: []) - expect(factory).to have_received(:fabricate!).with('something') + expect(resource).to have_received(:fabricate!).with('something') end it 'returns fabrication resource' do - result = subject.fabricate_via_browser_ui!('something', factory: factory, parents: []) + result = subject.fabricate_via_browser_ui!('something', resource: resource, parents: []) - expect(result).to eq(factory) + expect(result).to eq(resource) end - it 'logs the factory and build method when QA_DEBUG=true' do + it 'logs the resource and build method when QA_DEBUG=true' do stub_env('QA_DEBUG', 'true') - expect { subject.fabricate_via_browser_ui!('something', factory: factory, parents: []) } - .to output(/==> Built a MyFactory via browser_ui with args \["something"\] in [\d\w\.\-]+/) + expect { subject.fabricate_via_browser_ui!('something', resource: resource, parents: []) } + .to output(/==> Built a MyResource via browser_ui in [\d\.\-e]+ seconds+/) .to_stdout end end - shared_context 'simple factory' do + shared_context 'simple resource' do subject do - Class.new(QA::Factory::Base) do + Class.new(QA::Resource::Base) do attribute :test do 'block' end @@ -132,11 +132,11 @@ describe QA::Factory::Base do end end - let(:factory) { subject.new } + let(:resource) { subject.new } end describe '.attribute' do - include_context 'simple factory' + include_context 'simple resource' it 'appends new attribute' do expect(subject.attributes_names).to eq([:no_block, :test, :web_url]) @@ -144,7 +144,7 @@ describe QA::Factory::Base do context 'when the attribute is populated via a block' do it 'returns value from the block' do - result = subject.fabricate!(factory: factory) + result = subject.fabricate!(resource: resource) expect(result).to be_a(described_class) expect(result.test).to eq('block') @@ -155,11 +155,11 @@ describe QA::Factory::Base do let(:api_resource) { { no_block: 'api' } } before do - expect(factory).to receive(:api_resource).and_return(api_resource) + expect(resource).to receive(:api_resource).and_return(api_resource) end it 'returns value from api' do - result = subject.fabricate!(factory: factory) + result = subject.fabricate!(resource: resource) expect(result).to be_a(described_class) expect(result.no_block).to eq('api') @@ -173,7 +173,7 @@ describe QA::Factory::Base do end it 'returns value from api and emits an INFO log entry' do - result = subject.fabricate!(factory: factory) + result = subject.fabricate!(resource: resource) expect(result).to be_a(described_class) expect(result.test).to eq('api_with_block') @@ -185,11 +185,11 @@ describe QA::Factory::Base do context 'when the attribute is populated via direct assignment' do before do - factory.test = 'value' + resource.test = 'value' end it 'returns value from the assignment' do - result = subject.fabricate!(factory: factory) + result = subject.fabricate!(resource: resource) expect(result).to be_a(described_class) expect(result.test).to eq('value') @@ -197,11 +197,11 @@ describe QA::Factory::Base do context 'when the api also has such response' do before do - allow(factory).to receive(:api_resource).and_return({ test: 'api' }) + allow(resource).to receive(:api_resource).and_return({ test: 'api' }) end it 'returns value from the assignment' do - result = subject.fabricate!(factory: factory) + result = subject.fabricate!(resource: resource) expect(result).to be_a(described_class) expect(result.test).to eq('value') @@ -211,36 +211,36 @@ describe QA::Factory::Base do context 'when the attribute has no value' do it 'raises an error because no values could be found' do - result = subject.fabricate!(factory: factory) + result = subject.fabricate!(resource: resource) expect { result.no_block } - .to raise_error(described_class::NoValueError, "No value was computed for no_block of #{factory.class.name}.") + .to raise_error(described_class::NoValueError, "No value was computed for no_block of #{resource.class.name}.") end end end describe '#web_url' do - include_context 'simple factory' + include_context 'simple resource' it 'sets #web_url to #current_url after fabrication' do - subject.fabricate!(factory: factory) + subject.fabricate!(resource: resource) - expect(factory.web_url).to eq(subject.current_url) + expect(resource.web_url).to eq(subject.current_url) end end describe '#visit!' do - include_context 'simple factory' + include_context 'simple resource' before do - allow(factory).to receive(:visit) + allow(resource).to receive(:visit) end it 'calls #visit with the underlying #web_url' do - factory.web_url = subject.current_url - factory.visit! + resource.web_url = subject.current_url + resource.visit! - expect(factory).to have_received(:visit).with(subject.current_url) + expect(resource).to have_received(:visit).with(subject.current_url) end end end diff --git a/qa/spec/factory/repository/push_spec.rb b/qa/spec/resource/repository/push_spec.rb index 2eb6c008248..bf3ebce0cfe 100644 --- a/qa/spec/factory/repository/push_spec.rb +++ b/qa/spec/resource/repository/push_spec.rb @@ -1,6 +1,6 @@ # frozen_string_literal: true -describe QA::Factory::Repository::Push do +describe QA::Resource::Repository::Push do describe '.files=' do let(:files) do [ diff --git a/rubocop/cop/code_reuse/active_record.rb b/rubocop/cop/code_reuse/active_record.rb index d25e8548fd0..2be8f7c11aa 100644 --- a/rubocop/cop/code_reuse/active_record.rb +++ b/rubocop/cop/code_reuse/active_record.rb @@ -49,7 +49,6 @@ module RuboCop limit: true, lock: false, many?: false, - none: false, offset: true, order: true, pluck: true, diff --git a/scripts/build_assets_image b/scripts/build_assets_image new file mode 100755 index 00000000000..1d77524d503 --- /dev/null +++ b/scripts/build_assets_image @@ -0,0 +1,21 @@ +#!/bin/bash + +# Generate the image name based on the project this is being run in +ASSETS_IMAGE_NAME=$(echo ${CI_PROJECT_NAME} | + awk '{ + split($1, p, "-"); + interim = sprintf("%s-assets-%s", p[1], p[2]); + sub(/-$/, "", interim); + print interim + }' +) + +ASSETS_IMAGE_PATH=${CI_REGISTRY}/${CI_PROJECT_PATH}/${ASSETS_IMAGE_NAME} + +mkdir -p assets_container.build/public +cp -r public/assets assets_container.build/public/ +cp Dockerfile.assets assets_container.build/ +docker build -t ${ASSETS_IMAGE_PATH}:${CI_COMMIT_REF_SLUG} -f assets_container.build/Dockerfile.assets assets_container.build/ +docker login -u gitlab-ci-token -p ${CI_JOB_TOKEN} ${CI_REGISTRY} +docker push ${ASSETS_IMAGE_PATH} + diff --git a/scripts/static-analysis b/scripts/static-analysis index 0e67eabfec1..25ba7ec6c8e 100755 --- a/scripts/static-analysis +++ b/scripts/static-analysis @@ -29,6 +29,7 @@ tasks = [ %w[bin/rake lint:all], %w[bundle exec license_finder], %w[yarn run eslint], + %w[yarn run prettier-all], %w[bundle exec rubocop --parallel], %w[scripts/lint-conflicts.sh], %w[scripts/lint-rugged] diff --git a/scripts/trigger-build b/scripts/trigger-build index b76cd5dd6f0..dd0425b6472 100755 --- a/scripts/trigger-build +++ b/scripts/trigger-build @@ -32,32 +32,32 @@ module Trigger private - # Must be overriden + # Must be overridden def downstream_project_path raise NotImplementedError end - # Must be overriden + # Must be overridden def ref raise NotImplementedError end - # Must be overriden + # Must be overridden def trigger_token raise NotImplementedError end - # Must be overriden + # Must be overridden def access_token raise NotImplementedError end - # Can be overriden + # Can be overridden def extra_variables {} end - # Can be overriden + # Can be overridden def version_param_value(version_file) File.read(version_file).strip end diff --git a/spec/controllers/groups/boards_controller_spec.rb b/spec/controllers/groups/boards_controller_spec.rb index f7a4a4192d6..99429c93b82 100644 --- a/spec/controllers/groups/boards_controller_spec.rb +++ b/spec/controllers/groups/boards_controller_spec.rb @@ -32,12 +32,13 @@ describe Groups::BoardsController do end it 'renders template if visited board is not found' do - visited = double + temporary_board = create(:board, group: group) + visited = create(:board_group_recent_visit, group: temporary_board.group, board: temporary_board, user: user) + temporary_board.delete - allow(visited).to receive(:board_id).and_return(12) allow_any_instance_of(Boards::Visits::LatestService).to receive(:execute).and_return(visited) - list_boards format: :html + list_boards expect(response).to render_template :index expect(response.content_type).to eq 'text/html' diff --git a/spec/controllers/groups/milestones_controller_spec.rb b/spec/controllers/groups/milestones_controller_spec.rb index 465f3499703..42723bb3820 100644 --- a/spec/controllers/groups/milestones_controller_spec.rb +++ b/spec/controllers/groups/milestones_controller_spec.rb @@ -63,7 +63,7 @@ describe Groups::MilestonesController do let(:group_milestone) { create(:milestone, group: group) } context 'when there is a title parameter' do - it 'searchs for a legacy group milestone' do + it 'searches for a legacy group milestone' do expect(GlobalMilestone).to receive(:build) expect(Milestone).not_to receive(:find_by_iid) @@ -72,7 +72,7 @@ describe Groups::MilestonesController do end context 'when there is not a title parameter' do - it 'searchs for a group milestone' do + it 'searches for a group milestone' do expect(GlobalMilestone).not_to receive(:build) expect(Milestone).to receive(:find_by_iid) diff --git a/spec/controllers/groups_controller_spec.rb b/spec/controllers/groups_controller_spec.rb index a099cdafa58..4de61b65f71 100644 --- a/spec/controllers/groups_controller_spec.rb +++ b/spec/controllers/groups_controller_spec.rb @@ -529,7 +529,7 @@ describe GroupsController do sign_in(user) end - context 'when transfering to a subgroup goes right' do + context 'when transferring to a subgroup goes right' do let(:new_parent_group) { create(:group, :public) } let!(:group_member) { create(:group_member, :owner, group: group, user: user) } let!(:new_parent_group_member) { create(:group_member, :owner, group: new_parent_group, user: user) } diff --git a/spec/controllers/profiles/keys_controller_spec.rb b/spec/controllers/profiles/keys_controller_spec.rb index 363ed410bc0..ea26bc83353 100644 --- a/spec/controllers/profiles/keys_controller_spec.rb +++ b/spec/controllers/profiles/keys_controller_spec.rb @@ -4,7 +4,7 @@ describe Profiles::KeysController do let(:user) { create(:user) } describe "#get_keys" do - describe "non existant user" do + describe "non existent user" do it "does not generally work" do get :get_keys, username: 'not-existent' diff --git a/spec/controllers/projects/boards_controller_spec.rb b/spec/controllers/projects/boards_controller_spec.rb index 667eaa5e34f..8d503f6ad32 100644 --- a/spec/controllers/projects/boards_controller_spec.rb +++ b/spec/controllers/projects/boards_controller_spec.rb @@ -38,9 +38,10 @@ describe Projects::BoardsController do end it 'renders template if visited board is not found' do - visited = double + temporary_board = create(:board, project: project) + visited = create(:board_project_recent_visit, project: temporary_board.project, board: temporary_board, user: user) + temporary_board.delete - allow(visited).to receive(:board_id).and_return(12) allow_any_instance_of(Boards::Visits::LatestService).to receive(:execute).and_return(visited) list_boards diff --git a/spec/controllers/projects/clusters/applications_controller_spec.rb b/spec/controllers/projects/clusters/applications_controller_spec.rb index 9e17e392d3d..8106453a775 100644 --- a/spec/controllers/projects/clusters/applications_controller_spec.rb +++ b/spec/controllers/projects/clusters/applications_controller_spec.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + require 'spec_helper' describe Projects::Clusters::ApplicationsController do diff --git a/spec/controllers/projects/clusters_controller_spec.rb b/spec/controllers/projects/clusters_controller_spec.rb index 9201332c5c8..04aece26590 100644 --- a/spec/controllers/projects/clusters_controller_spec.rb +++ b/spec/controllers/projects/clusters_controller_spec.rb @@ -1,8 +1,11 @@ +# frozen_string_literal: true + require 'spec_helper' describe Projects::ClustersController do include AccessMatchersForController include GoogleApi::CloudPlatformHelpers + include KubernetesHelpers set(:project) { create(:project) } @@ -218,9 +221,9 @@ describe Projects::ClustersController do describe 'security' do before do allow_any_instance_of(described_class) - .to receive(:token_in_session).and_return('token') + .to receive(:token_in_session).and_return('token') allow_any_instance_of(described_class) - .to receive(:expires_at_in_session).and_return(1.hour.since.to_i.to_s) + .to receive(:expires_at_in_session).and_return(1.hour.since.to_i.to_s) allow_any_instance_of(GoogleApi::CloudPlatform::Client) .to receive(:projects_zones_clusters_create) do OpenStruct.new( @@ -307,6 +310,11 @@ describe Projects::ClustersController do end describe 'security' do + before do + allow(ClusterPlatformConfigureWorker).to receive(:perform_async) + stub_kubeclient_get_namespace('https://kubernetes.example.com', namespace: 'my-namespace') + end + it { expect { go }.to be_allowed_for(:admin) } it { expect { go }.to be_allowed_for(:owner).of(project) } it { expect { go }.to be_allowed_for(:maintainer).of(project) } @@ -318,14 +326,15 @@ describe Projects::ClustersController do end end - describe 'GET status' do + describe 'GET cluster_status' do let(:cluster) { create(:cluster, :providing_by_gcp, projects: [project]) } def go - get :status, namespace_id: project.namespace, - project_id: project, - id: cluster, - format: :json + get :cluster_status, + namespace_id: project.namespace.to_param, + project_id: project.to_param, + id: cluster, + format: :json end describe 'functionality' do @@ -359,9 +368,10 @@ describe Projects::ClustersController do let(:cluster) { create(:cluster, :provided_by_gcp, projects: [project]) } def go - get :show, namespace_id: project.namespace, - project_id: project, - id: cluster + get :show, + namespace_id: project.namespace, + project_id: project, + id: cluster end describe 'functionality' do @@ -401,13 +411,18 @@ describe Projects::ClustersController do end def go(format: :html) - put :update, params.merge(namespace_id: project.namespace, - project_id: project, + put :update, params.merge(namespace_id: project.namespace.to_param, + project_id: project.to_param, id: cluster, format: format ) end + before do + allow(ClusterPlatformConfigureWorker).to receive(:perform_async) + stub_kubeclient_get_namespace('https://kubernetes.example.com', namespace: 'my-namespace') + end + context 'when cluster is provided by GCP' do it "updates and redirects back to show page" do go @@ -530,9 +545,10 @@ describe Projects::ClustersController do let!(:cluster) { create(:cluster, :provided_by_gcp, :production_environment, projects: [project]) } def go - delete :destroy, namespace_id: project.namespace, - project_id: project, - id: cluster + delete :destroy, + namespace_id: project.namespace, + project_id: project, + id: cluster end describe 'functionality' do @@ -591,4 +607,10 @@ describe Projects::ClustersController do it { expect { go }.to be_denied_for(:external) } end end + + context 'no project_id param' do + it 'does not respond to any action without project_id param' do + expect { get :index }.to raise_error(ActionController::UrlGenerationError) + end + end end diff --git a/spec/controllers/projects/commit_controller_spec.rb b/spec/controllers/projects/commit_controller_spec.rb index 9e149bc4c3c..e34fdee62d6 100644 --- a/spec/controllers/projects/commit_controller_spec.rb +++ b/spec/controllers/projects/commit_controller_spec.rb @@ -356,6 +356,7 @@ describe Projects::CommitController do 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(response).to include_pagination_headers end end end diff --git a/spec/controllers/projects/deployments_controller_spec.rb b/spec/controllers/projects/deployments_controller_spec.rb index d1c960e895d..5b7da81b6a1 100644 --- a/spec/controllers/projects/deployments_controller_spec.rb +++ b/spec/controllers/projects/deployments_controller_spec.rb @@ -15,9 +15,9 @@ describe Projects::DeploymentsController do describe 'GET #index' do it 'returns list of deployments from last 8 hours' do - create(:deployment, environment: environment, created_at: 9.hours.ago) - create(:deployment, environment: environment, created_at: 7.hours.ago) - create(:deployment, environment: environment) + create(:deployment, :success, environment: environment, created_at: 9.hours.ago) + create(:deployment, :success, environment: environment, created_at: 7.hours.ago) + create(:deployment, :success, environment: environment) get :index, deployment_params(after: 8.hours.ago) @@ -27,7 +27,7 @@ describe Projects::DeploymentsController do end it 'returns a list with deployments information' do - create(:deployment, environment: environment) + create(:deployment, :success, environment: environment) get :index, deployment_params @@ -37,7 +37,7 @@ describe Projects::DeploymentsController do end describe 'GET #metrics' do - let(:deployment) { create(:deployment, project: project, environment: environment) } + let(:deployment) { create(:deployment, :success, project: project, environment: environment) } before do allow(controller).to receive(:deployment).and_return(deployment) @@ -110,7 +110,7 @@ describe Projects::DeploymentsController do end describe 'GET #additional_metrics' do - let(:deployment) { create(:deployment, project: project, environment: environment) } + let(:deployment) { create(:deployment, :success, project: project, environment: environment) } before do allow(controller).to receive(:deployment).and_return(deployment) diff --git a/spec/controllers/projects/environments_controller_spec.rb b/spec/controllers/projects/environments_controller_spec.rb index b86029a4baf..bc17331f531 100644 --- a/spec/controllers/projects/environments_controller_spec.rb +++ b/spec/controllers/projects/environments_controller_spec.rb @@ -216,7 +216,7 @@ describe Projects::EnvironmentsController do expect(response).to have_gitlab_http_status(200) end - it 'loads the terminals for the enviroment' do + it 'loads the terminals for the environment' do expect_any_instance_of(Environment).to receive(:terminals) get :terminal, environment_params diff --git a/spec/controllers/projects/jobs_controller_spec.rb b/spec/controllers/projects/jobs_controller_spec.rb index 2023d4b0bd0..da3d658d061 100644 --- a/spec/controllers/projects/jobs_controller_spec.rb +++ b/spec/controllers/projects/jobs_controller_spec.rb @@ -152,11 +152,33 @@ describe Projects::JobsController, :clean_gitlab_redis_shared_state do expect(response).to have_gitlab_http_status(:ok) expect(response).to match_response_schema('job/job_details') expect(json_response['raw_path']).to match(%r{jobs/\d+/raw\z}) - expect(json_response.dig('merge_request', 'path')).to match(%r{merge_requests/\d+\z}) + expect(json_response['merge_request']['path']).to match(%r{merge_requests/\d+\z}) expect(json_response['new_issue_path']).to include('/issues/new') end end + context 'when job is running' do + context 'job is cancelable' do + let(:job) { create(:ci_build, :running, pipeline: pipeline) } + + it 'cancel_path is present with correct redirect' do + expect(response).to have_gitlab_http_status(:ok) + expect(response).to match_response_schema('job/job_details') + expect(json_response['cancel_path']).to include(CGI.escape(json_response['build_path'])) + end + end + + context 'with web terminal' do + let(:job) { create(:ci_build, :running, :with_runner_session, pipeline: pipeline) } + + it 'exposes the terminal path' do + expect(response).to have_gitlab_http_status(:ok) + expect(response).to match_response_schema('job/job_details') + expect(json_response['terminal_path']).to match(%r{/terminal}) + end + end + end + context 'when job has artifacts' do context 'with not expiry date' do let(:job) { create(:ci_build, :success, :artifacts, pipeline: pipeline) } @@ -185,16 +207,6 @@ describe Projects::JobsController, :clean_gitlab_redis_shared_state do end end - context 'when job has terminal' do - let(:job) { create(:ci_build, :running, :with_runner_session, pipeline: pipeline) } - - it 'exposes the terminal path' do - expect(response).to have_gitlab_http_status(:ok) - expect(response).to match_response_schema('job/job_details') - expect(json_response['terminal_path']).to match(%r{/terminal}) - end - end - context 'when job passed with no trace' do let(:job) { create(:ci_build, :success, :artifacts, pipeline: pipeline) } @@ -219,7 +231,7 @@ describe Projects::JobsController, :clean_gitlab_redis_shared_state do context 'with deployment' do let(:merge_request) { create(:merge_request, source_project: project) } let(:environment) { create(:environment, project: project, name: 'staging', state: :available) } - let(:job) { create(:ci_build, :success, environment: environment.name, pipeline: pipeline) } + let(:job) { create(:ci_build, :running, environment: environment.name, pipeline: pipeline) } it 'exposes the deployment information' do expect(response).to have_gitlab_http_status(:ok) diff --git a/spec/controllers/projects/merge_requests_controller_spec.rb b/spec/controllers/projects/merge_requests_controller_spec.rb index 7b0459e0325..e62523c65c9 100644 --- a/spec/controllers/projects/merge_requests_controller_spec.rb +++ b/spec/controllers/projects/merge_requests_controller_spec.rb @@ -563,6 +563,7 @@ describe Projects::MergeRequestsController do it 'responds with serialized pipelines' do expect(json_response['pipelines']).not_to be_empty expect(json_response['count']['all']).to eq 1 + expect(response).to include_pagination_headers end end @@ -754,7 +755,7 @@ describe Projects::MergeRequestsController do let(:environment) { create(:environment, project: forked) } let(:pipeline) { create(:ci_pipeline, sha: sha, project: forked) } let(:build) { create(:ci_build, pipeline: pipeline) } - let!(:deployment) { create(:deployment, environment: environment, sha: sha, ref: 'master', deployable: build) } + let!(:deployment) { create(:deployment, :succeed, environment: environment, sha: sha, ref: 'master', deployable: build) } let(:merge_request) do create(:merge_request, source_project: forked, target_project: project, target_branch: 'master', head_pipeline: pipeline) @@ -779,14 +780,14 @@ describe Projects::MergeRequestsController do let(:merge_commit_sha) { project.repository.merge(user, forked.commit.id, merge_request, "merged in test") } let(:post_merge_pipeline) { create(:ci_pipeline, sha: merge_commit_sha, project: project) } let(:post_merge_build) { create(:ci_build, pipeline: post_merge_pipeline) } - let!(:source_deployment) { create(:deployment, environment: source_environment, sha: merge_commit_sha, ref: 'master', deployable: post_merge_build) } + let!(:source_deployment) { create(:deployment, :succeed, environment: source_environment, sha: merge_commit_sha, ref: 'master', deployable: post_merge_build) } before do merge_request.update!(merge_commit_sha: merge_commit_sha) merge_request.mark_as_merged! end - it 'returns the enviroment on the source project' do + it 'returns the environment on the source project' do get_ci_environments_status(environment_target: 'merge_commit') expect(response).to have_gitlab_http_status(:ok) diff --git a/spec/factories/ci/builds.rb b/spec/factories/ci/builds.rb index 85ba7d4097d..90754319f05 100644 --- a/spec/factories/ci/builds.rb +++ b/spec/factories/ci/builds.rb @@ -27,6 +27,12 @@ FactoryBot.define do pipeline factory: :ci_pipeline + trait :degenerated do + commands nil + options nil + yaml_variables nil + end + trait :started do started_at 'Di 29. Okt 09:51:28 CET 2013' end @@ -94,6 +100,30 @@ FactoryBot.define do url: 'http://staging.example.com/$CI_JOB_NAME' } end + trait :deploy_to_production do + environment 'production' + + options environment: { name: 'production', + url: 'http://prd.example.com/$CI_JOB_NAME' } + end + + trait :start_review_app do + environment 'review/$CI_COMMIT_REF_NAME' + + options environment: { name: 'review/$CI_COMMIT_REF_NAME', + url: 'http://staging.example.com/$CI_JOB_NAME', + on_stop: 'stop_review_app' } + end + + trait :stop_review_app do + name 'stop_review_app' + environment 'review/$CI_COMMIT_REF_NAME' + + options environment: { name: 'review/$CI_COMMIT_REF_NAME', + url: 'http://staging.example.com/$CI_JOB_NAME', + action: 'stop' } + end + trait :allowed_to_fail do allow_failure true end diff --git a/spec/factories/clusters/kubernetes_namespaces.rb b/spec/factories/clusters/kubernetes_namespaces.rb index 6fdada75a3d..3f10f0ecc74 100644 --- a/spec/factories/clusters/kubernetes_namespaces.rb +++ b/spec/factories/clusters/kubernetes_namespaces.rb @@ -2,8 +2,18 @@ FactoryBot.define do factory :cluster_kubernetes_namespace, class: Clusters::KubernetesNamespace do - cluster - project - cluster_project + association :cluster, :project, :provided_by_gcp + namespace { |n| "environment#{n}" } + + after(:build) do |kubernetes_namespace| + cluster_project = kubernetes_namespace.cluster.cluster_project + + kubernetes_namespace.project = cluster_project.project + kubernetes_namespace.cluster_project = cluster_project + end + + trait :with_token do + service_account_token { Faker::Lorem.characters(10) } + end end end diff --git a/spec/factories/deployments.rb b/spec/factories/deployments.rb index 90d6a338479..011c98599a3 100644 --- a/spec/factories/deployments.rb +++ b/spec/factories/deployments.rb @@ -21,5 +21,31 @@ FactoryBot.define do sha { TestEnv::BRANCH_SHA['pages-deploy'] } ref 'pages-deploy' end + + trait :running do + status :running + end + + trait :success do + status :success + finished_at { Time.now } + end + + trait :failed do + status :failed + finished_at { Time.now } + end + + trait :canceled do + status :canceled + finished_at { Time.now } + end + + # This trait hooks the state maechine's events + trait :succeed do + after(:create) do |deployment, evaluator| + deployment.succeed! + end + end end end diff --git a/spec/factories/environments.rb b/spec/factories/environments.rb index b5db57d5148..9d9e3d693b8 100644 --- a/spec/factories/environments.rb +++ b/spec/factories/environments.rb @@ -22,6 +22,7 @@ FactoryBot.define do pipeline: pipeline) deployment = create(:deployment, + :success, environment: environment, project: environment.project, deployable: deployable, diff --git a/spec/features/calendar_spec.rb b/spec/features/calendar_spec.rb index aa3ca8923ff..a1f93bd3fbd 100644 --- a/spec/features/calendar_spec.rb +++ b/spec/features/calendar_spec.rb @@ -153,7 +153,7 @@ describe 'Contributions Calendar', :js do include_context 'visit user page' it 'displays calendar activity log' do - expect(find('.tab-pane#activity .content_list .event-note')).to have_content issue_title + expect(find('.tab-pane#activity .content_list .event-target-title')).to have_content issue_title end end end diff --git a/spec/features/dashboard/archived_projects_spec.rb b/spec/features/dashboard/archived_projects_spec.rb index 6a0cd848345..d31df322d10 100644 --- a/spec/features/dashboard/archived_projects_spec.rb +++ b/spec/features/dashboard/archived_projects_spec.rb @@ -33,7 +33,7 @@ RSpec.describe 'Dashboard Archived Project' do expect(page).not_to have_content(project.name) end - it 'searchs archived projects', :js do + it 'searches archived projects', :js do click_button 'Last updated' click_link 'Show archived projects' diff --git a/spec/features/dashboard/project_member_activity_index_spec.rb b/spec/features/dashboard/project_member_activity_index_spec.rb index 498775acff3..16919fe63ad 100644 --- a/spec/features/dashboard/project_member_activity_index_spec.rb +++ b/spec/features/dashboard/project_member_activity_index_spec.rb @@ -14,14 +14,15 @@ describe 'Project member activity', :js do wait_for_requests end - subject { page.find(".event-title").text } - context 'when a user joins the project' do before do visit_activities_and_wait_with_event(Event::JOINED) end - it { is_expected.to eq("#{user.name} joined project") } + it "presents the correct message" do + expect(page.find('.event-user-info').text).to eq("#{user.name} #{user.to_reference}") + expect(page.find('.event-title').text).to eq("joined project") + end end context 'when a user leaves the project' do @@ -29,7 +30,10 @@ describe 'Project member activity', :js do visit_activities_and_wait_with_event(Event::LEFT) end - it { is_expected.to eq("#{user.name} left project") } + it "presents the correct message" do + expect(page.find('.event-user-info').text).to eq("#{user.name} #{user.to_reference}") + expect(page.find('.event-title').text).to eq("left project") + end end context 'when a users membership expires for the project' do @@ -38,8 +42,8 @@ describe 'Project member activity', :js do end it "presents the correct message" do - message = "#{user.name} removed due to membership expiration from project" - is_expected.to eq(message) + expect(page.find('.event-user-info').text).to eq("#{user.name} #{user.to_reference}") + expect(page.find('.event-title').text).to eq("removed due to membership expiration from project") end end end diff --git a/spec/features/groups/board_sidebar_spec.rb b/spec/features/groups/board_sidebar_spec.rb new file mode 100644 index 00000000000..9f597efa7b7 --- /dev/null +++ b/spec/features/groups/board_sidebar_spec.rb @@ -0,0 +1,45 @@ +# frozen_string_literal: true + +require 'rails_helper' + +describe 'Group Issue Boards', :js do + include BoardHelpers + + let(:group) { create(:group) } + let(:user) { create(:group_member, user: create(:user), group: group ).user } + let!(:project_1) { create(:project, :public, group: group) } + let!(:project_2) { create(:project, :public, group: group) } + let!(:project_1_label) { create(:label, project: project_1, name: 'Development 1') } + let!(:project_2_label) { create(:label, project: project_2, name: 'Development 2') } + let!(:group_label) { create(:group_label, title: 'Bug', description: 'Fusce consequat', group: group) } + let!(:issue_1) { create(:labeled_issue, project: project_1, relative_position: 1) } + let!(:issue_2) { create(:labeled_issue, project: project_2, relative_position: 2) } + let(:board) { create(:board, group: group) } + let!(:list) { create(:list, board: board, label: project_1_label, position: 0) } + let(:card) { find('.board:nth-child(1)').first('.board-card') } + + before do + sign_in(user) + + visit group_board_path(group, board) + wait_for_requests + end + + context 'labels' do + it 'only shows valid labels for the issue project and group' do + click_card(card) + + page.within('.labels') do + click_link 'Edit' + + wait_for_requests + + page.within('.selectbox') do + expect(page).to have_content(project_1_label.title) + expect(page).to have_content(group_label.title) + expect(page).not_to have_content(project_2_label.title) + end + end + end + end +end diff --git a/spec/features/import/manifest_import_spec.rb b/spec/features/import/manifest_import_spec.rb index e381d073804..a90cdd8d920 100644 --- a/spec/features/import/manifest_import_spec.rb +++ b/spec/features/import/manifest_import_spec.rb @@ -22,7 +22,7 @@ describe 'Import multiple repositories by uploading a manifest file', :js, :post expect(page).to have_content('https://android-review.googlesource.com/platform/build/blueprint') end - it 'imports succesfully imports a project' do + it 'imports successfully imports a project' do visit new_import_manifest_path attach_file('manifest', Rails.root.join('spec/fixtures/aosp_manifest.xml')) diff --git a/spec/features/issues/filtered_search/dropdown_emoji_spec.rb b/spec/features/issues/filtered_search/dropdown_emoji_spec.rb index be229e8aa7d..c42fcd92a36 100644 --- a/spec/features/issues/filtered_search/dropdown_emoji_spec.rb +++ b/spec/features/issues/filtered_search/dropdown_emoji_spec.rb @@ -92,7 +92,7 @@ describe 'Dropdown emoji', :js do it 'shows the most populated emoji at top of dropdown' do send_keys_to_filtered_search('my-reaction:') - expect(first('#js-dropdown-my-reaction li')).to have_content(award_emoji_star.name) + expect(first('#js-dropdown-my-reaction .filter-dropdown li')).to have_content(award_emoji_star.name) end end @@ -121,13 +121,29 @@ describe 'Dropdown emoji', :js do send_keys_to_filtered_search(':') end + it 'selects `None`' do + find('#js-dropdown-my-reaction .filter-dropdown-item', text: 'None').click + + expect(page).to have_css(js_dropdown_emoji, visible: false) + expect_tokens([reaction_token('none', false)]) + expect_filtered_search_input_empty + end + + it 'selects `Any`' do + find('#js-dropdown-my-reaction .filter-dropdown-item', text: 'Any').click + + expect(page).to have_css(js_dropdown_emoji, visible: false) + expect_tokens([reaction_token('any', false)]) + expect_filtered_search_input_empty + end + it 'fills in the my-reaction name' do click_emoji('thumbsup') wait_for_requests expect(page).to have_css(js_dropdown_emoji, visible: false) - expect_tokens([emoji_token('thumbsup')]) + expect_tokens([reaction_token('thumbsup')]) expect_filtered_search_input_empty end end diff --git a/spec/features/issues/gfm_autocomplete_spec.rb b/spec/features/issues/gfm_autocomplete_spec.rb index 593dc6b6690..605860b90cd 100644 --- a/spec/features/issues/gfm_autocomplete_spec.rb +++ b/spec/features/issues/gfm_autocomplete_spec.rb @@ -15,7 +15,7 @@ describe 'GFM autocomplete', :js do wait_for_requests end - it 'updates issue descripton with GFM reference' do + it 'updates issue description with GFM reference' do find('.js-issuable-edit').click simulate_input('#issue-description', "@#{user.name[0...3]}") diff --git a/spec/features/merge_request/user_sees_deployment_widget_spec.rb b/spec/features/merge_request/user_sees_deployment_widget_spec.rb index a298ead43db..0e439c8cb2d 100644 --- a/spec/features/merge_request/user_sees_deployment_widget_spec.rb +++ b/spec/features/merge_request/user_sees_deployment_widget_spec.rb @@ -11,7 +11,7 @@ describe 'Merge request > User sees deployment widget', :js do let(:sha) { project.commit(ref).id } let(:pipeline) { create(:ci_pipeline_without_jobs, sha: sha, project: project, ref: ref) } let(:build) { create(:ci_build, :success, pipeline: pipeline) } - let!(:deployment) { create(:deployment, environment: environment, sha: sha, ref: ref, deployable: build) } + let!(:deployment) { create(:deployment, :succeed, environment: environment, sha: sha, ref: ref, deployable: build) } let!(:manual) { } before do @@ -38,7 +38,7 @@ describe 'Merge request > User sees deployment widget', :js do end it 'does start build when stop button clicked' do - accept_confirm { click_button('Stop environment') } + accept_confirm { find('.js-stop-env').click } expect(page).to have_content('close_app') end @@ -47,7 +47,7 @@ describe 'Merge request > User sees deployment widget', :js do let(:role) { :reporter } it 'does not show stop button' do - expect(page).not_to have_button('Stop environment') + expect(page).not_to have_selector('.js-stop-env') end end end diff --git a/spec/features/merge_request/user_sees_merge_widget_spec.rb b/spec/features/merge_request/user_sees_merge_widget_spec.rb index 0c610edd6d1..d907ed4198c 100644 --- a/spec/features/merge_request/user_sees_merge_widget_spec.rb +++ b/spec/features/merge_request/user_sees_merge_widget_spec.rb @@ -45,7 +45,8 @@ describe 'Merge request > User sees merge widget', :js do let(:build) { create(:ci_build, :success, pipeline: pipeline) } let!(:deployment) do - create(:deployment, environment: environment, + create(:deployment, :succeed, + environment: environment, ref: merge_request.source_branch, deployable: build, sha: sha) @@ -179,7 +180,7 @@ describe 'Merge request > User sees merge widget', :js do # Wait for the `ci_status` and `merge_check` requests wait_for_requests - expect(page).to have_text(%r{Could not retrieve the pipeline status\. For troubleshooting steps, read the <a href=\".+\">documentation\.</a>}) + expect(page).to have_text("Could not retrieve the pipeline status. For troubleshooting steps, read the documentation.") end end diff --git a/spec/features/merge_request/user_sees_pipelines_spec.rb b/spec/features/merge_request/user_sees_pipelines_spec.rb index 41f447fba95..8faddee4daa 100644 --- a/spec/features/merge_request/user_sees_pipelines_spec.rb +++ b/spec/features/merge_request/user_sees_pipelines_spec.rb @@ -41,8 +41,7 @@ describe 'Merge request > User sees pipelines', :js do visit project_merge_request_path(project, merge_request) wait_for_requests - expect(page.find('.ci-widget')).to have_text( - %r{Could not retrieve the pipeline status\. For troubleshooting steps, read the <a href=\".+\">documentation\.</a>}) + expect(page.find('.ci-widget')).to have_text("Could not retrieve the pipeline status. For troubleshooting steps, read the documentation.") end end diff --git a/spec/features/milestones/user_creates_milestone_spec.rb b/spec/features/milestones/user_creates_milestone_spec.rb index 8fd057d587c..5de0c381cdf 100644 --- a/spec/features/milestones/user_creates_milestone_spec.rb +++ b/spec/features/milestones/user_creates_milestone_spec.rb @@ -24,6 +24,6 @@ describe "User creates milestone", :js do visit(activity_project_path(project)) - expect(page).to have_content("#{user.name} opened milestone") + expect(page).to have_content("#{user.name} #{user.to_reference} opened milestone") end end diff --git a/spec/features/milestones/user_deletes_milestone_spec.rb b/spec/features/milestones/user_deletes_milestone_spec.rb index a8c296b4cd2..f68ed1cde07 100644 --- a/spec/features/milestones/user_deletes_milestone_spec.rb +++ b/spec/features/milestones/user_deletes_milestone_spec.rb @@ -23,7 +23,7 @@ describe "User deletes milestone", :js do visit(activity_project_path(project)) - expect(page).to have_content("#{user.name} destroyed milestone") + expect(page).to have_content("#{user.name} #{user.to_reference} destroyed milestone") end end diff --git a/spec/features/projects/activity/user_sees_activity_spec.rb b/spec/features/projects/activity/user_sees_activity_spec.rb index ebaa137772d..bb4b2abc3c7 100644 --- a/spec/features/projects/activity/user_sees_activity_spec.rb +++ b/spec/features/projects/activity/user_sees_activity_spec.rb @@ -19,13 +19,13 @@ describe 'Projects > Activity > User sees activity' do it 'shows the last push in the activity page', :js do visit activity_project_path(project) - expect(page).to have_content "#{user.name} pushed new branch fix" + expect(page).to have_content "#{user.name} #{user.to_reference} pushed new branch fix" end it 'allows to filter event with the "event_filter=issue" URL param', :js do visit activity_project_path(project, event_filter: 'issue') - expect(page).not_to have_content "#{user.name} pushed new branch fix" - expect(page).to have_content "#{user.name} opened issue #{issue.to_reference}" + expect(page).not_to have_content "#{user.name} #{user.to_reference} pushed new branch fix" + expect(page).to have_content "#{user.name} #{user.to_reference} opened issue #{issue.to_reference}" end end diff --git a/spec/features/projects/activity/user_sees_private_activity_spec.rb b/spec/features/projects/activity/user_sees_private_activity_spec.rb index d7dc0a6712a..61ec2ce9d29 100644 --- a/spec/features/projects/activity/user_sees_private_activity_spec.rb +++ b/spec/features/projects/activity/user_sees_private_activity_spec.rb @@ -5,7 +5,7 @@ describe 'Project > Activity > User sees private activity', :js do let(:author) { create(:user) } let(:user) { create(:user) } let(:issue) { create(:issue, :confidential, project: project, author: author) } - let(:message) { "#{author.name} opened issue #{issue.to_reference}" } + let(:message) { "#{author.name} #{author.to_reference} opened issue #{issue.to_reference}" } before do project.add_developer(author) diff --git a/spec/features/projects/badges/pipeline_badge_spec.rb b/spec/features/projects/badges/pipeline_badge_spec.rb index 8c4488b2ca6..dee81898928 100644 --- a/spec/features/projects/badges/pipeline_badge_spec.rb +++ b/spec/features/projects/badges/pipeline_badge_spec.rb @@ -19,7 +19,7 @@ describe 'Pipeline Badge' do let!(:pipeline) { create(:ci_empty_pipeline, project: project, ref: ref, sha: project.commit(ref).sha) } let!(:job) { create(:ci_build, pipeline: pipeline) } - context 'when the pipeline was successfull' do + context 'when the pipeline was successful' do it 'displays so on the badge' do job.success diff --git a/spec/features/projects/clusters/gcp_spec.rb b/spec/features/projects/clusters/gcp_spec.rb index 8b92b9fc869..3d17eb3a73a 100644 --- a/spec/features/projects/clusters/gcp_spec.rb +++ b/spec/features/projects/clusters/gcp_spec.rb @@ -130,6 +130,7 @@ describe 'Gcp Cluster', :js do context 'when user changes cluster parameters' do before do + allow(ClusterPlatformConfigureWorker).to receive(:perform_async) fill_in 'cluster_platform_kubernetes_attributes_namespace', with: 'my-namespace' page.within('#js-cluster-details') { click_button 'Save changes' } end diff --git a/spec/features/projects/clusters/user_spec.rb b/spec/features/projects/clusters/user_spec.rb index 9ae1dba60b5..250c964cc32 100644 --- a/spec/features/projects/clusters/user_spec.rb +++ b/spec/features/projects/clusters/user_spec.rb @@ -9,7 +9,9 @@ describe 'User Cluster', :js do before do project.add_maintainer(user) gitlab_sign_in(user) + allow(Projects::ClustersController).to receive(:STATUS_POLLING_INTERVAL) { 100 } + allow_any_instance_of(Clusters::Gcp::Kubernetes::CreateOrUpdateNamespaceService).to receive(:execute) end context 'when user does not have a cluster and visits cluster index page' do diff --git a/spec/features/projects/clusters_spec.rb b/spec/features/projects/clusters_spec.rb index 91eac9c8278..f13c35c00d3 100644 --- a/spec/features/projects/clusters_spec.rb +++ b/spec/features/projects/clusters_spec.rb @@ -40,7 +40,7 @@ describe 'Clusters', :js do expect(page).to have_selector('.js-project-feature-toggle') end - context 'with sucessfull request' do + context 'with successful request' do it 'user sees updated cluster' do expect do page.find('.js-project-feature-toggle').click diff --git a/spec/features/projects/environments/environment_spec.rb b/spec/features/projects/environments/environment_spec.rb index 70e0879dd81..056f4ee2e22 100644 --- a/spec/features/projects/environments/environment_spec.rb +++ b/spec/features/projects/environments/environment_spec.rb @@ -33,7 +33,7 @@ describe 'Environment' do context 'with deployments' do context 'when there is no related deployable' do let(:deployment) do - create(:deployment, environment: environment, deployable: nil) + create(:deployment, :success, environment: environment, deployable: nil) end it 'does show deployment SHA' do @@ -48,15 +48,26 @@ describe 'Environment' do let(:build) { create(:ci_build, pipeline: pipeline) } let(:deployment) do - create(:deployment, environment: environment, deployable: build) + create(:deployment, :success, environment: environment, deployable: build) end it 'does show build name' do expect(page).to have_link("#{build.name} (##{build.id})") - expect(page).to have_link('Re-deploy') + expect(page).not_to have_link('Re-deploy') expect(page).not_to have_terminal_button end + context 'when user has ability to re-deploy' do + let(:permissions) do + create(:protected_branch, :developers_can_merge, + name: build.ref, project: project) + end + + it 'does show re-deploy' do + expect(page).to have_link('Re-deploy') + end + end + context 'with manual action' do let(:action) do create(:ci_build, :manual, pipeline: pipeline, @@ -97,7 +108,7 @@ describe 'Environment' do context 'with external_url' do let(:environment) { create(:environment, project: project, external_url: 'https://git.gitlab.com') } let(:build) { create(:ci_build, pipeline: pipeline) } - let(:deployment) { create(:deployment, environment: environment, deployable: build) } + let(:deployment) { create(:deployment, :success, environment: environment, deployable: build) } it 'does show an external link button' do expect(page).to have_link(nil, href: environment.external_url) @@ -158,7 +169,8 @@ describe 'Environment' do end let(:deployment) do - create(:deployment, environment: environment, + create(:deployment, :success, + environment: environment, deployable: build, on_stop: 'close_app') end diff --git a/spec/features/projects/environments/environments_spec.rb b/spec/features/projects/environments/environments_spec.rb index 917ba495f01..d0ddf69d574 100644 --- a/spec/features/projects/environments/environments_spec.rb +++ b/spec/features/projects/environments/environments_spec.rb @@ -132,7 +132,8 @@ describe 'Environments page', :js do let(:project) { create(:project, :repository) } let!(:deployment) do - create(:deployment, environment: environment, + create(:deployment, :success, + environment: environment, sha: project.commit.id) end @@ -152,7 +153,8 @@ describe 'Environments page', :js do end let!(:deployment) do - create(:deployment, environment: environment, + create(:deployment, :success, + environment: environment, deployable: build, sha: project.commit.id) end @@ -162,7 +164,7 @@ describe 'Environments page', :js do end it 'shows a play button' do - find('.js-dropdown-play-icon-container').click + find('.js-environment-actions-dropdown').click expect(page).to have_content(action.name.humanize) end @@ -170,7 +172,7 @@ describe 'Environments page', :js do it 'allows to play a manual action', :js do expect(action).to be_manual - find('.js-dropdown-play-icon-container').click + find('.js-environment-actions-dropdown').click expect(page).to have_content(action.name.humanize) expect { find('.js-manual-action-link').click } @@ -196,7 +198,7 @@ describe 'Environments page', :js do context 'with external_url' do let(:environment) { create(:environment, project: project, external_url: 'https://git.gitlab.com') } let(:build) { create(:ci_build, pipeline: pipeline) } - let(:deployment) { create(:deployment, environment: environment, deployable: build) } + let(:deployment) { create(:deployment, :success, environment: environment, deployable: build) } it 'shows an external link button' do expect(page).to have_link(nil, href: environment.external_url) @@ -209,7 +211,8 @@ describe 'Environments page', :js do end let(:deployment) do - create(:deployment, environment: environment, + create(:deployment, :success, + environment: environment, deployable: build, on_stop: 'close_app') end @@ -260,6 +263,70 @@ describe 'Environments page', :js do end end end + + context 'when there is a delayed job' do + let!(:pipeline) { create(:ci_pipeline, project: project) } + let!(:build) { create(:ci_build, pipeline: pipeline) } + + let!(:delayed_job) do + create(:ci_build, :scheduled, + pipeline: pipeline, + name: 'delayed job', + stage: 'test', + commands: 'test') + end + + let!(:deployment) do + create(:deployment, + :success, + environment: environment, + deployable: build, + sha: project.commit.id) + end + + before do + visit_environments(project) + end + + it 'has a dropdown for actionable jobs' do + expect(page).to have_selector('.dropdown-new.btn.btn-default .ic-play') + end + + it "has link to the delayed job's action" do + find('.js-environment-actions-dropdown').click + + expect(page).to have_button('Delayed job') + expect(page).to have_content(/\d{2}:\d{2}:\d{2}/) + end + + context 'when delayed job is expired already' do + let!(:delayed_job) do + create(:ci_build, :expired_scheduled, + pipeline: pipeline, + name: 'delayed job', + stage: 'test', + commands: 'test') + end + + it "shows 00:00:00 as the remaining time" do + find('.js-environment-actions-dropdown').click + + expect(page).to have_content("00:00:00") + end + end + + context 'when user played a delayed job immediately' do + before do + find('.js-environment-actions-dropdown').click + page.accept_confirm { click_button('Delayed job') } + wait_for_requests + end + + it 'enqueues the delayed job', :js do + expect(delayed_job.reload).to be_pending + end + end + end end end diff --git a/spec/features/projects/files/user_creates_files_spec.rb b/spec/features/projects/files/user_creates_files_spec.rb index d9df4b50621..14b5bd58bd1 100644 --- a/spec/features/projects/files/user_creates_files_spec.rb +++ b/spec/features/projects/files/user_creates_files_spec.rb @@ -16,7 +16,7 @@ describe 'Projects > Files > User creates files' do sign_in(user) end - context 'without commiting a new file' do + context 'without committing a new file' do context 'when an user has write access' do before do visit(project_tree_path_root_ref) @@ -49,7 +49,7 @@ describe 'Projects > Files > User creates files' do end end - context 'with commiting a new file' do + context 'with committing a new file' do context 'when an user has write access' do before do visit(project_tree_path_root_ref) diff --git a/spec/features/projects/jobs_spec.rb b/spec/features/projects/jobs_spec.rb index b3bea92e635..cbb935abd53 100644 --- a/spec/features/projects/jobs_spec.rb +++ b/spec/features/projects/jobs_spec.rb @@ -198,6 +198,24 @@ describe 'Jobs', :clean_gitlab_redis_shared_state do end end + context 'when job is running', :js do + let(:job) { create(:ci_build, :running, pipeline: pipeline) } + let(:job_url) { project_job_path(project, job) } + + before do + visit job_url + wait_for_requests + end + + context 'job is cancelable' do + it 'shows cancel button' do + click_link 'Cancel' + + expect(page.current_path).to eq(job_url) + end + end + end + context "Job from other project" do before do visit project_job_path(project, job2) @@ -378,8 +396,8 @@ describe 'Jobs', :clean_gitlab_redis_shared_state do end context 'job is successful and has deployment' do - let(:build) { create(:ci_build, :success, :trace_live, environment: environment.name, pipeline: pipeline) } - let!(:deployment) { create(:deployment, environment: environment, project: environment.project, deployable: build) } + let(:build) { create(:ci_build, :success, :trace_live, environment: environment.name, pipeline: pipeline, deployment: deployment) } + let(:deployment) { create(:deployment, :success, environment: environment, project: environment.project) } it 'shows a link for the job' do expect(page).to have_link environment.name @@ -401,7 +419,7 @@ describe 'Jobs', :clean_gitlab_redis_shared_state do end context 'deployment still not finished' do - let(:build) { create(:ci_build, :success, environment: environment.name, pipeline: pipeline) } + let(:build) { create(:ci_build, :running, environment: environment.name, pipeline: pipeline) } it 'shows a link to latest deployment' do expect(page).to have_link environment.name @@ -438,6 +456,8 @@ describe 'Jobs', :clean_gitlab_redis_shared_state do describe 'environment info in job view', :js do before do + allow_any_instance_of(Ci::Build).to receive(:create_deployment) + visit project_job_path(project, job) wait_for_requests end @@ -446,8 +466,8 @@ describe 'Jobs', :clean_gitlab_redis_shared_state do let(:job) { create(:ci_build, :success, :trace_artifact, environment: 'staging', pipeline: pipeline) } let(:second_build) { create(:ci_build, :success, :trace_artifact, environment: 'staging', pipeline: pipeline) } let(:environment) { create(:environment, name: 'staging', project: project) } - let!(:first_deployment) { create(:deployment, environment: environment, deployable: job) } - let!(:second_deployment) { create(:deployment, environment: environment, deployable: second_build) } + let!(:first_deployment) { create(:deployment, :success, environment: environment, deployable: job) } + let!(:second_deployment) { create(:deployment, :success, environment: environment, deployable: second_build) } it 'shows deployment message' do expected_text = 'This job is an out-of-date deployment ' \ @@ -487,7 +507,7 @@ describe 'Jobs', :clean_gitlab_redis_shared_state do end context 'when it has deployment' do - let!(:deployment) { create(:deployment, environment: environment) } + let!(:deployment) { create(:deployment, :success, environment: environment) } it 'shows that deployment will be overwritten' do expected_text = 'This job is creating a deployment to staging' diff --git a/spec/features/projects/pipelines/pipeline_spec.rb b/spec/features/projects/pipelines/pipeline_spec.rb index cd6c37bf54d..049bbca958f 100644 --- a/spec/features/projects/pipelines/pipeline_spec.rb +++ b/spec/features/projects/pipelines/pipeline_spec.rb @@ -388,54 +388,83 @@ describe 'Pipeline', :js do let(:pipeline_failures_page) { failures_project_pipeline_path(project, pipeline) } let!(:failed_build) { create(:ci_build, :failed, pipeline: pipeline) } + subject { visit pipeline_failures_page } + context 'with failed build' do before do failed_build.trace.set('4 examples, 1 failure') - - visit pipeline_failures_page end it 'shows jobs tab pane as active' do + subject + expect(page).to have_content('Failed Jobs') expect(page).to have_css('#js-tab-failures.active') end it 'lists failed builds' do + subject + expect(page).to have_content(failed_build.name) expect(page).to have_content(failed_build.stage) end it 'shows build failure logs' do + subject + expect(page).to have_content('4 examples, 1 failure') end it 'shows the failure reason' do + subject + expect(page).to have_content('There is an unknown failure, please try again') end - it 'shows retry button for failed build' do - page.within(find('.build-failures', match: :first)) do - expect(page).to have_link('Retry') + context 'when user does not have permission to retry build' do + it 'shows retry button for failed build' do + subject + + page.within(find('.build-failures', match: :first)) do + expect(page).not_to have_link('Retry') + end end end - end - context 'when missing build logs' do - before do - visit pipeline_failures_page + context 'when user does have permission to retry build' do + before do + create(:protected_branch, :developers_can_merge, + name: pipeline.ref, project: project) + end + + it 'shows retry button for failed build' do + subject + + page.within(find('.build-failures', match: :first)) do + expect(page).to have_link('Retry') + end + end end + end + context 'when missing build logs' do it 'shows jobs tab pane as active' do + subject + expect(page).to have_content('Failed Jobs') expect(page).to have_css('#js-tab-failures.active') end it 'lists failed builds' do + subject + expect(page).to have_content(failed_build.name) expect(page).to have_content(failed_build.stage) end it 'does not show trace' do + subject + expect(page).to have_content('No job trace') end end @@ -448,11 +477,9 @@ describe 'Pipeline', :js do end context 'when accessing failed jobs page' do - before do - visit pipeline_failures_page - end - it 'fails to access the page' do + subject + expect(page).to have_title('Access Denied') end end @@ -461,11 +488,11 @@ describe 'Pipeline', :js do context 'without failures' do before do failed_build.update!(status: :success) - - visit pipeline_failures_page end it 'displays the pipeline graph' do + subject + expect(current_path).to eq(pipeline_path(pipeline)) expect(page).not_to have_content('Failed Jobs') expect(page).to have_selector('.pipeline-visualization') diff --git a/spec/features/projects/view_on_env_spec.rb b/spec/features/projects/view_on_env_spec.rb index a48ad94e9fa..7bfcd46713e 100644 --- a/spec/features/projects/view_on_env_spec.rb +++ b/spec/features/projects/view_on_env_spec.rb @@ -44,7 +44,7 @@ describe 'View on environment', :js do context 'and an active deployment' do let(:sha) { project.commit(branch_name).sha } let(:environment) { create(:environment, project: project, name: 'review/feature', external_url: 'http://feature.review.example.com') } - let!(:deployment) { create(:deployment, environment: environment, ref: branch_name, sha: sha) } + let!(:deployment) { create(:deployment, :success, environment: environment, ref: branch_name, sha: sha) } context 'when visiting the diff of a merge request for the branch' do let(:merge_request) { create(:merge_request, :simple, source_project: project, source_branch: branch_name) } diff --git a/spec/finders/environments_finder_spec.rb b/spec/finders/environments_finder_spec.rb index 3cd421f22eb..25835bb4d94 100644 --- a/spec/finders/environments_finder_spec.rb +++ b/spec/finders/environments_finder_spec.rb @@ -12,7 +12,7 @@ describe EnvironmentsFinder do context 'tagged deployment' do before do - create(:deployment, environment: environment, ref: 'v1.1.0', tag: true, sha: project.commit.id) + create(:deployment, :success, environment: environment, ref: 'v1.1.0', tag: true, sha: project.commit.id) end it 'returns environment when with_tags is set' do @@ -33,7 +33,7 @@ describe EnvironmentsFinder do context 'branch deployment' do before do - create(:deployment, environment: environment, ref: 'master', sha: project.commit.id) + create(:deployment, :success, environment: environment, ref: 'master', sha: project.commit.id) end it 'returns environment when ref is set' do @@ -59,7 +59,7 @@ describe EnvironmentsFinder do context 'commit deployment' do before do - create(:deployment, environment: environment, ref: 'master', sha: project.commit.id) + create(:deployment, :success, environment: environment, ref: 'master', sha: project.commit.id) end it 'returns environment' do @@ -71,7 +71,7 @@ describe EnvironmentsFinder do context 'recently updated' do context 'when last deployment to environment is the most recent one' do before do - create(:deployment, environment: environment, ref: 'feature') + create(:deployment, :success, environment: environment, ref: 'feature') end it 'finds recently updated environment' do @@ -82,8 +82,8 @@ describe EnvironmentsFinder do context 'when last deployment to environment is not the most recent' do before do - create(:deployment, environment: environment, ref: 'feature') - create(:deployment, environment: environment, ref: 'master') + create(:deployment, :success, environment: environment, ref: 'feature') + create(:deployment, :success, environment: environment, ref: 'master') end it 'does not find environment' do @@ -96,8 +96,8 @@ describe EnvironmentsFinder do let(:second_environment) { create(:environment, project: project) } before do - create(:deployment, environment: environment, ref: 'feature') - create(:deployment, environment: second_environment, ref: 'feature') + create(:deployment, :success, environment: environment, ref: 'feature') + create(:deployment, :success, environment: second_environment, ref: 'feature') end it 'finds both environments' do diff --git a/spec/finders/issues_finder_spec.rb b/spec/finders/issues_finder_spec.rb index 2f164ffa8b0..c0488c83bd8 100644 --- a/spec/finders/issues_finder_spec.rb +++ b/spec/finders/issues_finder_spec.rb @@ -144,19 +144,31 @@ describe IssuesFinder do end context 'filtering by no milestone' do - let(:params) { { milestone_title: Milestone::None.title } } + let(:params) { { milestone_title: 'None' } } it 'returns issues with no milestone' do expect(issues).to contain_exactly(issue2, issue3, issue4) end + + it 'returns issues with no milestone (deprecated)' do + params[:milestone_title] = Milestone::None.title + + expect(issues).to contain_exactly(issue2, issue3, issue4) + end end context 'filtering by any milestone' do - let(:params) { { milestone_title: Milestone::Any.title } } + let(:params) { { milestone_title: 'Any' } } it 'returns issues with any assigned milestone' do expect(issues).to contain_exactly(issue1) end + + it 'returns issues with any assigned milestone (deprecated)' do + params[:milestone_title] = Milestone::Any.title + + expect(issues).to contain_exactly(issue1) + end end context 'filtering by upcoming milestone' do @@ -360,6 +372,22 @@ describe IssuesFinder do end context 'filtering by reaction name' do + context 'user searches by no reaction' do + let(:params) { { my_reaction_emoji: 'None' } } + + it 'returns issues that the user did not react to' do + expect(issues).to contain_exactly(issue2, issue4) + end + end + + context 'user searches by any reaction' do + let(:params) { { my_reaction_emoji: 'Any' } } + + it 'returns issues that the user reacted to' do + expect(issues).to contain_exactly(issue1, issue3) + end + end + context 'user searches by "thumbsup" reaction' do let(:params) { { my_reaction_emoji: 'thumbsup' } } diff --git a/spec/finders/notes_finder_spec.rb b/spec/finders/notes_finder_spec.rb index de9974c45e1..b51f1955ac4 100644 --- a/spec/finders/notes_finder_spec.rb +++ b/spec/finders/notes_finder_spec.rb @@ -13,7 +13,7 @@ describe NotesFinder do let!(:comment) { create(:note_on_issue, project: project) } let!(:system_note) { create(:note_on_issue, project: project, system: true) } - it 'filters system notes' do + it 'returns only user notes when using only_comments filter' do finder = described_class.new(project, user, notes_filter: UserPreference::NOTES_FILTERS[:only_comments]) notes = finder.execute @@ -21,6 +21,14 @@ describe NotesFinder do expect(notes).to match_array(comment) end + it 'returns only system notes when using only_activity filters' do + finder = described_class.new(project, user, notes_filter: UserPreference::NOTES_FILTERS[:only_activity]) + + notes = finder.execute + + expect(notes).to match_array(system_note) + end + it 'gets all notes' do finder = described_class.new(project, user, notes_filter: UserPreference::NOTES_FILTERS[:all_activity]) diff --git a/spec/finders/personal_access_tokens_finder_spec.rb b/spec/finders/personal_access_tokens_finder_spec.rb index 3f22b3a253d..3e849c9a644 100644 --- a/spec/finders/personal_access_tokens_finder_spec.rb +++ b/spec/finders/personal_access_tokens_finder_spec.rb @@ -92,7 +92,7 @@ describe PersonalAccessTokensFinder do end describe 'with id' do - subject { finder(params).find_by(id: active_personal_access_token.id) } + subject { finder(params).find_by_id(active_personal_access_token.id) } it { is_expected.to eq(active_personal_access_token) } @@ -106,7 +106,7 @@ describe PersonalAccessTokensFinder do end describe 'with token' do - subject { finder(params).find_by(token: active_personal_access_token.token) } + subject { finder(params).find_by_token(active_personal_access_token.token) } it { is_expected.to eq(active_personal_access_token) } @@ -207,7 +207,7 @@ describe PersonalAccessTokensFinder do end describe 'with id' do - subject { finder(params).find_by(id: active_personal_access_token.id) } + subject { finder(params).find_by_id(active_personal_access_token.id) } it { is_expected.to eq(active_personal_access_token) } @@ -221,7 +221,7 @@ describe PersonalAccessTokensFinder do end describe 'with token' do - subject { finder(params).find_by(token: active_personal_access_token.token) } + subject { finder(params).find_by_token(active_personal_access_token.token) } it { is_expected.to eq(active_personal_access_token) } diff --git a/spec/fixtures/api/schemas/deployment.json b/spec/fixtures/api/schemas/deployment.json index 44835386cfc..0828f113495 100644 --- a/spec/fixtures/api/schemas/deployment.json +++ b/spec/fixtures/api/schemas/deployment.json @@ -48,6 +48,10 @@ "manual_actions": { "type": "array", "items": { "$ref": "job/job.json" } + }, + "scheduled_actions": { + "type": "array", + "items": { "$ref": "job/job.json" } } }, "additionalProperties": false diff --git a/spec/fixtures/api/schemas/issue.json b/spec/fixtures/api/schemas/issue.json index 8833825e3fb..4878df43d28 100644 --- a/spec/fixtures/api/schemas/issue.json +++ b/spec/fixtures/api/schemas/issue.json @@ -15,6 +15,7 @@ "relative_position": { "type": "integer" }, "issue_sidebar_endpoint": { "type": "string" }, "toggle_subscription_endpoint": { "type": "string" }, + "assignable_labels_endpoint": { "type": "string" }, "reference_path": { "type": "string" }, "real_path": { "type": "string" }, "project": { diff --git a/spec/fixtures/api/schemas/job/job.json b/spec/fixtures/api/schemas/job/job.json index 734c535ef70..f3d5e9b038a 100644 --- a/spec/fixtures/api/schemas/job/job.json +++ b/spec/fixtures/api/schemas/job/job.json @@ -9,7 +9,8 @@ "playable", "created_at", "updated_at", - "status" + "status", + "archived" ], "properties": { "id": { "type": "integer" }, @@ -27,7 +28,8 @@ "updated_at": { "type": "string" }, "status": { "$ref": "../status/ci_detailed_status.json" }, "callout_message": { "type": "string" }, - "recoverable": { "type": "boolean" } + "recoverable": { "type": "boolean" }, + "archived": { "type": "boolean" } }, "additionalProperties": true } diff --git a/spec/helpers/labels_helper_spec.rb b/spec/helpers/labels_helper_spec.rb index a2cda58e5d2..c04f679bcf0 100644 --- a/spec/helpers/labels_helper_spec.rb +++ b/spec/helpers/labels_helper_spec.rb @@ -211,4 +211,29 @@ describe LabelsHelper do end end end + + describe 'labels_filter_path' do + let(:group) { create(:group) } + let(:project) { create(:project) } + + it 'links to the dashboard labels page' do + expect(labels_filter_path).to eq(dashboard_labels_path) + end + + it 'links to the group labels page' do + assign(:group, group) + + expect(helper.labels_filter_path).to eq(group_labels_path(group)) + end + + it 'links to the project labels page' do + assign(:project, project) + + expect(helper.labels_filter_path).to eq(project_labels_path(project)) + end + + it 'supports json format' do + expect(labels_filter_path(format: :json)).to eq(dashboard_labels_path(format: :json)) + end + end end diff --git a/spec/helpers/tree_helper_spec.rb b/spec/helpers/tree_helper_spec.rb index ffdf6561a53..ab4566e261b 100644 --- a/spec/helpers/tree_helper_spec.rb +++ b/spec/helpers/tree_helper_spec.rb @@ -3,7 +3,7 @@ require 'spec_helper' describe TreeHelper do let(:project) { create(:project, :repository) } let(:repository) { project.repository } - let(:sha) { 'ce369011c189f62c815f5971d096b26759bab0d1' } + let(:sha) { 'c1c67abbaf91f624347bb3ae96eabe3a1b742478' } describe '.render_tree' do before do @@ -32,6 +32,49 @@ describe TreeHelper do end end + describe '.fast_project_blob_path' do + it 'generates the same path as project_blob_path' do + blob_path = repository.tree(sha, 'with space').entries.first.path + fast_path = fast_project_blob_path(project, blob_path) + std_path = project_blob_path(project, blob_path) + + expect(fast_path).to eq(std_path) + end + + it 'generates the same path with encoded file names' do + tree = repository.tree(sha, 'encoding') + blob_path = tree.entries.find { |entry| entry.path == 'encoding/テスト.txt' }.path + fast_path = fast_project_blob_path(project, blob_path) + std_path = project_blob_path(project, blob_path) + + expect(fast_path).to eq(std_path) + end + + it 'respects a configured relative URL' do + allow(Gitlab.config.gitlab).to receive(:relative_url_root).and_return('/gitlab/root') + blob_path = repository.tree(sha, '').entries.first.path + fast_path = fast_project_blob_path(project, blob_path) + + expect(fast_path).to start_with('/gitlab/root') + end + end + + describe '.fast_project_tree_path' do + let(:tree_path) { repository.tree(sha, 'with space').path } + let(:fast_path) { fast_project_tree_path(project, tree_path) } + let(:std_path) { project_tree_path(project, tree_path) } + + it 'generates the same path as project_tree_path' do + expect(fast_path).to eq(std_path) + end + + it 'respects a configured relative URL' do + allow(Gitlab.config.gitlab).to receive(:relative_url_root).and_return('/gitlab/root') + + expect(fast_path).to start_with('/gitlab/root') + end + end + describe 'flatten_tree' do let(:tree) { repository.tree(sha, 'files') } let(:root_path) { 'files' } diff --git a/spec/javascripts/awards_handler_spec.js b/spec/javascripts/awards_handler_spec.js index b0689fc7cfe..ce5d2022441 100644 --- a/spec/javascripts/awards_handler_spec.js +++ b/spec/javascripts/awards_handler_spec.js @@ -219,7 +219,7 @@ describe('AwardsHandler', function() { expect($thumbsUpEmoji.data('originalTitle')).toBe('You, sam, jerry, max, and andy'); }); - it('handles the special case where "You" is not cleanly comma seperated', function() { + it('handles the special case where "You" is not cleanly comma separated', function() { const awardUrl = awardsHandler.getAwardUrl(); const $votesBlock = $('.js-awards-block').eq(0); const $thumbsUpEmoji = $votesBlock.find('[data-name=thumbsup]').parent(); @@ -244,7 +244,7 @@ describe('AwardsHandler', function() { expect($thumbsUpEmoji.data('originalTitle')).toBe('sam, jerry, max, and andy'); }); - it('handles the special case where "You" is not cleanly comma seperated', function() { + it('handles the special case where "You" is not cleanly comma separated', function() { const awardUrl = awardsHandler.getAwardUrl(); const $votesBlock = $('.js-awards-block').eq(0); const $thumbsUpEmoji = $votesBlock.find('[data-name=thumbsup]').parent(); diff --git a/spec/javascripts/boards/board_list_spec.js b/spec/javascripts/boards/board_list_spec.js index 037e06cf3b2..2642c8b1bdb 100644 --- a/spec/javascripts/boards/board_list_spec.js +++ b/spec/javascripts/boards/board_list_spec.js @@ -18,7 +18,7 @@ describe('Board list component', () => { let mock; let component; - beforeEach((done) => { + beforeEach(done => { const el = document.createElement('div'); document.body.appendChild(el); @@ -62,122 +62,102 @@ describe('Board list component', () => { }); it('renders component', () => { - expect( - component.$el.classList.contains('board-list-component'), - ).toBe(true); + expect(component.$el.classList.contains('board-list-component')).toBe(true); }); - it('renders loading icon', (done) => { + it('renders loading icon', done => { component.loading = true; Vue.nextTick(() => { - expect( - component.$el.querySelector('.board-list-loading'), - ).not.toBeNull(); + expect(component.$el.querySelector('.board-list-loading')).not.toBeNull(); done(); }); }); it('renders issues', () => { - expect( - component.$el.querySelectorAll('.board-card').length, - ).toBe(1); + expect(component.$el.querySelectorAll('.board-card').length).toBe(1); }); it('sets data attribute with issue id', () => { - expect( - component.$el.querySelector('.board-card').getAttribute('data-issue-id'), - ).toBe('1'); + expect(component.$el.querySelector('.board-card').getAttribute('data-issue-id')).toBe('1'); }); - it('shows new issue form', (done) => { + it('shows new issue form', done => { component.toggleForm(); Vue.nextTick(() => { - expect( - component.$el.querySelector('.board-new-issue-form'), - ).not.toBeNull(); + expect(component.$el.querySelector('.board-new-issue-form')).not.toBeNull(); - expect( - component.$el.querySelector('.is-smaller'), - ).not.toBeNull(); + expect(component.$el.querySelector('.is-smaller')).not.toBeNull(); done(); }); }); - it('shows new issue form after eventhub event', (done) => { + it('shows new issue form after eventhub event', done => { eventHub.$emit(`hide-issue-form-${component.list.id}`); Vue.nextTick(() => { - expect( - component.$el.querySelector('.board-new-issue-form'), - ).not.toBeNull(); + expect(component.$el.querySelector('.board-new-issue-form')).not.toBeNull(); - expect( - component.$el.querySelector('.is-smaller'), - ).not.toBeNull(); + expect(component.$el.querySelector('.is-smaller')).not.toBeNull(); done(); }); }); - it('does not show new issue form for closed list', (done) => { + it('does not show new issue form for closed list', done => { component.list.type = 'closed'; component.toggleForm(); Vue.nextTick(() => { - expect( - component.$el.querySelector('.board-new-issue-form'), - ).toBeNull(); + expect(component.$el.querySelector('.board-new-issue-form')).toBeNull(); done(); }); }); - it('shows count list item', (done) => { + it('shows count list item', done => { component.showCount = true; Vue.nextTick(() => { - expect( - component.$el.querySelector('.board-list-count'), - ).not.toBeNull(); + expect(component.$el.querySelector('.board-list-count')).not.toBeNull(); - expect( - component.$el.querySelector('.board-list-count').textContent.trim(), - ).toBe('Showing all issues'); + expect(component.$el.querySelector('.board-list-count').textContent.trim()).toBe( + 'Showing all issues', + ); done(); }); }); - it('sets data attribute with invalid id', (done) => { + it('sets data attribute with invalid id', done => { component.showCount = true; Vue.nextTick(() => { - expect( - component.$el.querySelector('.board-list-count').getAttribute('data-issue-id'), - ).toBe('-1'); + expect(component.$el.querySelector('.board-list-count').getAttribute('data-issue-id')).toBe( + '-1', + ); done(); }); }); - it('shows how many more issues to load', (done) => { + it('shows how many more issues to load', done => { component.showCount = true; component.list.issuesSize = 20; Vue.nextTick(() => { - expect( - component.$el.querySelector('.board-list-count').textContent.trim(), - ).toBe('Showing 1 of 20 issues'); + expect(component.$el.querySelector('.board-list-count').textContent.trim()).toBe( + 'Showing 1 of 20 issues', + ); done(); }); }); - it('loads more issues after scrolling', (done) => { + it('loads more issues after scrolling', done => { spyOn(component.list, 'nextPage'); component.$refs.list.style.height = '100px'; component.$refs.list.style.overflow = 'scroll'; @@ -200,7 +180,9 @@ describe('Board list component', () => { }); it('does not load issues if already loading', () => { - component.list.nextPage = spyOn(component.list, 'nextPage').and.returnValue(new Promise(() => {})); + component.list.nextPage = spyOn(component.list, 'nextPage').and.returnValue( + new Promise(() => {}), + ); component.onScroll(); component.onScroll(); @@ -208,14 +190,12 @@ describe('Board list component', () => { expect(component.list.nextPage).toHaveBeenCalledTimes(1); }); - it('shows loading more spinner', (done) => { + it('shows loading more spinner', done => { component.showCount = true; component.list.loadingMore = true; Vue.nextTick(() => { - expect( - component.$el.querySelector('.board-list-count .fa-spinner'), - ).not.toBeNull(); + expect(component.$el.querySelector('.board-list-count .fa-spinner')).not.toBeNull(); done(); }); diff --git a/spec/javascripts/boards/components/board_spec.js b/spec/javascripts/boards/components/board_spec.js index d4c53bd5a7d..dee7841c088 100644 --- a/spec/javascripts/boards/components/board_spec.js +++ b/spec/javascripts/boards/components/board_spec.js @@ -8,7 +8,7 @@ describe('Board component', () => { let vm; let el; - beforeEach((done) => { + beforeEach(done => { loadFixtures('boards/show.html.raw'); el = document.createElement('div'); @@ -50,56 +50,46 @@ describe('Board component', () => { }); it('board is expandable when list type is backlog', () => { - expect( - vm.$el.classList.contains('is-expandable'), - ).toBe(true); + expect(vm.$el.classList.contains('is-expandable')).toBe(true); }); - it('board is expandable when list type is closed', (done) => { + it('board is expandable when list type is closed', done => { vm.list.type = 'closed'; Vue.nextTick(() => { - expect( - vm.$el.classList.contains('is-expandable'), - ).toBe(true); + expect(vm.$el.classList.contains('is-expandable')).toBe(true); done(); }); }); - it('board is not expandable when list type is label', (done) => { + it('board is not expandable when list type is label', done => { vm.list.type = 'label'; vm.list.isExpandable = false; Vue.nextTick(() => { - expect( - vm.$el.classList.contains('is-expandable'), - ).toBe(false); + expect(vm.$el.classList.contains('is-expandable')).toBe(false); done(); }); }); - it('collapses when clicking header', (done) => { + it('collapses when clicking header', done => { vm.$el.querySelector('.board-header').click(); Vue.nextTick(() => { - expect( - vm.$el.classList.contains('is-collapsed'), - ).toBe(true); + expect(vm.$el.classList.contains('is-collapsed')).toBe(true); done(); }); }); - it('created sets isExpanded to true from localStorage', (done) => { + it('created sets isExpanded to true from localStorage', done => { vm.$el.querySelector('.board-header').click(); return Vue.nextTick() .then(() => { - expect( - vm.$el.classList.contains('is-collapsed'), - ).toBe(true); + expect(vm.$el.classList.contains('is-collapsed')).toBe(true); // call created manually vm.$options.created[0].call(vm); @@ -107,11 +97,10 @@ describe('Board component', () => { return Vue.nextTick(); }) .then(() => { - expect( - vm.$el.classList.contains('is-collapsed'), - ).toBe(true); + expect(vm.$el.classList.contains('is-collapsed')).toBe(true); done(); - }).catch(done.fail); + }) + .catch(done.fail); }); }); diff --git a/spec/javascripts/commit/commit_pipeline_status_component_spec.js b/spec/javascripts/commit/commit_pipeline_status_component_spec.js index 4fc56fd9a27..f6b36e88a5f 100644 --- a/spec/javascripts/commit/commit_pipeline_status_component_spec.js +++ b/spec/javascripts/commit/commit_pipeline_status_component_spec.js @@ -22,7 +22,7 @@ describe('Commit pipeline status component', () => { Component = Vue.extend(commitPipelineStatus); }); - describe('While polling pipeline data succesfully', () => { + describe('While polling pipeline data successfully', () => { beforeEach(() => { mock = new MockAdapter(axios); mock.onGet('/dummy/endpoint').reply(() => { @@ -59,14 +59,14 @@ describe('Commit pipeline status component', () => { }); }); - it('contains a ciStatus when the polling is succesful ', done => { + it('contains a ciStatus when the polling is successful ', done => { setTimeout(() => { expect(vm.ciStatus).toEqual(mockCiStatus); done(); }); }); - it('contains a ci-status icon when polling is succesful', done => { + it('contains a ci-status icon when polling is successful', done => { setTimeout(() => { expect(vm.$el.querySelector('.ci-status-icon')).not.toBe(null); expect(vm.$el.querySelector('.ci-status-icon').classList).toContain( @@ -77,7 +77,7 @@ describe('Commit pipeline status component', () => { }); }); - describe('When polling data was not succesful', () => { + describe('When polling data was not successful', () => { beforeEach(() => { mock = new MockAdapter(axios); mock.onGet('/dummy/endpoint').reply(502, {}); diff --git a/spec/javascripts/commit/pipelines/pipelines_spec.js b/spec/javascripts/commit/pipelines/pipelines_spec.js index b797cc44ae7..04c8ab44405 100644 --- a/spec/javascripts/commit/pipelines/pipelines_spec.js +++ b/spec/javascripts/commit/pipelines/pipelines_spec.js @@ -72,6 +72,29 @@ describe('Pipelines table in Commits and Merge requests', function() { done(); }, 0); }); + + describe('with pagination', () => { + it('should make an API request when using pagination', done => { + setTimeout(() => { + spyOn(vm, 'updateContent'); + + vm.store.state.pageInfo = { + page: 1, + total: 10, + perPage: 2, + nextPage: 2, + totalPages: 5, + }; + + vm.$nextTick(() => { + vm.$el.querySelector('.js-next-button a').click(); + + expect(vm.updateContent).toHaveBeenCalledWith({ page: '2' }); + done(); + }); + }); + }); + }); }); describe('pipeline badge counts', () => { diff --git a/spec/javascripts/dirty_submit/dirty_submit_form_spec.js b/spec/javascripts/dirty_submit/dirty_submit_form_spec.js index b7b29190c31..093fec97951 100644 --- a/spec/javascripts/dirty_submit/dirty_submit_form_spec.js +++ b/spec/javascripts/dirty_submit/dirty_submit_form_spec.js @@ -1,23 +1,35 @@ import DirtySubmitForm from '~/dirty_submit/dirty_submit_form'; import { setInput, createForm } from './helper'; +function expectToToggleDisableOnDirtyUpdate(submit, input) { + const originalValue = input.value; + + expect(submit.disabled).toBe(true); + + return setInput(input, `${originalValue} changes`) + .then(() => expect(submit.disabled).toBe(false)) + .then(() => setInput(input, originalValue)) + .then(() => expect(submit.disabled).toBe(true)); +} + describe('DirtySubmitForm', () => { it('disables submit until there are changes', done => { const { form, input, submit } = createForm(); - const originalValue = input.value; new DirtySubmitForm(form); // eslint-disable-line no-new - expect(submit.disabled).toBe(true); + return expectToToggleDisableOnDirtyUpdate(submit, input) + .then(done) + .catch(done.fail); + }); + + it('disables submit until there are changes when initializing with a falsy value', done => { + const { form, input, submit } = createForm(); + input.value = ''; + + new DirtySubmitForm(form); // eslint-disable-line no-new - return setInput(input, `${originalValue} changes`) - .then(() => { - expect(submit.disabled).toBe(false); - }) - .then(() => setInput(input, originalValue)) - .then(() => { - expect(submit.disabled).toBe(true); - }) + return expectToToggleDisableOnDirtyUpdate(submit, input) .then(done) .catch(done.fail); }); diff --git a/spec/javascripts/environments/emtpy_state_spec.js b/spec/javascripts/environments/emtpy_state_spec.js index d71dfe8197e..1f986d49bc7 100644 --- a/spec/javascripts/environments/emtpy_state_spec.js +++ b/spec/javascripts/environments/emtpy_state_spec.js @@ -1,4 +1,3 @@ - import Vue from 'vue'; import emptyState from '~/environments/components/empty_state.vue'; import mountComponent from 'spec/helpers/vue_mount_component_helper'; @@ -25,13 +24,13 @@ describe('environments empty state', () => { }); it('renders empty state and new environment button', () => { - expect( - vm.$el.querySelector('.js-blank-state-title').textContent.trim(), - ).toEqual('You don\'t have any environments right now'); + expect(vm.$el.querySelector('.js-blank-state-title').textContent.trim()).toEqual( + "You don't have any environments right now", + ); - expect( - vm.$el.querySelector('.js-new-environment-button').getAttribute('href'), - ).toEqual('foo'); + expect(vm.$el.querySelector('.js-new-environment-button').getAttribute('href')).toEqual( + 'foo', + ); }); }); @@ -45,13 +44,11 @@ describe('environments empty state', () => { }); it('renders empty state without new button', () => { - expect( - vm.$el.querySelector('.js-blank-state-title').textContent.trim(), - ).toEqual('You don\'t have any environments right now'); + expect(vm.$el.querySelector('.js-blank-state-title').textContent.trim()).toEqual( + "You don't have any environments right now", + ); - expect( - vm.$el.querySelector('.js-new-environment-button'), - ).toBeNull(); + expect(vm.$el.querySelector('.js-new-environment-button')).toBeNull(); }); }); }); diff --git a/spec/javascripts/environments/environment_actions_spec.js b/spec/javascripts/environments/environment_actions_spec.js index 223153d4e31..787df757d32 100644 --- a/spec/javascripts/environments/environment_actions_spec.js +++ b/spec/javascripts/environments/environment_actions_spec.js @@ -1,15 +1,19 @@ import Vue from 'vue'; -import actionsComp from '~/environments/components/environment_actions.vue'; +import eventHub from '~/environments/event_hub'; +import EnvironmentActions from '~/environments/components/environment_actions.vue'; +import mountComponent from 'spec/helpers/vue_mount_component_helper'; +import { TEST_HOST } from 'spec/test_constants'; -describe('Actions Component', () => { - let ActionsComponent; - let actionsMock; - let component; +describe('EnvironmentActions Component', () => { + const Component = Vue.extend(EnvironmentActions); + let vm; - beforeEach(() => { - ActionsComponent = Vue.extend(actionsComp); + afterEach(() => { + vm.$destroy(); + }); - actionsMock = [ + describe('manual actions', () => { + const actions = [ { name: 'bar', play_path: 'https://gitlab.com/play', @@ -25,43 +29,89 @@ describe('Actions Component', () => { }, ]; - component = new ActionsComponent({ - propsData: { - actions: actionsMock, - }, - }).$mount(); - }); + beforeEach(() => { + vm = mountComponent(Component, { actions }); + }); + + it('should render a dropdown button with icon and title attribute', () => { + expect(vm.$el.querySelector('.fa-caret-down')).toBeDefined(); + expect(vm.$el.querySelector('.dropdown-new').getAttribute('data-original-title')).toEqual( + 'Deploy to...', + ); - describe('computed', () => { - it('title', () => { - expect(component.title).toEqual('Deploy to...'); + expect(vm.$el.querySelector('.dropdown-new').getAttribute('aria-label')).toEqual( + 'Deploy to...', + ); }); - }); - it('should render a dropdown button with icon and title attribute', () => { - expect(component.$el.querySelector('.fa-caret-down')).toBeDefined(); - expect( - component.$el.querySelector('.dropdown-new').getAttribute('data-original-title'), - ).toEqual('Deploy to...'); + it('should render a dropdown with the provided list of actions', () => { + expect(vm.$el.querySelectorAll('.dropdown-menu li').length).toEqual(actions.length); + }); - expect(component.$el.querySelector('.dropdown-new').getAttribute('aria-label')).toEqual( - 'Deploy to...', - ); - }); + it("should render a disabled action when it's not playable", () => { + expect( + vm.$el.querySelector('.dropdown-menu li:last-child button').getAttribute('disabled'), + ).toEqual('disabled'); - it('should render a dropdown with the provided list of actions', () => { - expect(component.$el.querySelectorAll('.dropdown-menu li').length).toEqual(actionsMock.length); + expect( + vm.$el.querySelector('.dropdown-menu li:last-child button').classList.contains('disabled'), + ).toEqual(true); + }); }); - it("should render a disabled action when it's not playable", () => { - expect( - component.$el.querySelector('.dropdown-menu li:last-child button').getAttribute('disabled'), - ).toEqual('disabled'); + describe('scheduled jobs', () => { + const scheduledJobAction = { + name: 'scheduled action', + playPath: `${TEST_HOST}/scheduled/job/action`, + playable: true, + scheduledAt: '2063-04-05T00:42:00Z', + }; + const expiredJobAction = { + name: 'expired action', + playPath: `${TEST_HOST}/expired/job/action`, + playable: true, + scheduledAt: '2018-10-05T08:23:00Z', + }; + const findDropdownItem = action => { + const buttons = vm.$el.querySelectorAll('.dropdown-menu li button'); + return Array.prototype.find.call(buttons, element => + element.innerText.trim().startsWith(action.name), + ); + }; + + beforeEach(() => { + spyOn(Date, 'now').and.callFake(() => new Date('2063-04-04T00:42:00Z').getTime()); + vm = mountComponent(Component, { actions: [scheduledJobAction, expiredJobAction] }); + }); + + it('emits postAction event after confirming', () => { + const emitSpy = jasmine.createSpy('emit'); + eventHub.$on('postAction', emitSpy); + spyOn(window, 'confirm').and.callFake(() => true); + + findDropdownItem(scheduledJobAction).click(); + + expect(window.confirm).toHaveBeenCalled(); + expect(emitSpy).toHaveBeenCalledWith({ endpoint: scheduledJobAction.playPath }); + }); + + it('does not emit postAction event if confirmation is cancelled', () => { + const emitSpy = jasmine.createSpy('emit'); + eventHub.$on('postAction', emitSpy); + spyOn(window, 'confirm').and.callFake(() => false); + + findDropdownItem(scheduledJobAction).click(); - expect( - component.$el - .querySelector('.dropdown-menu li:last-child button') - .classList.contains('disabled'), - ).toEqual(true); + expect(window.confirm).toHaveBeenCalled(); + expect(emitSpy).not.toHaveBeenCalled(); + }); + + it('displays the remaining time in the dropdown', () => { + expect(findDropdownItem(scheduledJobAction)).toContainText('24:00:00'); + }); + + it('displays 00:00:00 for expired jobs in the dropdown', () => { + expect(findDropdownItem(expiredJobAction)).toContainText('00:00:00'); + }); }); }); diff --git a/spec/javascripts/environments/environment_item_spec.js b/spec/javascripts/environments/environment_item_spec.js index 0b933dda431..7618c2f50ce 100644 --- a/spec/javascripts/environments/environment_item_spec.js +++ b/spec/javascripts/environments/environment_item_spec.js @@ -38,7 +38,9 @@ describe('Environment item', () => { }); it('Should render the number of children in a badge', () => { - expect(component.$el.querySelector('.folder-name .badge').textContent).toContain(mockItem.size); + expect(component.$el.querySelector('.folder-name .badge').textContent).toContain( + mockItem.size, + ); }); }); @@ -68,7 +70,8 @@ describe('Environment item', () => { username: 'root', id: 1, state: 'active', - avatar_url: 'https://www.gravatar.com/avatar/e64c7d89f26bd1972efa854d13d7dd61?s=80\u0026d=identicon', + avatar_url: + 'https://www.gravatar.com/avatar/e64c7d89f26bd1972efa854d13d7dd61?s=80\u0026d=identicon', web_url: 'http://localhost:3000/root', }, commit: { @@ -84,7 +87,8 @@ describe('Environment item', () => { username: 'root', id: 1, state: 'active', - avatar_url: 'https://www.gravatar.com/avatar/e64c7d89f26bd1972efa854d13d7dd61?s=80\u0026d=identicon', + avatar_url: + 'https://www.gravatar.com/avatar/e64c7d89f26bd1972efa854d13d7dd61?s=80\u0026d=identicon', web_url: 'http://localhost:3000/root', }, commit_path: '/root/ci-folders/tree/500aabcb17c97bdcf2d0c410b70cb8556f0362dd', @@ -121,18 +125,18 @@ describe('Environment item', () => { }); it('should render environment name', () => { - expect(component.$el.querySelector('.environment-name').textContent).toContain(environment.name); + expect(component.$el.querySelector('.environment-name').textContent).toContain( + environment.name, + ); }); describe('With deployment', () => { it('should render deployment internal id', () => { - expect( - component.$el.querySelector('.deployment-column span').textContent, - ).toContain(environment.last_deployment.iid); + expect(component.$el.querySelector('.deployment-column span').textContent).toContain( + environment.last_deployment.iid, + ); - expect( - component.$el.querySelector('.deployment-column span').textContent, - ).toContain('#'); + expect(component.$el.querySelector('.deployment-column span').textContent).toContain('#'); }); it('should render last deployment date', () => { @@ -156,56 +160,46 @@ describe('Environment item', () => { describe('With build url', () => { it('Should link to build url provided', () => { - expect( - component.$el.querySelector('.build-link').getAttribute('href'), - ).toEqual(environment.last_deployment.deployable.build_path); + expect(component.$el.querySelector('.build-link').getAttribute('href')).toEqual( + environment.last_deployment.deployable.build_path, + ); }); it('Should render deployable name and id', () => { - expect( - component.$el.querySelector('.build-link').getAttribute('href'), - ).toEqual(environment.last_deployment.deployable.build_path); + expect(component.$el.querySelector('.build-link').getAttribute('href')).toEqual( + environment.last_deployment.deployable.build_path, + ); }); }); describe('With commit information', () => { it('should render commit component', () => { - expect( - component.$el.querySelector('.js-commit-component'), - ).toBeDefined(); + expect(component.$el.querySelector('.js-commit-component')).toBeDefined(); }); }); }); describe('With manual actions', () => { it('Should render actions component', () => { - expect( - component.$el.querySelector('.js-manual-actions-container'), - ).toBeDefined(); + expect(component.$el.querySelector('.js-manual-actions-container')).toBeDefined(); }); }); describe('With external URL', () => { it('should render external url component', () => { - expect( - component.$el.querySelector('.js-external-url-container'), - ).toBeDefined(); + expect(component.$el.querySelector('.js-external-url-container')).toBeDefined(); }); }); describe('With stop action', () => { it('Should render stop action component', () => { - expect( - component.$el.querySelector('.js-stop-component-container'), - ).toBeDefined(); + expect(component.$el.querySelector('.js-stop-component-container')).toBeDefined(); }); }); describe('With retry action', () => { it('Should render rollback component', () => { - expect( - component.$el.querySelector('.js-rollback-component-container'), - ).toBeDefined(); + expect(component.$el.querySelector('.js-rollback-component-container')).toBeDefined(); }); }); }); diff --git a/spec/javascripts/environments/environments_app_spec.js b/spec/javascripts/environments/environments_app_spec.js index 7edc0ccac0b..e2d81eb454a 100644 --- a/spec/javascripts/environments/environments_app_spec.js +++ b/spec/javascripts/environments/environments_app_spec.js @@ -31,9 +31,9 @@ describe('Environment', () => { mock.restore(); }); - describe('successfull request', () => { + describe('successful request', () => { describe('without environments', () => { - beforeEach((done) => { + beforeEach(done => { mock.onGet(mockData.endpoint).reply(200, { environments: [] }); component = mountComponent(EnvironmentsComponent, mockData); @@ -44,30 +44,34 @@ describe('Environment', () => { }); it('should render the empty state', () => { - expect( - component.$el.querySelector('.js-new-environment-button').textContent, - ).toContain('New environment'); + expect(component.$el.querySelector('.js-new-environment-button').textContent).toContain( + 'New environment', + ); - expect( - component.$el.querySelector('.js-blank-state-title').textContent, - ).toContain('You don\'t have any environments right now'); + expect(component.$el.querySelector('.js-blank-state-title').textContent).toContain( + "You don't have any environments right now", + ); }); }); describe('with paginated environments', () => { - beforeEach((done) => { - mock.onGet(mockData.endpoint).reply(200, { - environments: [environment], - stopped_count: 1, - available_count: 0, - }, { - 'X-nExt-pAge': '2', - 'x-page': '1', - 'X-Per-Page': '1', - 'X-Prev-Page': '', - 'X-TOTAL': '37', - 'X-Total-Pages': '2', - }); + beforeEach(done => { + mock.onGet(mockData.endpoint).reply( + 200, + { + environments: [environment], + stopped_count: 1, + available_count: 0, + }, + { + 'X-nExt-pAge': '2', + 'x-page': '1', + 'X-Per-Page': '1', + 'X-Prev-Page': '', + 'X-TOTAL': '37', + 'X-Total-Pages': '2', + }, + ); component = mountComponent(EnvironmentsComponent, mockData); @@ -78,19 +82,17 @@ describe('Environment', () => { it('should render a table with environments', () => { expect(component.$el.querySelectorAll('table')).not.toBeNull(); - expect( - component.$el.querySelector('.environment-name').textContent.trim(), - ).toEqual(environment.name); + expect(component.$el.querySelector('.environment-name').textContent.trim()).toEqual( + environment.name, + ); }); describe('pagination', () => { it('should render pagination', () => { - expect( - component.$el.querySelectorAll('.gl-pagination li').length, - ).toEqual(5); + expect(component.$el.querySelectorAll('.gl-pagination li').length).toEqual(5); }); - it('should make an API request when page is clicked', (done) => { + it('should make an API request when page is clicked', done => { spyOn(component, 'updateContent'); setTimeout(() => { component.$el.querySelector('.gl-pagination li:nth-child(5) a').click(); @@ -100,7 +102,7 @@ describe('Environment', () => { }, 0); }); - it('should make an API request when using tabs', (done) => { + it('should make an API request when using tabs', done => { setTimeout(() => { spyOn(component, 'updateContent'); component.$el.querySelector('.js-environments-tab-stopped').click(); @@ -114,7 +116,7 @@ describe('Environment', () => { }); describe('unsuccessfull request', () => { - beforeEach((done) => { + beforeEach(done => { mock.onGet(mockData.endpoint).reply(500, {}); component = mountComponent(EnvironmentsComponent, mockData); @@ -125,15 +127,16 @@ describe('Environment', () => { }); it('should render empty state', () => { - expect( - component.$el.querySelector('.js-blank-state-title').textContent, - ).toContain('You don\'t have any environments right now'); + expect(component.$el.querySelector('.js-blank-state-title').textContent).toContain( + "You don't have any environments right now", + ); }); }); describe('expandable folders', () => { beforeEach(() => { - mock.onGet(mockData.endpoint).reply(200, + mock.onGet(mockData.endpoint).reply( + 200, { environments: [folder], stopped_count: 0, @@ -154,7 +157,7 @@ describe('Environment', () => { component = mountComponent(EnvironmentsComponent, mockData); }); - it('should open a closed folder', (done) => { + it('should open a closed folder', done => { setTimeout(() => { component.$el.querySelector('.folder-name').click(); @@ -165,7 +168,7 @@ describe('Environment', () => { }, 0); }); - it('should close an opened folder', (done) => { + it('should close an opened folder', done => { setTimeout(() => { // open folder component.$el.querySelector('.folder-name').click(); @@ -182,7 +185,7 @@ describe('Environment', () => { }, 0); }); - it('should show children environments and a button to show all environments', (done) => { + it('should show children environments and a button to show all environments', done => { setTimeout(() => { // open folder component.$el.querySelector('.folder-name').click(); @@ -191,7 +194,9 @@ describe('Environment', () => { // wait for next async request setTimeout(() => { expect(component.$el.querySelectorAll('.js-child-row').length).toEqual(1); - expect(component.$el.querySelector('.text-center > a.btn').textContent).toContain('Show all'); + expect(component.$el.querySelector('.text-center > a.btn').textContent).toContain( + 'Show all', + ); done(); }); }); @@ -201,7 +206,8 @@ describe('Environment', () => { describe('methods', () => { beforeEach(() => { - mock.onGet(mockData.endpoint).reply(200, + mock.onGet(mockData.endpoint).reply( + 200, { environments: [], stopped_count: 0, @@ -215,8 +221,9 @@ describe('Environment', () => { }); describe('updateContent', () => { - it('should set given parameters', (done) => { - component.updateContent({ scope: 'stopped', page: '3' }) + it('should set given parameters', done => { + component + .updateContent({ scope: 'stopped', page: '3' }) .then(() => { expect(component.page).toEqual('3'); expect(component.scope).toEqual('stopped'); diff --git a/spec/javascripts/environments/folder/environments_folder_view_spec.js b/spec/javascripts/environments/folder/environments_folder_view_spec.js index 51d4213c38f..7f0a9475d5f 100644 --- a/spec/javascripts/environments/folder/environments_folder_view_spec.js +++ b/spec/javascripts/environments/folder/environments_folder_view_spec.js @@ -30,39 +30,43 @@ describe('Environments Folder View', () => { component.$destroy(); }); - describe('successfull request', () => { + describe('successful request', () => { beforeEach(() => { - mock.onGet(mockData.endpoint).reply(200, { - environments: environmentsList, - stopped_count: 1, - available_count: 0, - }, { - 'X-nExt-pAge': '2', - 'x-page': '1', - 'X-Per-Page': '2', - 'X-Prev-Page': '', - 'X-TOTAL': '20', - 'X-Total-Pages': '10', - }); + mock.onGet(mockData.endpoint).reply( + 200, + { + environments: environmentsList, + stopped_count: 1, + available_count: 0, + }, + { + 'X-nExt-pAge': '2', + 'x-page': '1', + 'X-Per-Page': '2', + 'X-Prev-Page': '', + 'X-TOTAL': '20', + 'X-Total-Pages': '10', + }, + ); component = mountComponent(Component, mockData); }); - it('should render a table with environments', (done) => { + it('should render a table with environments', done => { setTimeout(() => { expect(component.$el.querySelectorAll('table')).not.toBeNull(); - expect( - component.$el.querySelector('.environment-name').textContent.trim(), - ).toEqual(environmentsList[0].name); + expect(component.$el.querySelector('.environment-name').textContent.trim()).toEqual( + environmentsList[0].name, + ); done(); }, 0); }); - it('should render available tab with count', (done) => { + it('should render available tab with count', done => { setTimeout(() => { - expect( - component.$el.querySelector('.js-environments-tab-available').textContent, - ).toContain('Available'); + expect(component.$el.querySelector('.js-environments-tab-available').textContent).toContain( + 'Available', + ); expect( component.$el.querySelector('.js-environments-tab-available .badge').textContent, @@ -71,11 +75,11 @@ describe('Environments Folder View', () => { }, 0); }); - it('should render stopped tab with count', (done) => { + it('should render stopped tab with count', done => { setTimeout(() => { - expect( - component.$el.querySelector('.js-environments-tab-stopped').textContent, - ).toContain('Stopped'); + expect(component.$el.querySelector('.js-environments-tab-stopped').textContent).toContain( + 'Stopped', + ); expect( component.$el.querySelector('.js-environments-tab-stopped .badge').textContent, @@ -84,36 +88,37 @@ describe('Environments Folder View', () => { }, 0); }); - it('should render parent folder name', (done) => { + it('should render parent folder name', done => { setTimeout(() => { - expect( - component.$el.querySelector('.js-folder-name').textContent.trim(), - ).toContain('Environments / review'); + expect(component.$el.querySelector('.js-folder-name').textContent.trim()).toContain( + 'Environments / review', + ); done(); }, 0); }); describe('pagination', () => { - it('should render pagination', (done) => { + it('should render pagination', done => { setTimeout(() => { - expect( - component.$el.querySelectorAll('.gl-pagination'), - ).not.toBeNull(); + expect(component.$el.querySelectorAll('.gl-pagination')).not.toBeNull(); done(); }, 0); }); - it('should make an API request when changing page', (done) => { + it('should make an API request when changing page', done => { spyOn(component, 'updateContent'); setTimeout(() => { component.$el.querySelector('.gl-pagination .js-last-button a').click(); - expect(component.updateContent).toHaveBeenCalledWith({ scope: component.scope, page: '10' }); + expect(component.updateContent).toHaveBeenCalledWith({ + scope: component.scope, + page: '10', + }); done(); }, 0); }); - it('should make an API request when using tabs', (done) => { + it('should make an API request when using tabs', done => { setTimeout(() => { spyOn(component, 'updateContent'); component.$el.querySelector('.js-environments-tab-stopped').click(); @@ -134,20 +139,18 @@ describe('Environments Folder View', () => { component = mountComponent(Component, mockData); }); - it('should not render a table', (done) => { + it('should not render a table', done => { setTimeout(() => { - expect( - component.$el.querySelector('table'), - ).toBe(null); + expect(component.$el.querySelector('table')).toBe(null); done(); }, 0); }); - it('should render available tab with count 0', (done) => { + it('should render available tab with count 0', done => { setTimeout(() => { - expect( - component.$el.querySelector('.js-environments-tab-available').textContent, - ).toContain('Available'); + expect(component.$el.querySelector('.js-environments-tab-available').textContent).toContain( + 'Available', + ); expect( component.$el.querySelector('.js-environments-tab-available .badge').textContent, @@ -156,11 +159,11 @@ describe('Environments Folder View', () => { }, 0); }); - it('should render stopped tab with count 0', (done) => { + it('should render stopped tab with count 0', done => { setTimeout(() => { - expect( - component.$el.querySelector('.js-environments-tab-stopped').textContent, - ).toContain('Stopped'); + expect(component.$el.querySelector('.js-environments-tab-stopped').textContent).toContain( + 'Stopped', + ); expect( component.$el.querySelector('.js-environments-tab-stopped .badge').textContent, @@ -181,8 +184,9 @@ describe('Environments Folder View', () => { }); describe('updateContent', () => { - it('should set given parameters', (done) => { - component.updateContent({ scope: 'stopped', page: '4' }) + it('should set given parameters', done => { + component + .updateContent({ scope: 'stopped', page: '4' }) .then(() => { expect(component.page).toEqual('4'); expect(component.scope).toEqual('stopped'); diff --git a/spec/javascripts/issue_show/components/title_spec.js b/spec/javascripts/issue_show/components/title_spec.js index 9e6f236b687..9754c8a6755 100644 --- a/spec/javascripts/issue_show/components/title_spec.js +++ b/spec/javascripts/issue_show/components/title_spec.js @@ -25,25 +25,21 @@ describe('Title component', () => { }); it('renders title HTML', () => { - expect( - vm.$el.querySelector('.title').innerHTML.trim(), - ).toBe('Testing <img>'); + expect(vm.$el.querySelector('.title').innerHTML.trim()).toBe('Testing <img>'); }); - it('updates page title when changing titleHtml', (done) => { + it('updates page title when changing titleHtml', done => { spyOn(vm, 'setPageTitle'); vm.titleHtml = 'test'; Vue.nextTick(() => { - expect( - vm.setPageTitle, - ).toHaveBeenCalled(); + expect(vm.setPageTitle).toHaveBeenCalled(); done(); }); }); - it('animates title changes', (done) => { + it('animates title changes', done => { vm.titleHtml = 'test'; Vue.nextTick(() => { @@ -61,14 +57,12 @@ describe('Title component', () => { }); }); - it('updates page title after changing title', (done) => { + it('updates page title after changing title', done => { vm.titleHtml = 'changed'; vm.titleText = 'changed'; Vue.nextTick(() => { - expect( - document.querySelector('title').textContent.trim(), - ).toContain('changed'); + expect(document.querySelector('title').textContent.trim()).toContain('changed'); done(); }); diff --git a/spec/javascripts/jobs/components/job_app_spec.js b/spec/javascripts/jobs/components/job_app_spec.js index 288c06d6615..ba1889c4dcd 100644 --- a/spec/javascripts/jobs/components/job_app_spec.js +++ b/spec/javascripts/jobs/components/job_app_spec.js @@ -52,7 +52,7 @@ describe('Job App ', () => { }); }); - describe('with successfull request', () => { + describe('with successful request', () => { beforeEach(() => { mock.onGet(`${props.pagePath}/trace.json`).replyOnce(200, {}); }); @@ -234,7 +234,7 @@ describe('Job App ', () => { ); done(); }, 0); - }) + }); }); it('does not renders stuck block when there are no runners', done => { diff --git a/spec/javascripts/jobs/store/actions_spec.js b/spec/javascripts/jobs/store/actions_spec.js index 45130b983e7..77b44995b12 100644 --- a/spec/javascripts/jobs/store/actions_spec.js +++ b/spec/javascripts/jobs/store/actions_spec.js @@ -68,41 +68,20 @@ describe('Job State actions', () => { describe('hideSidebar', () => { it('should commit HIDE_SIDEBAR mutation', done => { - testAction( - hideSidebar, - null, - mockedState, - [{ type: types.HIDE_SIDEBAR }], - [], - done, - ); + testAction(hideSidebar, null, mockedState, [{ type: types.HIDE_SIDEBAR }], [], done); }); }); describe('showSidebar', () => { it('should commit HIDE_SIDEBAR mutation', done => { - testAction( - showSidebar, - null, - mockedState, - [{ type: types.SHOW_SIDEBAR }], - [], - done, - ); + testAction(showSidebar, null, mockedState, [{ type: types.SHOW_SIDEBAR }], [], done); }); }); describe('toggleSidebar', () => { describe('when isSidebarOpen is true', () => { it('should dispatch hideSidebar', done => { - testAction( - toggleSidebar, - null, - mockedState, - [], - [{ type: 'hideSidebar' }], - done, - ); + testAction(toggleSidebar, null, mockedState, [], [{ type: 'hideSidebar' }], done); }); }); @@ -110,14 +89,7 @@ describe('Job State actions', () => { it('should dispatch showSidebar', done => { mockedState.isSidebarOpen = false; - testAction( - toggleSidebar, - null, - mockedState, - [], - [{ type: 'showSidebar' }], - done, - ); + testAction(toggleSidebar, null, mockedState, [], [{ type: 'showSidebar' }], done); }); }); }); diff --git a/spec/javascripts/jobs/store/getters_spec.js b/spec/javascripts/jobs/store/getters_spec.js index 34e9707eadd..4195d9d3680 100644 --- a/spec/javascripts/jobs/store/getters_spec.js +++ b/spec/javascripts/jobs/store/getters_spec.js @@ -180,7 +180,7 @@ describe('Job Store Getters', () => { it('returns true', () => { localState.job.runners = { available: true, - online: false + online: false, }; expect(getters.hasRunnersForProject(localState)).toEqual(true); @@ -191,7 +191,7 @@ describe('Job Store Getters', () => { it('returns false', () => { localState.job.runners = { available: false, - online: false + online: false, }; expect(getters.hasRunnersForProject(localState)).toEqual(false); @@ -202,7 +202,7 @@ describe('Job Store Getters', () => { it('returns false', () => { localState.job.runners = { available: false, - online: true + online: true, }; expect(getters.hasRunnersForProject(localState)).toEqual(false); diff --git a/spec/javascripts/lib/utils/common_utils_spec.js b/spec/javascripts/lib/utils/common_utils_spec.js index 514d6ddeae5..0fb90c3b78c 100644 --- a/spec/javascripts/lib/utils/common_utils_spec.js +++ b/spec/javascripts/lib/utils/common_utils_spec.js @@ -35,9 +35,7 @@ describe('common_utils', () => { }); it('should decode params', () => { - expect( - commonUtils.urlParamsToArray('?label_name%5B%5D=test')[0], - ).toBe('label_name[]=test'); + expect(commonUtils.urlParamsToArray('?label_name%5B%5D=test')[0]).toBe('label_name[]=test'); }); it('should remove the question mark from the search params', () => { @@ -49,25 +47,19 @@ describe('common_utils', () => { describe('urlParamsToObject', () => { it('parses path for label with trailing +', () => { - expect( - commonUtils.urlParamsToObject('label_name[]=label%2B', {}), - ).toEqual({ + expect(commonUtils.urlParamsToObject('label_name[]=label%2B', {})).toEqual({ label_name: ['label+'], }); }); it('parses path for milestone with trailing +', () => { - expect( - commonUtils.urlParamsToObject('milestone_title=A%2B', {}), - ).toEqual({ + expect(commonUtils.urlParamsToObject('milestone_title=A%2B', {})).toEqual({ milestone_title: 'A+', }); }); it('parses path for search terms with spaces', () => { - expect( - commonUtils.urlParamsToObject('search=two+words', {}), - ).toEqual({ + expect(commonUtils.urlParamsToObject('search=two+words', {})).toEqual({ search: 'two words', }); }); @@ -187,7 +179,11 @@ describe('common_utils', () => { describe('parseQueryStringIntoObject', () => { it('should return object with query parameters', () => { - expect(commonUtils.parseQueryStringIntoObject('scope=all&page=2')).toEqual({ scope: 'all', page: '2' }); + expect(commonUtils.parseQueryStringIntoObject('scope=all&page=2')).toEqual({ + scope: 'all', + page: '2', + }); + expect(commonUtils.parseQueryStringIntoObject('scope=all')).toEqual({ scope: 'all' }); expect(commonUtils.parseQueryStringIntoObject()).toEqual({}); }); @@ -211,7 +207,9 @@ describe('common_utils', () => { describe('buildUrlWithCurrentLocation', () => { it('should build an url with current location and given parameters', () => { expect(commonUtils.buildUrlWithCurrentLocation()).toEqual(window.location.pathname); - expect(commonUtils.buildUrlWithCurrentLocation('?page=2')).toEqual(`${window.location.pathname}?page=2`); + expect(commonUtils.buildUrlWithCurrentLocation('?page=2')).toEqual( + `${window.location.pathname}?page=2`, + ); }); }); @@ -266,21 +264,24 @@ describe('common_utils', () => { }); describe('normalizeCRLFHeaders', () => { - beforeEach(function () { - this.CLRFHeaders = 'a-header: a-value\nAnother-Header: ANOTHER-VALUE\nLaSt-HeAdEr: last-VALUE'; + beforeEach(function() { + this.CLRFHeaders = + 'a-header: a-value\nAnother-Header: ANOTHER-VALUE\nLaSt-HeAdEr: last-VALUE'; spyOn(String.prototype, 'split').and.callThrough(); this.normalizeCRLFHeaders = commonUtils.normalizeCRLFHeaders(this.CLRFHeaders); }); - it('should split by newline', function () { + it('should split by newline', function() { expect(String.prototype.split).toHaveBeenCalledWith('\n'); }); - it('should split by colon+space for each header', function () { - expect(String.prototype.split.calls.allArgs().filter(args => args[0] === ': ').length).toBe(3); + it('should split by colon+space for each header', function() { + expect(String.prototype.split.calls.allArgs().filter(args => args[0] === ': ').length).toBe( + 3, + ); }); - it('should return a normalized headers object', function () { + it('should return a normalized headers object', function() { expect(this.normalizeCRLFHeaders).toEqual({ 'A-HEADER': 'a-value', 'ANOTHER-HEADER': 'ANOTHER-VALUE', @@ -359,67 +360,79 @@ describe('common_utils', () => { spyOn(window, 'setTimeout').and.callFake(cb => origSetTimeout(cb, 0)); }); - it('solves the promise from the callback', (done) => { + it('solves the promise from the callback', done => { const expectedResponseValue = 'Success!'; - commonUtils.backOff((next, stop) => ( - new Promise((resolve) => { - resolve(expectedResponseValue); - }).then((resp) => { - stop(resp); + commonUtils + .backOff((next, stop) => + new Promise(resolve => { + resolve(expectedResponseValue); + }) + .then(resp => { + stop(resp); + }) + .catch(done.fail), + ) + .then(respBackoff => { + expect(respBackoff).toBe(expectedResponseValue); + done(); }) - ).catch(done.fail)).then((respBackoff) => { - expect(respBackoff).toBe(expectedResponseValue); - done(); - }).catch(done.fail); + .catch(done.fail); }); - it('catches the rejected promise from the callback ', (done) => { + it('catches the rejected promise from the callback ', done => { const errorMessage = 'Mistakes were made!'; - commonUtils.backOff((next, stop) => { - new Promise((resolve, reject) => { - reject(new Error(errorMessage)); - }).then((resp) => { - stop(resp); - }).catch(err => stop(err)); - }).catch((errBackoffResp) => { - expect(errBackoffResp instanceof Error).toBe(true); - expect(errBackoffResp.message).toBe(errorMessage); - done(); - }); + commonUtils + .backOff((next, stop) => { + new Promise((resolve, reject) => { + reject(new Error(errorMessage)); + }) + .then(resp => { + stop(resp); + }) + .catch(err => stop(err)); + }) + .catch(errBackoffResp => { + expect(errBackoffResp instanceof Error).toBe(true); + expect(errBackoffResp.message).toBe(errorMessage); + done(); + }); }); - it('solves the promise correctly after retrying a third time', (done) => { + it('solves the promise correctly after retrying a third time', done => { let numberOfCalls = 1; const expectedResponseValue = 'Success!'; - commonUtils.backOff((next, stop) => ( - Promise.resolve(expectedResponseValue) - .then((resp) => { - if (numberOfCalls < 3) { - numberOfCalls += 1; - next(); - } else { - stop(resp); - } - }) - ).catch(done.fail)).then((respBackoff) => { - const timeouts = window.setTimeout.calls.allArgs().map(([, timeout]) => timeout); + commonUtils + .backOff((next, stop) => + Promise.resolve(expectedResponseValue) + .then(resp => { + if (numberOfCalls < 3) { + numberOfCalls += 1; + next(); + } else { + stop(resp); + } + }) + .catch(done.fail), + ) + .then(respBackoff => { + const timeouts = window.setTimeout.calls.allArgs().map(([, timeout]) => timeout); - expect(timeouts).toEqual([2000, 4000]); - expect(respBackoff).toBe(expectedResponseValue); - done(); - }).catch(done.fail); + expect(timeouts).toEqual([2000, 4000]); + expect(respBackoff).toBe(expectedResponseValue); + done(); + }) + .catch(done.fail); }); - it('rejects the backOff promise after timing out', (done) => { - commonUtils.backOff(next => next(), 64000) - .catch((errBackoffResp) => { - const timeouts = window.setTimeout.calls.allArgs().map(([, timeout]) => timeout); + it('rejects the backOff promise after timing out', done => { + commonUtils.backOff(next => next(), 64000).catch(errBackoffResp => { + const timeouts = window.setTimeout.calls.allArgs().map(([, timeout]) => timeout); - expect(timeouts).toEqual([2000, 4000, 8000, 16000, 32000, 32000]); - expect(errBackoffResp instanceof Error).toBe(true); - expect(errBackoffResp.message).toBe('BACKOFF_TIMEOUT'); - done(); - }); + expect(timeouts).toEqual([2000, 4000, 8000, 16000, 32000, 32000]); + expect(errBackoffResp instanceof Error).toBe(true); + expect(errBackoffResp.message).toBe('BACKOFF_TIMEOUT'); + done(); + }); }); }); @@ -466,11 +479,14 @@ describe('common_utils', () => { }); describe('createOverlayIcon', () => { - it('should return the favicon with the overlay', (done) => { - commonUtils.createOverlayIcon(faviconDataUrl, overlayDataUrl).then((url) => { - expect(url).toEqual(faviconWithOverlayDataUrl); - done(); - }).catch(done.fail); + it('should return the favicon with the overlay', done => { + commonUtils + .createOverlayIcon(faviconDataUrl, overlayDataUrl) + .then(url => { + expect(url).toEqual(faviconWithOverlayDataUrl); + done(); + }) + .catch(done.fail); }); }); @@ -486,11 +502,16 @@ describe('common_utils', () => { document.body.removeChild(document.getElementById('favicon')); }); - it('should set page favicon to provided favicon overlay', (done) => { - commonUtils.setFaviconOverlay(overlayDataUrl).then(() => { - expect(document.getElementById('favicon').getAttribute('href')).toEqual(faviconWithOverlayDataUrl); - done(); - }).catch(done.fail); + it('should set page favicon to provided favicon overlay', done => { + commonUtils + .setFaviconOverlay(overlayDataUrl) + .then(() => { + expect(document.getElementById('favicon').getAttribute('href')).toEqual( + faviconWithOverlayDataUrl, + ); + done(); + }) + .catch(done.fail); }); }); @@ -512,24 +533,24 @@ describe('common_utils', () => { document.body.removeChild(document.getElementById('favicon')); }); - it('should reset favicon in case of error', (done) => { + it('should reset favicon in case of error', done => { mock.onGet(BUILD_URL).replyOnce(500); - commonUtils.setCiStatusFavicon(BUILD_URL) - .catch(() => { - const favicon = document.getElementById('favicon'); + commonUtils.setCiStatusFavicon(BUILD_URL).catch(() => { + const favicon = document.getElementById('favicon'); - expect(favicon.getAttribute('href')).toEqual(faviconDataUrl); - done(); - }); + expect(favicon.getAttribute('href')).toEqual(faviconDataUrl); + done(); + }); }); - it('should set page favicon to CI status favicon based on provided status', (done) => { + it('should set page favicon to CI status favicon based on provided status', done => { mock.onGet(BUILD_URL).reply(200, { favicon: overlayDataUrl, }); - commonUtils.setCiStatusFavicon(BUILD_URL) + commonUtils + .setCiStatusFavicon(BUILD_URL) .then(() => { const favicon = document.getElementById('favicon'); @@ -554,11 +575,15 @@ describe('common_utils', () => { }); it('should return the svg for a linked icon', () => { - expect(commonUtils.spriteIcon('test')).toEqual('<svg ><use xlink:href="icons.svg#test" /></svg>'); + expect(commonUtils.spriteIcon('test')).toEqual( + '<svg ><use xlink:href="icons.svg#test" /></svg>', + ); }); it('should set svg className when passed', () => { - expect(commonUtils.spriteIcon('test', 'fa fa-test')).toEqual('<svg class="fa fa-test"><use xlink:href="icons.svg#test" /></svg>'); + expect(commonUtils.spriteIcon('test', 'fa fa-test')).toEqual( + '<svg class="fa fa-test"><use xlink:href="icons.svg#test" /></svg>', + ); }); }); @@ -578,7 +603,7 @@ describe('common_utils', () => { const convertedObj = commonUtils.convertObjectPropsToCamelCase(mockObj); - Object.keys(convertedObj).forEach((prop) => { + Object.keys(convertedObj).forEach(prop => { expect(snakeRegEx.test(prop)).toBeFalsy(); expect(convertedObj[prop]).toBe(mockObj[mappings[prop]]); }); @@ -597,9 +622,7 @@ describe('common_utils', () => { }, }; - expect( - commonUtils.convertObjectPropsToCamelCase(obj), - ).toEqual({ + expect(commonUtils.convertObjectPropsToCamelCase(obj)).toEqual({ snakeKey: { child_snake_key: 'value', }, @@ -614,9 +637,7 @@ describe('common_utils', () => { }, }; - expect( - commonUtils.convertObjectPropsToCamelCase(obj, { deep: true }), - ).toEqual({ + expect(commonUtils.convertObjectPropsToCamelCase(obj, { deep: true })).toEqual({ snakeKey: { childSnakeKey: 'value', }, @@ -630,9 +651,7 @@ describe('common_utils', () => { }, ]; - expect( - commonUtils.convertObjectPropsToCamelCase(arr, { deep: true }), - ).toEqual([ + expect(commonUtils.convertObjectPropsToCamelCase(arr, { deep: true })).toEqual([ { childSnakeKey: 'value', }, @@ -648,9 +667,7 @@ describe('common_utils', () => { ], ]; - expect( - commonUtils.convertObjectPropsToCamelCase(arr, { deep: true }), - ).toEqual([ + expect(commonUtils.convertObjectPropsToCamelCase(arr, { deep: true })).toEqual([ [ { childSnakeKey: 'value', diff --git a/spec/javascripts/lib/utils/datetime_utility_spec.js b/spec/javascripts/lib/utils/datetime_utility_spec.js index de6b96aab57..d699e66b8ca 100644 --- a/spec/javascripts/lib/utils/datetime_utility_spec.js +++ b/spec/javascripts/lib/utils/datetime_utility_spec.js @@ -199,11 +199,11 @@ describe('datefix', () => { expect(datetimeUtility.pad(2)).toEqual('02'); }); - it('should not add a zero when lenght matches the default', () => { + it('should not add a zero when length matches the default', () => { expect(datetimeUtility.pad(12)).toEqual('12'); }); - it('should add a 0 when lenght is smaller than the provided', () => { + it('should add a 0 when length is smaller than the provided', () => { expect(datetimeUtility.pad(12, 3)).toEqual('012'); }); }); diff --git a/spec/javascripts/lib/utils/number_utility_spec.js b/spec/javascripts/lib/utils/number_utility_spec.js index a5099a2a3b8..94c6214c86a 100644 --- a/spec/javascripts/lib/utils/number_utility_spec.js +++ b/spec/javascripts/lib/utils/number_utility_spec.js @@ -1,4 +1,10 @@ -import { formatRelevantDigits, bytesToKiB, bytesToMiB, bytesToGiB, numberToHumanSize } from '~/lib/utils/number_utils'; +import { + formatRelevantDigits, + bytesToKiB, + bytesToMiB, + bytesToGiB, + numberToHumanSize, +} from '~/lib/utils/number_utils'; describe('Number Utils', () => { describe('formatRelevantDigits', () => { diff --git a/spec/javascripts/lib/utils/text_utility_spec.js b/spec/javascripts/lib/utils/text_utility_spec.js index ac3270baef5..92ebfc38722 100644 --- a/spec/javascripts/lib/utils/text_utility_spec.js +++ b/spec/javascripts/lib/utils/text_utility_spec.js @@ -120,7 +120,7 @@ describe('text_utility', () => { }); describe('getFirstCharacterCapitalized', () => { - it('returns the first character captialized, if first character is alphabetic', () => { + it('returns the first character capitalized, if first character is alphabetic', () => { expect(textUtils.getFirstCharacterCapitalized('loremIpsumDolar')).toEqual('L'); expect(textUtils.getFirstCharacterCapitalized('Sit amit !')).toEqual('S'); }); diff --git a/spec/javascripts/monitoring/graph/flag_spec.js b/spec/javascripts/monitoring/graph/flag_spec.js index a837b71db0b..038bfffd44f 100644 --- a/spec/javascripts/monitoring/graph/flag_spec.js +++ b/spec/javascripts/monitoring/graph/flag_spec.js @@ -2,7 +2,7 @@ import Vue from 'vue'; import GraphFlag from '~/monitoring/components/graph/flag.vue'; import { deploymentData } from '../mock_data'; -const createComponent = (propsData) => { +const createComponent = propsData => { const Component = Vue.extend(GraphFlag); return new Component({ @@ -51,8 +51,7 @@ describe('GraphFlag', () => { it('has a line at the currentXCoordinate', () => { component = createComponent(defaultValuesComponent); - expect(component.$el.style.left) - .toEqual(`${70 + component.currentXCoordinate}px`); + expect(component.$el.style.left).toEqual(`${70 + component.currentXCoordinate}px`); }); describe('Deployment flag', () => { @@ -62,9 +61,7 @@ describe('GraphFlag', () => { deploymentFlagData, }); - expect( - deploymentFlagComponent.$el.querySelector('.popover-title'), - ).toContainText('Deployed'); + expect(deploymentFlagComponent.$el.querySelector('.popover-title')).toContainText('Deployed'); }); it('contains the ref when a tag is available', () => { @@ -78,13 +75,13 @@ describe('GraphFlag', () => { }, }); - expect( - deploymentFlagComponent.$el.querySelector('.deploy-meta-content'), - ).toContainText('f5bcd1d9'); + expect(deploymentFlagComponent.$el.querySelector('.deploy-meta-content')).toContainText( + 'f5bcd1d9', + ); - expect( - deploymentFlagComponent.$el.querySelector('.deploy-meta-content'), - ).toContainText('1.0'); + expect(deploymentFlagComponent.$el.querySelector('.deploy-meta-content')).toContainText( + '1.0', + ); }); it('does not contain the ref when a tag is unavailable', () => { @@ -98,13 +95,13 @@ describe('GraphFlag', () => { }, }); - expect( - deploymentFlagComponent.$el.querySelector('.deploy-meta-content'), - ).toContainText('f5bcd1d9'); + expect(deploymentFlagComponent.$el.querySelector('.deploy-meta-content')).toContainText( + 'f5bcd1d9', + ); - expect( - deploymentFlagComponent.$el.querySelector('.deploy-meta-content'), - ).not.toContainText('1.0'); + expect(deploymentFlagComponent.$el.querySelector('.deploy-meta-content')).not.toContainText( + '1.0', + ); }); }); diff --git a/spec/javascripts/notes/components/discussion_filter_spec.js b/spec/javascripts/notes/components/discussion_filter_spec.js index 70dd5bb3be5..9070d968cfd 100644 --- a/spec/javascripts/notes/components/discussion_filter_spec.js +++ b/spec/javascripts/notes/components/discussion_filter_spec.js @@ -11,13 +11,15 @@ describe('DiscussionFilter component', () => { beforeEach(() => { store = createStore(); - const discussions = [{ - ...discussionMock, - id: discussionMock.id, - notes: [{ ...discussionMock.notes[0], resolvable: true, resolved: true }], - }]; + const discussions = [ + { + ...discussionMock, + id: discussionMock.id, + notes: [{ ...discussionMock.notes[0], resolvable: true, resolved: true }], + }, + ]; const Component = Vue.extend(DiscussionFilter); - const defaultValue = discussionFiltersMock[0].value; + const selectedValue = discussionFiltersMock[0].value; store.state.discussions = discussions; vm = mountComponentWithStore(Component, { @@ -25,7 +27,7 @@ describe('DiscussionFilter component', () => { store, props: { filters: discussionFiltersMock, - defaultValue, + selectedValue, }, }); }); @@ -35,11 +37,15 @@ describe('DiscussionFilter component', () => { }); it('renders the all filters', () => { - expect(vm.$el.querySelectorAll('.dropdown-menu li').length).toEqual(discussionFiltersMock.length); + expect(vm.$el.querySelectorAll('.dropdown-menu li').length).toEqual( + discussionFiltersMock.length, + ); }); it('renders the default selected item', () => { - expect(vm.$el.querySelector('#discussion-filter-dropdown').textContent.trim()).toEqual(discussionFiltersMock[0].title); + expect(vm.$el.querySelector('#discussion-filter-dropdown').textContent.trim()).toEqual( + discussionFiltersMock[0].title, + ); }); it('updates to the selected item', () => { @@ -57,4 +63,24 @@ describe('DiscussionFilter component', () => { expect(vm.filterDiscussion).not.toHaveBeenCalled(); }); + + it('disables commenting when "Show history only" filter is applied', () => { + const filterItem = vm.$el.querySelector('.dropdown-menu li:last-child button'); + filterItem.click(); + + expect(vm.$store.state.commentsDisabled).toBe(true); + }); + + it('enables commenting when "Show history only" filter is not applied', () => { + const filterItem = vm.$el.querySelector('.dropdown-menu li:first-child button'); + filterItem.click(); + + expect(vm.$store.state.commentsDisabled).toBe(false); + }); + + it('renders a dropdown divider for the default filter', () => { + const defaultFilter = vm.$el.querySelector('.dropdown-menu li:first-child'); + + expect(defaultFilter.lastChild.classList).toContain('dropdown-divider'); + }); }); diff --git a/spec/javascripts/notes/components/note_app_spec.js b/spec/javascripts/notes/components/note_app_spec.js index 06b30375306..0081f42c330 100644 --- a/spec/javascripts/notes/components/note_app_spec.js +++ b/spec/javascripts/notes/components/note_app_spec.js @@ -97,7 +97,8 @@ describe('note_app', () => { }); it('should render list of notes', done => { - const note = mockData.INDIVIDUAL_NOTE_RESPONSE_MAP.GET[ + const note = + mockData.INDIVIDUAL_NOTE_RESPONSE_MAP.GET[ '/gitlab-org/gitlab-ce/issues/26/discussions.json' ][0].notes[0]; @@ -120,6 +121,13 @@ describe('note_app', () => { ).toEqual('Write a comment or drag your files here…'); }); + it('should not render form when commenting is disabled', () => { + store.state.commentsDisabled = true; + vm = mountComponent(); + + expect(vm.$el.querySelector('.js-main-target-form')).toEqual(null); + }); + it('should render form comment button as disabled', () => { expect(vm.$el.querySelector('.js-note-new-discussion').getAttribute('disabled')).toEqual( 'disabled', diff --git a/spec/javascripts/notes/stores/actions_spec.js b/spec/javascripts/notes/stores/actions_spec.js index f4643fd55ed..0c0bc45b201 100644 --- a/spec/javascripts/notes/stores/actions_spec.js +++ b/spec/javascripts/notes/stores/actions_spec.js @@ -509,4 +509,17 @@ describe('Actions Notes Store', () => { expect(mrWidgetEventHub.$emit).toHaveBeenCalledWith('mr.discussion.updated'); }); }); + + describe('setCommentsDisabled', () => { + it('should set comments disabled state', done => { + testAction( + actions.setCommentsDisabled, + true, + null, + [{ type: 'DISABLE_COMMENTS', payload: true }], + [], + done, + ); + }); + }); }); diff --git a/spec/javascripts/notes/stores/mutation_spec.js b/spec/javascripts/notes/stores/mutation_spec.js index 9d652ba9f1e..461de5a3106 100644 --- a/spec/javascripts/notes/stores/mutation_spec.js +++ b/spec/javascripts/notes/stores/mutation_spec.js @@ -78,7 +78,7 @@ describe('Notes Store mutations', () => { }); describe('COLLAPSE_DISCUSSION', () => { - it('should collpase an expanded discussion', () => { + it('should collapse an expanded discussion', () => { const discussion = Object.assign({}, discussionMock, { expanded: true }); const state = { @@ -427,4 +427,14 @@ describe('Notes Store mutations', () => { expect(state.discussions[0].expanded).toBe(true); }); }); + + describe('DISABLE_COMMENTS', () => { + it('should set comments disabled state', () => { + const state = {}; + + mutations.DISABLE_COMMENTS(state, true); + + expect(state.commentsDisabled).toEqual(true); + }); + }); }); diff --git a/spec/javascripts/pipelines/empty_state_spec.js b/spec/javascripts/pipelines/empty_state_spec.js index e21dca45fa1..f12950b8fce 100644 --- a/spec/javascripts/pipelines/empty_state_spec.js +++ b/spec/javascripts/pipelines/empty_state_spec.js @@ -24,7 +24,7 @@ describe('Pipelines Empty State', () => { expect(component.$el.querySelector('.svg-content svg')).toBeDefined(); }); - it('should render emtpy state information', () => { + it('should render empty state information', () => { expect(component.$el.querySelector('h4').textContent).toContain('Build with confidence'); expect( diff --git a/spec/javascripts/pipelines/graph/action_component_spec.js b/spec/javascripts/pipelines/graph/action_component_spec.js index 027066e1d4d..3d2232ff239 100644 --- a/spec/javascripts/pipelines/graph/action_component_spec.js +++ b/spec/javascripts/pipelines/graph/action_component_spec.js @@ -50,7 +50,7 @@ describe('pipeline graph action component', () => { }); describe('on click', () => { - it('emits `pipelineActionRequestComplete` after a successfull request', done => { + it('emits `pipelineActionRequestComplete` after a successful request', done => { spyOn(component, '$emit'); component.$el.click(); diff --git a/spec/javascripts/pipelines/graph/graph_component_spec.js b/spec/javascripts/pipelines/graph/graph_component_spec.js index b6fa4272c8b..96a2d5f62fa 100644 --- a/spec/javascripts/pipelines/graph/graph_component_spec.js +++ b/spec/javascripts/pipelines/graph/graph_component_spec.js @@ -40,7 +40,9 @@ describe('graph component', () => { ).toEqual(true); expect( - component.$el.querySelector('.stage-column:nth-child(2) .build:nth-child(1)').classList.contains('left-connector'), + component.$el + .querySelector('.stage-column:nth-child(2) .build:nth-child(1)') + .classList.contains('left-connector'), ).toEqual(true); expect(component.$el.querySelector('loading-icon')).toBe(null); @@ -56,7 +58,9 @@ describe('graph component', () => { pipeline: graphJSON, }); - expect(component.$el.querySelector('.stage-column:nth-child(2) .stage-name').textContent.trim()).toEqual('Deploy <img src=x onerror=alert(document.domain)>'); + expect( + component.$el.querySelector('.stage-column:nth-child(2) .stage-name').textContent.trim(), + ).toEqual('Deploy <img src=x onerror=alert(document.domain)>'); }); }); }); diff --git a/spec/javascripts/pipelines/pipelines_spec.js b/spec/javascripts/pipelines/pipelines_spec.js index 37908153e0e..97ded16db69 100644 --- a/spec/javascripts/pipelines/pipelines_spec.js +++ b/spec/javascripts/pipelines/pipelines_spec.js @@ -372,7 +372,7 @@ describe('Pipelines', () => { }); }); - describe('successfull request', () => { + describe('successful request', () => { describe('with pipelines', () => { beforeEach(() => { mock.onGet('twitter/flight/pipelines.json').reply(200, pipelines); @@ -667,7 +667,7 @@ describe('Pipelines', () => { }); }); - it('returns false when state is emtpy state', done => { + it('returns false when state is empty state', done => { vm.isLoading = false; vm.hasMadeRequest = true; vm.hasGitlabCi = false; diff --git a/spec/javascripts/pipelines/stage_spec.js b/spec/javascripts/pipelines/stage_spec.js index a3caaeb44dc..3c8b8032de8 100644 --- a/spec/javascripts/pipelines/stage_spec.js +++ b/spec/javascripts/pipelines/stage_spec.js @@ -40,7 +40,7 @@ describe('Pipelines stage component', () => { expect(component.$el.querySelector('button').getAttribute('data-toggle')).toEqual('dropdown'); }); - describe('with successfull request', () => { + describe('with successful request', () => { beforeEach(() => { mock.onGet('path.json').reply(200, stageReply); }); diff --git a/spec/javascripts/reports/components/grouped_test_reports_app_spec.js b/spec/javascripts/reports/components/grouped_test_reports_app_spec.js index f58515daa4f..69767d9cf1c 100644 --- a/spec/javascripts/reports/components/grouped_test_reports_app_spec.js +++ b/spec/javascripts/reports/components/grouped_test_reports_app_spec.js @@ -151,11 +151,11 @@ describe('Grouped Test Reports App', () => { it('renders resolved failures', done => { setTimeout(() => { - expect(vm.$el.querySelector('.js-mr-code-resolved-issues').textContent).toContain( + expect(vm.$el.querySelector('.report-block-container').textContent).toContain( resolvedFailures.suites[0].resolved_failures[0].name, ); - expect(vm.$el.querySelector('.js-mr-code-resolved-issues').textContent).toContain( + expect(vm.$el.querySelector('.report-block-container').textContent).toContain( resolvedFailures.suites[0].resolved_failures[1].name, ); done(); diff --git a/spec/javascripts/reports/components/report_section_spec.js b/spec/javascripts/reports/components/report_section_spec.js index eb7307605d7..b02af8baaec 100644 --- a/spec/javascripts/reports/components/report_section_spec.js +++ b/spec/javascripts/reports/components/report_section_spec.js @@ -120,7 +120,7 @@ describe('Report section', () => { 'Code quality improved on 1 point and degraded on 1 point', ); - expect(vm.$el.querySelectorAll('.js-mr-code-resolved-issues li').length).toEqual( + expect(vm.$el.querySelectorAll('.report-block-container li').length).toEqual( resolvedIssues.length, ); }); diff --git a/spec/javascripts/sidebar/assignees_spec.js b/spec/javascripts/sidebar/assignees_spec.js index e7f8f4f9936..eced4925489 100644 --- a/spec/javascripts/sidebar/assignees_spec.js +++ b/spec/javascripts/sidebar/assignees_spec.js @@ -78,9 +78,7 @@ describe('Assignee component', () => { component = new AssigneeComponent({ propsData: { rootPath: 'http://localhost:3000', - users: [ - UsersMock.user, - ], + users: [UsersMock.user], editable: false, }, }).$mount(); @@ -90,7 +88,10 @@ describe('Assignee component', () => { expect(collapsed.childElementCount).toEqual(1); expect(assignee.querySelector('.avatar').getAttribute('src')).toEqual(UsersMock.user.avatar); - expect(assignee.querySelector('.avatar').getAttribute('alt')).toEqual(`${UsersMock.user.name}'s avatar`); + expect(assignee.querySelector('.avatar').getAttribute('alt')).toEqual( + `${UsersMock.user.name}'s avatar`, + ); + expect(assignee.querySelector('.author').innerText.trim()).toEqual(UsersMock.user.name); }); @@ -98,34 +99,38 @@ describe('Assignee component', () => { component = new AssigneeComponent({ propsData: { rootPath: 'http://localhost:3000/', - users: [ - UsersMock.user, - ], + users: [UsersMock.user], editable: true, }, }).$mount(); expect(component.$el.querySelector('.author-link')).not.toBeNull(); // The image - expect(component.$el.querySelector('.author-link img').getAttribute('src')).toEqual(UsersMock.user.avatar); + expect(component.$el.querySelector('.author-link img').getAttribute('src')).toEqual( + UsersMock.user.avatar, + ); // Author name - expect(component.$el.querySelector('.author-link .author').innerText.trim()).toEqual(UsersMock.user.name); + expect(component.$el.querySelector('.author-link .author').innerText.trim()).toEqual( + UsersMock.user.name, + ); // Username - expect(component.$el.querySelector('.author-link .username').innerText.trim()).toEqual(`@${UsersMock.user.username}`); + expect(component.$el.querySelector('.author-link .username').innerText.trim()).toEqual( + `@${UsersMock.user.username}`, + ); }); it('has the root url present in the assigneeUrl method', () => { component = new AssigneeComponent({ propsData: { rootPath: 'http://localhost:3000/', - users: [ - UsersMock.user, - ], + users: [UsersMock.user], editable: true, }, }).$mount(); - expect(component.assigneeUrl(UsersMock.user).indexOf('http://localhost:3000/')).not.toEqual(-1); + expect(component.assigneeUrl(UsersMock.user).indexOf('http://localhost:3000/')).not.toEqual( + -1, + ); }); }); @@ -147,13 +152,19 @@ describe('Assignee component', () => { const first = collapsed.children[0]; expect(first.querySelector('.avatar').getAttribute('src')).toEqual(users[0].avatar); - expect(first.querySelector('.avatar').getAttribute('alt')).toEqual(`${users[0].name}'s avatar`); + expect(first.querySelector('.avatar').getAttribute('alt')).toEqual( + `${users[0].name}'s avatar`, + ); + expect(first.querySelector('.author').innerText.trim()).toEqual(users[0].name); const second = collapsed.children[1]; expect(second.querySelector('.avatar').getAttribute('src')).toEqual(users[1].avatar); - expect(second.querySelector('.avatar').getAttribute('alt')).toEqual(`${users[1].name}'s avatar`); + expect(second.querySelector('.avatar').getAttribute('alt')).toEqual( + `${users[1].name}'s avatar`, + ); + expect(second.querySelector('.author').innerText.trim()).toEqual(users[1].name); }); @@ -174,7 +185,10 @@ describe('Assignee component', () => { const first = collapsed.children[0]; expect(first.querySelector('.avatar').getAttribute('src')).toEqual(users[0].avatar); - expect(first.querySelector('.avatar').getAttribute('alt')).toEqual(`${users[0].name}'s avatar`); + expect(first.querySelector('.avatar').getAttribute('alt')).toEqual( + `${users[0].name}'s avatar`, + ); + expect(first.querySelector('.author').innerText.trim()).toEqual(users[0].name); const second = collapsed.children[1]; @@ -196,7 +210,7 @@ describe('Assignee component', () => { expect(component.$el.querySelector('.user-list-more')).toBe(null); }); - it('Shows the "show-less" assignees label', (done) => { + it('Shows the "show-less" assignees label', done => { const users = UsersMockHelper.createNumberRandomUsers(6); component = new AssigneeComponent({ propsData: { @@ -206,21 +220,26 @@ describe('Assignee component', () => { }, }).$mount(); - expect(component.$el.querySelectorAll('.user-item').length).toEqual(component.defaultRenderCount); + expect(component.$el.querySelectorAll('.user-item').length).toEqual( + component.defaultRenderCount, + ); + expect(component.$el.querySelector('.user-list-more')).not.toBe(null); const usersLabelExpectation = users.length - component.defaultRenderCount; - expect(component.$el.querySelector('.user-list-more .btn-link').innerText.trim()) - .not.toBe(`+${usersLabelExpectation} more`); + expect(component.$el.querySelector('.user-list-more .btn-link').innerText.trim()).not.toBe( + `+${usersLabelExpectation} more`, + ); component.toggleShowLess(); Vue.nextTick(() => { - expect(component.$el.querySelector('.user-list-more .btn-link').innerText.trim()) - .toBe('- show less'); + expect(component.$el.querySelector('.user-list-more .btn-link').innerText.trim()).toBe( + '- show less', + ); done(); }); }); - it('Shows the "show-less" when "n+ more " label is clicked', (done) => { + it('Shows the "show-less" when "n+ more " label is clicked', done => { const users = UsersMockHelper.createNumberRandomUsers(6); component = new AssigneeComponent({ propsData: { @@ -232,8 +251,9 @@ describe('Assignee component', () => { component.$el.querySelector('.user-list-more .btn-link').click(); Vue.nextTick(() => { - expect(component.$el.querySelector('.user-list-more .btn-link').innerText.trim()) - .toBe('- show less'); + expect(component.$el.querySelector('.user-list-more .btn-link').innerText.trim()).toBe( + '- show less', + ); done(); }); }); @@ -264,16 +284,18 @@ describe('Assignee component', () => { }); it('shows "+1 more" label', () => { - expect(component.$el.querySelector('.user-list-more .btn-link').innerText.trim()) - .toBe('+ 1 more'); + expect(component.$el.querySelector('.user-list-more .btn-link').innerText.trim()).toBe( + '+ 1 more', + ); }); - it('shows "show less" label', (done) => { + it('shows "show less" label', done => { component.toggleShowLess(); Vue.nextTick(() => { - expect(component.$el.querySelector('.user-list-more .btn-link').innerText.trim()) - .toBe('- show less'); + expect(component.$el.querySelector('.user-list-more .btn-link').innerText.trim()).toBe( + '- show less', + ); done(); }); }); diff --git a/spec/javascripts/vue_mr_widget/components/deployment_spec.js b/spec/javascripts/vue_mr_widget/components/deployment_spec.js index 3d44af11153..2f1bd00fa10 100644 --- a/spec/javascripts/vue_mr_widget/components/deployment_spec.js +++ b/spec/javascripts/vue_mr_widget/components/deployment_spec.js @@ -242,6 +242,10 @@ describe('Deployment component', () => { it('renders information about running deployment', () => { expect(vm.$el.querySelector('.js-deployment-info').textContent).toContain('Deploying to'); }); + + it('renders disabled stop button', () => { + expect(vm.$el.querySelector('.js-stop-env').getAttribute('disabled')).toBe('disabled'); + }); }); describe('success', () => { diff --git a/spec/javascripts/vue_mr_widget/components/mr_widget_pipeline_spec.js b/spec/javascripts/vue_mr_widget/components/mr_widget_pipeline_spec.js index 689580e6b91..d905bbe4040 100644 --- a/spec/javascripts/vue_mr_widget/components/mr_widget_pipeline_spec.js +++ b/spec/javascripts/vue_mr_widget/components/mr_widget_pipeline_spec.js @@ -69,12 +69,12 @@ describe('MRWidgetPipeline', () => { pipeline: mockData.pipeline, hasCi: true, ciStatus: null, - troubleshootingDocsPath: 'help', + troubleshootingDocsPath: 'help', }); - expect( - vm.$el.querySelector('.media-body').textContent.trim(), - ).toContain('Could not retrieve the pipeline status. For troubleshooting steps, read the <a href="help">documentation.</a>'); + expect(vm.$el.querySelector('.media-body').textContent.trim()).toContain( + 'Could not retrieve the pipeline status. For troubleshooting steps, read the documentation.', + ); }); describe('with a pipeline', () => { @@ -88,34 +88,36 @@ describe('MRWidgetPipeline', () => { }); it('should render pipeline ID', () => { - expect( - vm.$el.querySelector('.pipeline-id').textContent.trim(), - ).toEqual(`#${mockData.pipeline.id}`); + expect(vm.$el.querySelector('.pipeline-id').textContent.trim()).toEqual( + `#${mockData.pipeline.id}`, + ); }); it('should render pipeline status and commit id', () => { - expect( - vm.$el.querySelector('.media-body').textContent.trim(), - ).toContain(mockData.pipeline.details.status.label); + expect(vm.$el.querySelector('.media-body').textContent.trim()).toContain( + mockData.pipeline.details.status.label, + ); - expect( - vm.$el.querySelector('.js-commit-link').textContent.trim(), - ).toEqual(mockData.pipeline.commit.short_id); + expect(vm.$el.querySelector('.js-commit-link').textContent.trim()).toEqual( + mockData.pipeline.commit.short_id, + ); - expect( - vm.$el.querySelector('.js-commit-link').getAttribute('href'), - ).toEqual(mockData.pipeline.commit.commit_path); + expect(vm.$el.querySelector('.js-commit-link').getAttribute('href')).toEqual( + mockData.pipeline.commit.commit_path, + ); }); it('should render pipeline graph', () => { expect(vm.$el.querySelector('.mr-widget-pipeline-graph')).toBeDefined(); - expect(vm.$el.querySelectorAll('.stage-container').length).toEqual(mockData.pipeline.details.stages.length); + expect(vm.$el.querySelectorAll('.stage-container').length).toEqual( + mockData.pipeline.details.stages.length, + ); }); it('should render coverage information', () => { - expect( - vm.$el.querySelector('.media-body').textContent, - ).toContain(`Coverage ${mockData.pipeline.coverage}`); + expect(vm.$el.querySelector('.media-body').textContent).toContain( + `Coverage ${mockData.pipeline.coverage}`, + ); }); }); @@ -133,30 +135,30 @@ describe('MRWidgetPipeline', () => { }); it('should render pipeline ID', () => { - expect( - vm.$el.querySelector('.pipeline-id').textContent.trim(), - ).toEqual(`#${mockData.pipeline.id}`); + expect(vm.$el.querySelector('.pipeline-id').textContent.trim()).toEqual( + `#${mockData.pipeline.id}`, + ); }); it('should render pipeline status', () => { - expect( - vm.$el.querySelector('.media-body').textContent.trim(), - ).toContain(mockData.pipeline.details.status.label); + expect(vm.$el.querySelector('.media-body').textContent.trim()).toContain( + mockData.pipeline.details.status.label, + ); - expect( - vm.$el.querySelector('.js-commit-link'), - ).toBeNull(); + expect(vm.$el.querySelector('.js-commit-link')).toBeNull(); }); it('should render pipeline graph', () => { expect(vm.$el.querySelector('.mr-widget-pipeline-graph')).toBeDefined(); - expect(vm.$el.querySelectorAll('.stage-container').length).toEqual(mockData.pipeline.details.stages.length); + expect(vm.$el.querySelectorAll('.stage-container').length).toEqual( + mockData.pipeline.details.stages.length, + ); }); it('should render coverage information', () => { - expect( - vm.$el.querySelector('.media-body').textContent, - ).toContain(`Coverage ${mockData.pipeline.coverage}`); + expect(vm.$el.querySelector('.media-body').textContent).toContain( + `Coverage ${mockData.pipeline.coverage}`, + ); }); }); @@ -172,9 +174,7 @@ describe('MRWidgetPipeline', () => { troubleshootingDocsPath: 'help', }); - expect( - vm.$el.querySelector('.media-body').textContent, - ).not.toContain('Coverage'); + expect(vm.$el.querySelector('.media-body').textContent).not.toContain('Coverage'); }); }); diff --git a/spec/javascripts/vue_mr_widget/components/states/mr_widget_merged_spec.js b/spec/javascripts/vue_mr_widget/components/states/mr_widget_merged_spec.js index d68342635ef..da5cb752c6f 100644 --- a/spec/javascripts/vue_mr_widget/components/states/mr_widget_merged_spec.js +++ b/spec/javascripts/vue_mr_widget/components/states/mr_widget_merged_spec.js @@ -69,7 +69,7 @@ describe('MRWidgetMerged', () => { expect(vm.shouldShowRemoveSourceBranch).toEqual(true); }); - it('returns false wehn sourceBranchRemoved is true', () => { + it('returns false when sourceBranchRemoved is true', () => { vm.mr.sourceBranchRemoved = true; expect(vm.shouldShowRemoveSourceBranch).toEqual(false); diff --git a/spec/javascripts/vue_mr_widget/mock_data.js b/spec/javascripts/vue_mr_widget/mock_data.js index c5e30a730cb..17554c4fe42 100644 --- a/spec/javascripts/vue_mr_widget/mock_data.js +++ b/spec/javascripts/vue_mr_widget/mock_data.js @@ -218,6 +218,7 @@ export default { diverged_commits_count: 0, only_allow_merge_if_pipeline_succeeds: false, commit_change_content_path: '/root/acets-app/merge_requests/22/commit_change_content', - merge_commit_path: 'http://localhost:3000/root/acets-app/commit/53027d060246c8f47e4a9310fb332aa52f221775', - troubleshooting_docs_path: 'help' + merge_commit_path: + 'http://localhost:3000/root/acets-app/commit/53027d060246c8f47e4a9310fb332aa52f221775', + troubleshooting_docs_path: 'help', }; diff --git a/spec/javascripts/vue_mr_widget/mr_widget_options_spec.js b/spec/javascripts/vue_mr_widget/mr_widget_options_spec.js index 27b6c91e154..09fbe87b27e 100644 --- a/spec/javascripts/vue_mr_widget/mr_widget_options_spec.js +++ b/spec/javascripts/vue_mr_widget/mr_widget_options_spec.js @@ -454,7 +454,7 @@ describe('mrWidgetOptions', () => { deployed_at: '2017-03-22T22:44:42.258Z', deployed_at_formatted: 'Mar 22, 2017 10:44pm', changes, - status: 'success' + status: 'success', }; beforeEach(done => { @@ -607,33 +607,36 @@ describe('mrWidgetOptions', () => { describe('with post merge deployments', () => { beforeEach(done => { - vm.mr.postMergeDeployments = [{ - id: 15, - name: 'review/diplo', - url: '/root/acets-review-apps/environments/15', - stop_url: '/root/acets-review-apps/environments/15/stop', - metrics_url: '/root/acets-review-apps/environments/15/deployments/1/metrics', - metrics_monitoring_url: '/root/acets-review-apps/environments/15/metrics', - external_url: 'http://diplo.', - external_url_formatted: 'diplo.', - deployed_at: '2017-03-22T22:44:42.258Z', - deployed_at_formatted: 'Mar 22, 2017 10:44pm', - changes: [ - { - path: 'index.html', - external_url: 'http://root-master-patch-91341.volatile-watch.surge.sh/index.html', - }, - { - path: 'imgs/gallery.html', - external_url: 'http://root-master-patch-91341.volatile-watch.surge.sh/imgs/gallery.html', - }, - { - path: 'about/', - external_url: 'http://root-master-patch-91341.volatile-watch.surge.sh/about/', - }, - ], - status: 'success' - }]; + vm.mr.postMergeDeployments = [ + { + id: 15, + name: 'review/diplo', + url: '/root/acets-review-apps/environments/15', + stop_url: '/root/acets-review-apps/environments/15/stop', + metrics_url: '/root/acets-review-apps/environments/15/deployments/1/metrics', + metrics_monitoring_url: '/root/acets-review-apps/environments/15/metrics', + external_url: 'http://diplo.', + external_url_formatted: 'diplo.', + deployed_at: '2017-03-22T22:44:42.258Z', + deployed_at_formatted: 'Mar 22, 2017 10:44pm', + changes: [ + { + path: 'index.html', + external_url: 'http://root-master-patch-91341.volatile-watch.surge.sh/index.html', + }, + { + path: 'imgs/gallery.html', + external_url: + 'http://root-master-patch-91341.volatile-watch.surge.sh/imgs/gallery.html', + }, + { + path: 'about/', + external_url: 'http://root-master-patch-91341.volatile-watch.surge.sh/about/', + }, + ], + status: 'success', + }, + ]; vm.$nextTick(done); }); diff --git a/spec/javascripts/vue_shared/components/filtered_search_dropdown_spec.js b/spec/javascripts/vue_shared/components/filtered_search_dropdown_spec.js index b71cb36ecf6..b84b5ae67a8 100644 --- a/spec/javascripts/vue_shared/components/filtered_search_dropdown_spec.js +++ b/spec/javascripts/vue_shared/components/filtered_search_dropdown_spec.js @@ -41,7 +41,7 @@ describe('Filtered search dropdown', () => { }); }); - describe('when visible number is bigger than the items lenght', () => { + describe('when visible number is bigger than the items length', () => { beforeEach(() => { vm = mountComponent(Component, { items: [{ title: 'One' }, { title: 'Two' }, { title: 'Three' }], diff --git a/spec/javascripts/vue_shared/components/sidebar/collapsed_grouped_date_picker_spec.js b/spec/javascripts/vue_shared/components/sidebar/collapsed_grouped_date_picker_spec.js index 3483b7d387d..c507a97d37e 100644 --- a/spec/javascripts/vue_shared/components/sidebar/collapsed_grouped_date_picker_spec.js +++ b/spec/javascripts/vue_shared/components/sidebar/collapsed_grouped_date_picker_spec.js @@ -12,7 +12,7 @@ describe('collapsedGroupedDatePicker', () => { }); describe('toggleCollapse events', () => { - beforeEach((done) => { + beforeEach(done => { spyOn(vm, 'toggleSidebar'); vm.minDate = new Date('07/17/2016'); Vue.nextTick(done); @@ -26,7 +26,7 @@ describe('collapsedGroupedDatePicker', () => { }); describe('minDate and maxDate', () => { - beforeEach((done) => { + beforeEach(done => { vm.minDate = new Date('07/17/2016'); vm.maxDate = new Date('07/17/2017'); Vue.nextTick(done); @@ -42,7 +42,7 @@ describe('collapsedGroupedDatePicker', () => { }); describe('minDate', () => { - beforeEach((done) => { + beforeEach(done => { vm.minDate = new Date('07/17/2016'); Vue.nextTick(done); }); @@ -56,7 +56,7 @@ describe('collapsedGroupedDatePicker', () => { }); describe('maxDate', () => { - beforeEach((done) => { + beforeEach(done => { vm.maxDate = new Date('07/17/2017'); Vue.nextTick(done); }); diff --git a/spec/javascripts/vue_shared/components/sidebar/date_picker_spec.js b/spec/javascripts/vue_shared/components/sidebar/date_picker_spec.js index 1581f4e3eb1..805ba7b9947 100644 --- a/spec/javascripts/vue_shared/components/sidebar/date_picker_spec.js +++ b/spec/javascripts/vue_shared/components/sidebar/date_picker_spec.js @@ -41,7 +41,7 @@ describe('sidebarDatePicker', () => { expect(vm.$el.querySelector('.value-content span').innerText.trim()).toEqual('None'); }); - it('should render date-picker when editing', (done) => { + it('should render date-picker when editing', done => { vm.editing = true; Vue.nextTick(() => { expect(vm.$el.querySelector('.pika-label')).toBeDefined(); @@ -50,7 +50,7 @@ describe('sidebarDatePicker', () => { }); describe('editable', () => { - beforeEach((done) => { + beforeEach(done => { vm.editable = true; Vue.nextTick(done); }); @@ -59,7 +59,7 @@ describe('sidebarDatePicker', () => { expect(vm.$el.querySelector('.title .btn-blank').innerText.trim()).toEqual('Edit'); }); - it('should enable editing when edit button is clicked', (done) => { + it('should enable editing when edit button is clicked', done => { vm.isLoading = false; Vue.nextTick(() => { vm.$el.querySelector('.title .btn-blank').click(); @@ -70,7 +70,7 @@ describe('sidebarDatePicker', () => { }); }); - it('should render date if selectedDate', (done) => { + it('should render date if selectedDate', done => { vm.selectedDate = new Date('07/07/2017'); Vue.nextTick(() => { expect(vm.$el.querySelector('.value-content strong').innerText.trim()).toEqual('Jul 7, 2017'); @@ -79,7 +79,7 @@ describe('sidebarDatePicker', () => { }); describe('selectedDate and editable', () => { - beforeEach((done) => { + beforeEach(done => { vm.selectedDate = new Date('07/07/2017'); vm.editable = true; Vue.nextTick(done); @@ -100,7 +100,7 @@ describe('sidebarDatePicker', () => { }); describe('showToggleSidebar', () => { - beforeEach((done) => { + beforeEach(done => { vm.showToggleSidebar = true; Vue.nextTick(done); }); diff --git a/spec/javascripts/vue_shared/components/sidebar/labels_select/dropdown_value_collapsed_spec.js b/spec/javascripts/vue_shared/components/sidebar/labels_select/dropdown_value_collapsed_spec.js index 9a691116cf8..804b33422bd 100644 --- a/spec/javascripts/vue_shared/components/sidebar/labels_select/dropdown_value_collapsed_spec.js +++ b/spec/javascripts/vue_shared/components/sidebar/labels_select/dropdown_value_collapsed_spec.js @@ -49,7 +49,9 @@ describe('DropdownValueCollapsedComponent', () => { const vmMoreLabels = createComponent(mockMoreLabels); - expect(vmMoreLabels.labelsList).toBe('Foo Label, Foo Label, Foo Label, Foo Label, Foo Label, and 2 more'); + expect(vmMoreLabels.labelsList).toBe( + 'Foo Label, Foo Label, Foo Label, Foo Label, Foo Label, and 2 more', + ); vmMoreLabels.$destroy(); }); diff --git a/spec/javascripts/vue_shared/components/smart_virtual_list_spec.js b/spec/javascripts/vue_shared/components/smart_virtual_list_spec.js new file mode 100644 index 00000000000..e723fead65e --- /dev/null +++ b/spec/javascripts/vue_shared/components/smart_virtual_list_spec.js @@ -0,0 +1,83 @@ +import Vue from 'vue'; +import SmartVirtualScrollList from '~/vue_shared/components/smart_virtual_list.vue'; +import mountComponent from 'spec/helpers/vue_mount_component_helper'; + +describe('Toggle Button', () => { + let vm; + + const createComponent = ({ length, remain }) => { + const smartListProperties = { + rtag: 'section', + wtag: 'ul', + wclass: 'test-class', + // Size in pixels does not matter for our tests here + size: 35, + length, + remain, + }; + + const Component = Vue.extend({ + components: { + SmartVirtualScrollList, + }, + smartListProperties, + items: Array(length).fill(1), + template: ` + <smart-virtual-scroll-list v-bind="$options.smartListProperties"> + <li v-for="(val, key) in $options.items" :key="key">{{ key + 1 }}</li> + </smart-virtual-scroll-list>`, + }); + + return mountComponent(Component); + }; + + afterEach(() => { + vm.$destroy(); + }); + + describe('if the list is shorter than the maximum shown elements', () => { + const listLength = 10; + + beforeEach(() => { + vm = createComponent({ length: listLength, remain: 20 }); + }); + + it('renders without the vue-virtual-scroll-list component', () => { + expect(vm.$el.classList).not.toContain('js-virtual-list'); + expect(vm.$el.classList).toContain('js-plain-element'); + }); + + it('renders list with provided tags and classes for the wrapper elements', () => { + expect(vm.$el.tagName).toEqual('SECTION'); + expect(vm.$el.firstChild.tagName).toEqual('UL'); + expect(vm.$el.firstChild.classList).toContain('test-class'); + }); + + it('renders all children list elements', () => { + expect(vm.$el.querySelectorAll('li').length).toEqual(listLength); + }); + }); + + describe('if the list is longer than the maximum shown elements', () => { + const maxItemsShown = 20; + + beforeEach(() => { + vm = createComponent({ length: 1000, remain: maxItemsShown }); + }); + + it('uses the vue-virtual-scroll-list component', () => { + expect(vm.$el.classList).toContain('js-virtual-list'); + expect(vm.$el.classList).not.toContain('js-plain-element'); + }); + + it('renders list with provided tags and classes for the wrapper elements', () => { + expect(vm.$el.tagName).toEqual('SECTION'); + expect(vm.$el.firstChild.tagName).toEqual('UL'); + expect(vm.$el.firstChild.classList).toContain('test-class'); + }); + + it('renders at max twice the maximum shown elements', () => { + expect(vm.$el.querySelectorAll('li').length).toBeLessThanOrEqual(2 * maxItemsShown); + }); + }); +}); diff --git a/spec/javascripts/vue_shared/components/user_avatar/user_avatar_link_spec.js b/spec/javascripts/vue_shared/components/user_avatar/user_avatar_link_spec.js index 50b8d49d4bd..e022245d3ea 100644 --- a/spec/javascripts/vue_shared/components/user_avatar/user_avatar_link_spec.js +++ b/spec/javascripts/vue_shared/components/user_avatar/user_avatar_link_spec.js @@ -44,7 +44,7 @@ describe('User Avatar Link Component', function() { expect(this.userAvatarLink.$el.querySelector('img')).not.toBeNull(); }); - it('should return neccessary props as defined', function() { + it('should return necessary props as defined', function() { _.each(this.propsData, (val, key) => { expect(this.userAvatarLink[key]).toBeDefined(); }); diff --git a/spec/lib/banzai/filter/autolink_filter_spec.rb b/spec/lib/banzai/filter/autolink_filter_spec.rb index a50329473ad..7a457403b51 100644 --- a/spec/lib/banzai/filter/autolink_filter_spec.rb +++ b/spec/lib/banzai/filter/autolink_filter_spec.rb @@ -76,7 +76,7 @@ describe Banzai::Filter::AutolinkFilter do expect(doc.at_css('a')['href']).to eq link end - it 'autolinks multiple occurences of smb' do + it 'autolinks multiple occurrences of smb' do link1 = 'smb:///Volumes/shared/foo.pdf' link2 = 'smb:///Volumes/shared/bar.pdf' diff --git a/spec/lib/banzai/filter/relative_link_filter_spec.rb b/spec/lib/banzai/filter/relative_link_filter_spec.rb index ed1ebe9ebf6..415ded05e6e 100644 --- a/spec/lib/banzai/filter/relative_link_filter_spec.rb +++ b/spec/lib/banzai/filter/relative_link_filter_spec.rb @@ -226,7 +226,7 @@ describe Banzai::Filter::RelativeLinkFilter do let(:ref) {'mark#\'@],+;-._/#@!$&()+down'} it 'correctly escapes the ref' do - # Adressable won't escape the '#', so we do this manually + # Addressable won't escape the '#', so we do this manually ref_escaped = 'mark%23\'@%5D,+;-._/%23@!$&()+down' # Stub this method so the branch doesn't actually need to be in the repo diff --git a/spec/lib/container_registry/blob_spec.rb b/spec/lib/container_registry/blob_spec.rb index c73faa55513..d3fff5bad42 100644 --- a/spec/lib/container_registry/blob_spec.rb +++ b/spec/lib/container_registry/blob_spec.rb @@ -64,7 +64,7 @@ describe ContainerRegistry::Blob do .to_return(status: 200) end - it 'returns true when blob has been successfuly deleted' do + it 'returns true when blob has been successfully deleted' do expect(blob.delete).to be_truthy end end diff --git a/spec/lib/gitlab/background_migration/create_fork_network_memberships_range_spec.rb b/spec/lib/gitlab/background_migration/create_fork_network_memberships_range_spec.rb index e1c4f9cfea7..5076996474f 100644 --- a/spec/lib/gitlab/background_migration/create_fork_network_memberships_range_spec.rb +++ b/spec/lib/gitlab/background_migration/create_fork_network_memberships_range_spec.rb @@ -118,7 +118,7 @@ describe Gitlab::BackgroundMigration::CreateForkNetworkMembershipsRange, :migrat expect(fork_network_members.count).to eq(12) end - it 'knows when not all memberships withing a batch have been created' do + it 'knows when not all memberships within a batch have been created' do expect(migration.missing_members?(8, 10)).to be_truthy end end diff --git a/spec/lib/gitlab/background_migration/populate_cluster_kubernetes_namespace_table_spec.rb b/spec/lib/gitlab/background_migration/populate_cluster_kubernetes_namespace_table_spec.rb new file mode 100644 index 00000000000..4f1b01eed41 --- /dev/null +++ b/spec/lib/gitlab/background_migration/populate_cluster_kubernetes_namespace_table_spec.rb @@ -0,0 +1,97 @@ +# frozen_string_literal: true + +require 'spec_helper' + +describe Gitlab::BackgroundMigration::PopulateClusterKubernetesNamespaceTable, :migration, schema: 20181022173835 do + let(:migration) { described_class.new } + let(:clusters) { create_list(:cluster, 10, :project, :provided_by_gcp) } + + before do + clusters + end + + shared_examples 'consistent kubernetes namespace attributes' do + it 'should populate namespace and service account information' do + subject + + clusters_with_namespace.each do |cluster| + project = cluster.project + cluster_project = cluster.cluster_projects.first + namespace = "#{project.path}-#{project.id}" + kubernetes_namespace = cluster.reload.kubernetes_namespace + + expect(kubernetes_namespace).to be_present + expect(kubernetes_namespace.cluster_project).to eq(cluster_project) + expect(kubernetes_namespace.project).to eq(cluster_project.project) + expect(kubernetes_namespace.cluster).to eq(cluster_project.cluster) + expect(kubernetes_namespace.namespace).to eq(namespace) + expect(kubernetes_namespace.service_account_name).to eq("#{namespace}-service-account") + end + end + end + + subject { migration.perform } + + context 'when no Clusters::Project has a Clusters::KubernetesNamespace' do + let(:cluster_projects) { Clusters::Project.all } + + it 'should create a Clusters::KubernetesNamespace per Clusters::Project' do + expect do + subject + end.to change(Clusters::KubernetesNamespace, :count).by(cluster_projects.count) + end + + it_behaves_like 'consistent kubernetes namespace attributes' do + let(:clusters_with_namespace) { clusters } + end + end + + context 'when every Clusters::Project has Clusters::KubernetesNamespace' do + before do + clusters.each do |cluster| + create(:cluster_kubernetes_namespace, + cluster_project: cluster.cluster_projects.first, + cluster: cluster, + project: cluster.project) + end + end + + it 'should not create any Clusters::KubernetesNamespace' do + expect do + subject + end.not_to change(Clusters::KubernetesNamespace, :count) + end + end + + context 'when only some Clusters::Project have Clusters::KubernetesNamespace related' do + let(:with_kubernetes_namespace) { clusters.first(6) } + let(:with_no_kubernetes_namespace) { clusters.last(4) } + + before do + with_kubernetes_namespace.each do |cluster| + create(:cluster_kubernetes_namespace, + cluster_project: cluster.cluster_projects.first, + cluster: cluster, + project: cluster.project) + end + end + + it 'creates limited number of Clusters::KubernetesNamespace' do + expect do + subject + end.to change(Clusters::KubernetesNamespace, :count).by(with_no_kubernetes_namespace.count) + end + + it 'should not modify clusters with Clusters::KubernetesNamespace' do + subject + + with_kubernetes_namespace.each do |cluster| + expect(cluster.kubernetes_namespaces.count).to eq(1) + end + end + + it_behaves_like 'consistent kubernetes namespace attributes' do + let(:clusters_with_namespace) { with_no_kubernetes_namespace } + end + end +end diff --git a/spec/lib/gitlab/checks/lfs_integrity_spec.rb b/spec/lib/gitlab/checks/lfs_integrity_spec.rb index 0488720cec8..887ea8fc1e0 100644 --- a/spec/lib/gitlab/checks/lfs_integrity_spec.rb +++ b/spec/lib/gitlab/checks/lfs_integrity_spec.rb @@ -68,7 +68,7 @@ describe Gitlab::Checks::LfsIntegrity do expect(subject.objects_missing?).to be_truthy end - it 'is false parent project already conatins LFS objects for the fork' do + it 'is false parent project already contains LFS objects for the fork' do lfs_object = create(:lfs_object, oid: blob_object.lfs_oid) create(:lfs_objects_project, project: parent_project, lfs_object: lfs_object) diff --git a/spec/lib/gitlab/ci/ansi2html_spec.rb b/spec/lib/gitlab/ci/ansi2html_spec.rb index 7549e9941b6..5a5c071c639 100644 --- a/spec/lib/gitlab/ci/ansi2html_spec.rb +++ b/spec/lib/gitlab/ci/ansi2html_spec.rb @@ -7,7 +7,7 @@ describe Gitlab::Ci::Ansi2html do expect(convert_html("Hello")).to eq('Hello') end - it "strips non-color-changing controll sequences" do + it "strips non-color-changing control sequences" do expect(convert_html("Hello \e[2Kworld")).to eq('Hello world') end diff --git a/spec/lib/gitlab/ci/build/policy/variables_spec.rb b/spec/lib/gitlab/ci/build/policy/variables_spec.rb index 854c4cb718c..c2c0742efc3 100644 --- a/spec/lib/gitlab/ci/build/policy/variables_spec.rb +++ b/spec/lib/gitlab/ci/build/policy/variables_spec.rb @@ -24,7 +24,7 @@ describe Gitlab::Ci::Build::Policy::Variables do expect(policy).to be_satisfied_by(pipeline, seed) end - it 'is not satisfied by an overriden empty variable' do + it 'is not satisfied by an overridden empty variable' do policy = described_class.new(['$CI_PROJECT_NAME']) expect(policy).not_to be_satisfied_by(pipeline, seed) diff --git a/spec/lib/gitlab/ci/config/entry/global_spec.rb b/spec/lib/gitlab/ci/config/entry/global_spec.rb index 1860ed79bfd..7c18514934e 100644 --- a/spec/lib/gitlab/ci/config/entry/global_spec.rb +++ b/spec/lib/gitlab/ci/config/entry/global_spec.rb @@ -219,7 +219,7 @@ describe Gitlab::Ci::Config::Entry::Global do ## # When nodes are specified but not defined, we assume that - # configuration is valid, and we asume that entry is simply undefined, + # configuration is valid, and we assume that entry is simply undefined, # despite the fact, that key is present. See issue #18775 for more # details. # diff --git a/spec/lib/gitlab/ci/config/entry/reports_spec.rb b/spec/lib/gitlab/ci/config/entry/reports_spec.rb index 8095a231cf3..1140bfdf6c3 100644 --- a/spec/lib/gitlab/ci/config/entry/reports_spec.rb +++ b/spec/lib/gitlab/ci/config/entry/reports_spec.rb @@ -33,7 +33,7 @@ describe Gitlab::Ci::Config::Entry::Reports do where(:keyword, :file) do :junit | 'junit.xml' - :codequality | 'codequality.json' + :codequality | 'gl-code-quality-report.json' :sast | 'gl-sast-report.json' :dependency_scanning | 'gl-dependency-scanning-report.json' :container_scanning | 'gl-container-scanning-report.json' diff --git a/spec/lib/gitlab/cross_project_access/check_info_spec.rb b/spec/lib/gitlab/cross_project_access/check_info_spec.rb index bc9dbf2bece..239fa364f5e 100644 --- a/spec/lib/gitlab/cross_project_access/check_info_spec.rb +++ b/spec/lib/gitlab/cross_project_access/check_info_spec.rb @@ -50,7 +50,7 @@ describe Gitlab::CrossProjectAccess::CheckInfo do expect(info.should_run?(dummy_controller)).to be_truthy end - it 'returns the the oposite of #should_skip? when the check is a skip' do + it 'returns the the opposite of #should_skip? when the check is a skip' do info = described_class.new({}, nil, nil, true) expect(info).to receive(:should_skip?).with(dummy_controller).and_return(false) @@ -101,7 +101,7 @@ describe Gitlab::CrossProjectAccess::CheckInfo do expect(info.should_skip?(dummy_controller)).to be_truthy end - it 'returns the the oposite of #should_run? when the check is not a skip' do + it 'returns the the opposite of #should_run? when the check is not a skip' do info = described_class.new({}, nil, nil, false) expect(info).to receive(:should_run?).with(dummy_controller).and_return(false) diff --git a/spec/lib/gitlab/cycle_analytics/stage_summary_spec.rb b/spec/lib/gitlab/cycle_analytics/stage_summary_spec.rb index 2e67c1c7f78..f8009709ce2 100644 --- a/spec/lib/gitlab/cycle_analytics/stage_summary_spec.rb +++ b/spec/lib/gitlab/cycle_analytics/stage_summary_spec.rb @@ -44,15 +44,15 @@ describe Gitlab::CycleAnalytics::StageSummary do describe "#deploys" do it "finds the number of deploys made created after the 'from date'" do - Timecop.freeze(5.days.ago) { create(:deployment, project: project) } - Timecop.freeze(5.days.from_now) { create(:deployment, project: project) } + Timecop.freeze(5.days.ago) { create(:deployment, :success, project: project) } + Timecop.freeze(5.days.from_now) { create(:deployment, :success, project: project) } expect(subject.third[:value]).to eq(1) end it "doesn't find commits from other projects" do Timecop.freeze(5.days.from_now) do - create(:deployment, project: create(:project, :repository)) + create(:deployment, :success, project: create(:project, :repository)) end expect(subject.third[:value]).to eq(0) diff --git a/spec/lib/gitlab/database/rename_reserved_paths_migration/v1/rename_base_spec.rb b/spec/lib/gitlab/database/rename_reserved_paths_migration/v1/rename_base_spec.rb index cc7cb3f23fd..248cca25a2c 100644 --- a/spec/lib/gitlab/database/rename_reserved_paths_migration/v1/rename_base_spec.rb +++ b/spec/lib/gitlab/database/rename_reserved_paths_migration/v1/rename_base_spec.rb @@ -20,7 +20,7 @@ describe Gitlab::Database::RenameReservedPathsMigration::V1::RenameBase, :delete end describe "#remove_last_ocurrence" do - it "removes only the last occurance of a string" do + it "removes only the last occurrence of a string" do input = "this/is/a-word-to-replace/namespace/with/a-word-to-replace" expect(subject.remove_last_occurrence(input, "a-word-to-replace")) diff --git a/spec/lib/gitlab/diff/position_spec.rb b/spec/lib/gitlab/diff/position_spec.rb index 2d94356f386..cc4faf6f10b 100644 --- a/spec/lib/gitlab/diff/position_spec.rb +++ b/spec/lib/gitlab/diff/position_spec.rb @@ -566,13 +566,13 @@ describe Gitlab::Diff::Position do end end - context "for text positon" do + context "for text position" do let(:args) { args_for_text } it_behaves_like "diff position json" end - context "for image positon" do + context "for image position" do let(:args) { args_for_img } it_behaves_like "diff position json" @@ -592,13 +592,13 @@ describe Gitlab::Diff::Position do end end - context "for text positon" do + context "for text position" do let(:args) { args_for_text } it_behaves_like "diff position json" end - context "for image positon" do + context "for image position" do let(:args) { args_for_img } it_behaves_like "diff position json" diff --git a/spec/lib/gitlab/diff/position_tracer_spec.rb b/spec/lib/gitlab/diff/position_tracer_spec.rb index ddc4f6c5b5c..a2eed07ca55 100644 --- a/spec/lib/gitlab/diff/position_tracer_spec.rb +++ b/spec/lib/gitlab/diff/position_tracer_spec.rb @@ -43,7 +43,7 @@ describe Gitlab::Diff::PositionTracer do # # In any case, all of this means that the tests below will be extremely # (excessively, unjustifiably) thorough for scenarios where "the file was - # created in the old diff" and then drop off to comparitively lackluster + # created in the old diff" and then drop off to comparatively lackluster # testing of other scenarios. # # I did still try to cover most of the obvious and potentially tricky diff --git a/spec/lib/gitlab/file_detector_spec.rb b/spec/lib/gitlab/file_detector_spec.rb index 294ec2c2fd6..edab53247e9 100644 --- a/spec/lib/gitlab/file_detector_spec.rb +++ b/spec/lib/gitlab/file_detector_spec.rb @@ -15,7 +15,12 @@ describe Gitlab::FileDetector do describe '.type_of' do it 'returns the type of a README file' do - expect(described_class.type_of('README.md')).to eq(:readme) + %w[README readme INDEX index].each do |filename| + expect(described_class.type_of(filename)).to eq(:readme) + %w[.md .adoc .rst].each do |extname| + expect(described_class.type_of(filename + extname)).to eq(:readme) + end + end end it 'returns nil for a README file in a directory' do diff --git a/spec/lib/gitlab/git/attributes_at_ref_parser_spec.rb b/spec/lib/gitlab/git/attributes_at_ref_parser_spec.rb index ca067a29174..134bd5657e7 100644 --- a/spec/lib/gitlab/git/attributes_at_ref_parser_spec.rb +++ b/spec/lib/gitlab/git/attributes_at_ref_parser_spec.rb @@ -17,7 +17,7 @@ describe Gitlab::Git::AttributesAtRefParser, :seed_helper do end it 'handles missing blobs' do - expect { described_class.new(repository, 'non-existant-branch') }.not_to raise_error + expect { described_class.new(repository, 'non-existent-branch') }.not_to raise_error end describe '#attributes' do diff --git a/spec/lib/gitlab/git/repository_spec.rb b/spec/lib/gitlab/git/repository_spec.rb index 9a443fa7f20..54291e847d8 100644 --- a/spec/lib/gitlab/git/repository_spec.rb +++ b/spec/lib/gitlab/git/repository_spec.rb @@ -476,6 +476,27 @@ describe Gitlab::Git::Repository, :seed_helper do end end + describe '#fetch_remote' do + it 'delegates to the gitaly RepositoryService' do + ssh_auth = double(:ssh_auth) + expected_opts = { + ssh_auth: ssh_auth, + forced: true, + no_tags: true, + timeout: described_class::GITLAB_PROJECTS_TIMEOUT, + prune: false + } + + expect(repository.gitaly_repository_client).to receive(:fetch_remote).with('remote-name', expected_opts) + + repository.fetch_remote('remote-name', ssh_auth: ssh_auth, forced: true, no_tags: true, prune: false) + end + + it_behaves_like 'wrapping gRPC errors', Gitlab::GitalyClient::RepositoryService, :fetch_remote do + subject { repository.fetch_remote('remote-name') } + end + end + describe '#find_remote_root_ref' do it 'gets the remote root ref from GitalyClient' do expect_any_instance_of(Gitlab::GitalyClient::RemoteService) diff --git a/spec/lib/gitlab/gitaly_client/repository_service_spec.rb b/spec/lib/gitlab/gitaly_client/repository_service_spec.rb index 1547d447197..d605fcbafee 100644 --- a/spec/lib/gitlab/gitaly_client/repository_service_spec.rb +++ b/spec/lib/gitlab/gitaly_client/repository_service_spec.rb @@ -1,6 +1,8 @@ require 'spec_helper' describe Gitlab::GitalyClient::RepositoryService do + using RSpec::Parameterized::TableSyntax + let(:project) { create(:project) } let(:storage_name) { project.repository_storage } let(:relative_path) { project.disk_path + '.git' } @@ -107,16 +109,67 @@ describe Gitlab::GitalyClient::RepositoryService do end describe '#fetch_remote' do - let(:ssh_auth) { double(:ssh_auth, ssh_import?: true, ssh_key_auth?: false, ssh_known_hosts: nil) } - let(:import_url) { 'ssh://example.com' } + let(:remote) { 'remote-name' } it 'sends a fetch_remote_request message' do + expected_request = gitaly_request_with_params( + remote: remote, + ssh_key: '', + known_hosts: '', + force: false, + no_tags: false, + no_prune: false + ) + expect_any_instance_of(Gitaly::RepositoryService::Stub) .to receive(:fetch_remote) - .with(gitaly_request_with_params(no_prune: false), kind_of(Hash)) + .with(expected_request, kind_of(Hash)) .and_return(double(value: true)) - client.fetch_remote(import_url, ssh_auth: ssh_auth, forced: false, no_tags: false, timeout: 60) + client.fetch_remote(remote, ssh_auth: nil, forced: false, no_tags: false, timeout: 1) + end + + context 'SSH auth' do + where(:ssh_import, :ssh_key_auth, :ssh_private_key, :ssh_known_hosts, :expected_params) do + false | false | 'key' | 'known_hosts' | {} + false | true | 'key' | 'known_hosts' | {} + true | false | 'key' | 'known_hosts' | { known_hosts: 'known_hosts' } + true | true | 'key' | 'known_hosts' | { ssh_key: 'key', known_hosts: 'known_hosts' } + true | true | 'key' | nil | { ssh_key: 'key' } + true | true | nil | 'known_hosts' | { known_hosts: 'known_hosts' } + true | true | nil | nil | {} + true | true | '' | '' | {} + end + + with_them do + let(:ssh_auth) do + double( + :ssh_auth, + ssh_import?: ssh_import, + ssh_key_auth?: ssh_key_auth, + ssh_private_key: ssh_private_key, + ssh_known_hosts: ssh_known_hosts + ) + end + + it do + expected_request = gitaly_request_with_params({ + remote: remote, + ssh_key: '', + known_hosts: '', + force: false, + no_tags: false, + no_prune: false + }.update(expected_params)) + + expect_any_instance_of(Gitaly::RepositoryService::Stub) + .to receive(:fetch_remote) + .with(expected_request, kind_of(Hash)) + .and_return(double(value: true)) + + client.fetch_remote(remote, ssh_auth: ssh_auth, forced: false, no_tags: false, timeout: 1) + end + end end end diff --git a/spec/lib/gitlab/gpg_spec.rb b/spec/lib/gitlab/gpg_spec.rb index 47f37cae98f..39d09c49989 100644 --- a/spec/lib/gitlab/gpg_spec.rb +++ b/spec/lib/gitlab/gpg_spec.rb @@ -96,7 +96,7 @@ describe Gitlab::Gpg do expect(described_class.current_home_dir).to eq default_home_dir end - it 'returns the explicitely set home dir' do + it 'returns the explicitly set home dir' do GPGME::Engine.home_dir = '/tmp/gpg' expect(described_class.current_home_dir).to eq '/tmp/gpg' @@ -104,7 +104,7 @@ describe Gitlab::Gpg do GPGME::Engine.home_dir = GPGME::Engine.dirinfo('homedir') end - it 'returns the default value when explicitely setting the home dir to nil' do + it 'returns the default value when explicitly setting the home dir to nil' do GPGME::Engine.home_dir = nil expect(described_class.current_home_dir).to eq default_home_dir diff --git a/spec/lib/gitlab/import_export/all_models.yml b/spec/lib/gitlab/import_export/all_models.yml index a63f34b5536..f4efa450cca 100644 --- a/spec/lib/gitlab/import_export/all_models.yml +++ b/spec/lib/gitlab/import_export/all_models.yml @@ -299,6 +299,7 @@ project: - ci_cd_settings - import_export_upload - repository_languages +- pool_repository award_emoji: - awardable - user 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 b0570680d5a..365bfae0d88 100644 --- a/spec/lib/gitlab/import_export/project_tree_restorer_spec.rb +++ b/spec/lib/gitlab/import_export/project_tree_restorer_spec.rb @@ -321,7 +321,7 @@ describe Gitlab::ImportExport::ProjectTreeRestorer do end end - context 'when the project has overriden params in import data' do + context 'when the project has overridden params in import data' do it 'overwrites the params stored in the JSON' do project.create_import_data(data: { override_params: { description: "Overridden" } }) diff --git a/spec/lib/gitlab/kubernetes/helm/pod_spec.rb b/spec/lib/gitlab/kubernetes/helm/pod_spec.rb index b333b334f36..c92bc92c42d 100644 --- a/spec/lib/gitlab/kubernetes/helm/pod_spec.rb +++ b/spec/lib/gitlab/kubernetes/helm/pod_spec.rb @@ -30,7 +30,7 @@ describe Gitlab::Kubernetes::Helm::Pod do it 'should generate the appropriate specifications for the container' do container = subject.generate.spec.containers.first expect(container.name).to eq('helm') - expect(container.image).to eq('alpine:3.6') + expect(container.image).to eq('registry.gitlab.com/gitlab-org/cluster-integration/helm-install-image/releases/2.7.2-kube-1.11.0') expect(container.env.count).to eq(3) expect(container.env.map(&:name)).to match_array([:HELM_VERSION, :TILLER_NAMESPACE, :COMMAND_SCRIPT]) expect(container.command).to match_array(["/bin/sh"]) diff --git a/spec/lib/gitlab/kubernetes/role_binding_spec.rb b/spec/lib/gitlab/kubernetes/role_binding_spec.rb index da3f5d27b25..a1a59533bfb 100644 --- a/spec/lib/gitlab/kubernetes/role_binding_spec.rb +++ b/spec/lib/gitlab/kubernetes/role_binding_spec.rb @@ -20,7 +20,7 @@ describe Gitlab::Kubernetes::RoleBinding, '#generate' do let(:role_ref) do { apiGroup: 'rbac.authorization.k8s.io', - kind: 'Role', + kind: 'ClusterRole', name: role_name } end @@ -35,6 +35,7 @@ describe Gitlab::Kubernetes::RoleBinding, '#generate' do subject do described_class.new( + name: "gitlab-#{namespace}", role_name: role_name, namespace: namespace, service_account_name: service_account_name diff --git a/spec/lib/gitlab/shell_spec.rb b/spec/lib/gitlab/shell_spec.rb index b1b7c427313..6ce9d515a0f 100644 --- a/spec/lib/gitlab/shell_spec.rb +++ b/spec/lib/gitlab/shell_spec.rb @@ -498,126 +498,6 @@ describe Gitlab::Shell do end end - describe '#fetch_remote' do - def fetch_remote(ssh_auth = nil, prune = true) - gitlab_shell.fetch_remote(repository.raw_repository, 'remote-name', ssh_auth: ssh_auth, prune: prune) - end - - def expect_call(fail, options = {}) - receive_fetch_remote = - if fail - receive(:fetch_remote).and_raise(GRPC::NotFound) - else - receive(:fetch_remote).and_return(true) - end - - expect_any_instance_of(Gitlab::GitalyClient::RepositoryService).to receive_fetch_remote - end - - def build_ssh_auth(opts = {}) - defaults = { - ssh_import?: true, - ssh_key_auth?: false, - ssh_known_hosts: nil, - ssh_private_key: nil - } - - double(:ssh_auth, defaults.merge(opts)) - end - - it 'returns true when the command succeeds' do - expect_call(false, force: false, tags: true, prune: true) - - expect(fetch_remote).to be_truthy - end - - it 'returns true when the command succeeds' do - expect_call(false, force: false, tags: true, prune: false) - - expect(fetch_remote(nil, false)).to be_truthy - end - - it 'raises an exception when the command fails' do - expect_call(true, force: false, tags: true, prune: true) - - expect { fetch_remote }.to raise_error(Gitlab::Shell::Error) - end - - it 'allows forced and no_tags to be changed' do - expect_call(false, force: true, tags: false, prune: true) - - result = gitlab_shell.fetch_remote(repository.raw_repository, 'remote-name', forced: true, no_tags: true, prune: true) - expect(result).to be_truthy - end - - context 'SSH auth' do - it 'passes the SSH key if specified' do - expect_call(false, force: false, tags: true, prune: true, ssh_key: 'foo') - - ssh_auth = build_ssh_auth(ssh_key_auth?: true, ssh_private_key: 'foo') - - expect(fetch_remote(ssh_auth)).to be_truthy - end - - it 'does not pass an empty SSH key' do - expect_call(false, force: false, tags: true, prune: true) - - ssh_auth = build_ssh_auth(ssh_key_auth: true, ssh_private_key: '') - - expect(fetch_remote(ssh_auth)).to be_truthy - end - - it 'does not pass the key unless SSH key auth is to be used' do - expect_call(false, force: false, tags: true, prune: true) - - ssh_auth = build_ssh_auth(ssh_key_auth: false, ssh_private_key: 'foo') - - expect(fetch_remote(ssh_auth)).to be_truthy - end - - it 'passes the known_hosts data if specified' do - expect_call(false, force: false, tags: true, prune: true, known_hosts: 'foo') - - ssh_auth = build_ssh_auth(ssh_known_hosts: 'foo') - - expect(fetch_remote(ssh_auth)).to be_truthy - end - - it 'does not pass empty known_hosts data' do - expect_call(false, force: false, tags: true, prune: true) - - ssh_auth = build_ssh_auth(ssh_known_hosts: '') - - expect(fetch_remote(ssh_auth)).to be_truthy - end - - it 'does not pass known_hosts data unless SSH is to be used' do - expect_call(false, force: false, tags: true, prune: true) - - ssh_auth = build_ssh_auth(ssh_import?: false, ssh_known_hosts: 'foo') - - expect(fetch_remote(ssh_auth)).to be_truthy - end - end - - context 'gitaly call' do - let(:remote_name) { 'remote-name' } - let(:ssh_auth) { double(:ssh_auth) } - - subject do - gitlab_shell.fetch_remote(repository.raw_repository, remote_name, - forced: true, no_tags: true, ssh_auth: ssh_auth) - end - - it 'passes the correct params to the gitaly service' do - expect(repository.gitaly_repository_client).to receive(:fetch_remote) - .with(remote_name, ssh_auth: ssh_auth, forced: true, no_tags: true, prune: true, timeout: timeout) - - subject - end - end - end - describe '#import_repository' do let(:import_url) { 'https://gitlab.com/gitlab-org/gitlab-ce.git' } diff --git a/spec/lib/gitlab/slash_commands/command_spec.rb b/spec/lib/gitlab/slash_commands/command_spec.rb index 194cae8c645..eceacac58af 100644 --- a/spec/lib/gitlab/slash_commands/command_spec.rb +++ b/spec/lib/gitlab/slash_commands/command_spec.rb @@ -44,7 +44,7 @@ describe Gitlab::SlashCommands::Command do let!(:build) { create(:ci_build, pipeline: pipeline) } let!(:pipeline) { create(:ci_pipeline, project: project) } let!(:staging) { create(:environment, name: 'staging', project: project) } - let!(:deployment) { create(:deployment, environment: staging, deployable: build) } + let!(:deployment) { create(:deployment, :success, environment: staging, deployable: build) } let!(:manual) do create(:ci_build, :manual, pipeline: pipeline, diff --git a/spec/lib/gitlab/slash_commands/deploy_spec.rb b/spec/lib/gitlab/slash_commands/deploy_spec.rb index 0d57334aa4c..25f3e8a0409 100644 --- a/spec/lib/gitlab/slash_commands/deploy_spec.rb +++ b/spec/lib/gitlab/slash_commands/deploy_spec.rb @@ -31,7 +31,7 @@ describe Gitlab::SlashCommands::Deploy do let!(:staging) { create(:environment, name: 'staging', project: project) } let!(:pipeline) { create(:ci_pipeline, project: project) } let!(:build) { create(:ci_build, pipeline: pipeline) } - let!(:deployment) { create(:deployment, environment: staging, deployable: build) } + let!(:deployment) { create(:deployment, :success, environment: staging, deployable: build) } context 'without actions' do it 'does not execute an action' do diff --git a/spec/lib/gitlab/view/presenter/base_spec.rb b/spec/lib/gitlab/view/presenter/base_spec.rb index 4eca53032a2..02c2fd47197 100644 --- a/spec/lib/gitlab/view/presenter/base_spec.rb +++ b/spec/lib/gitlab/view/presenter/base_spec.rb @@ -40,7 +40,7 @@ describe Gitlab::View::Presenter::Base do end end - context 'subject is overriden' do + context 'subject is overridden' do it 'returns true' do presenter = presenter_class.new(build_stubbed(:project, :public)) diff --git a/spec/lib/microsoft_teams/notifier_spec.rb b/spec/lib/microsoft_teams/notifier_spec.rb index c9756544bd6..2aaa7c24ad8 100644 --- a/spec/lib/microsoft_teams/notifier_spec.rb +++ b/spec/lib/microsoft_teams/notifier_spec.rb @@ -48,7 +48,7 @@ describe MicrosoftTeams::Notifier do stub_request(:post, webhook_url).with(body: JSON(body), headers: { 'Content-Type' => 'application/json' }).to_return(status: 200, body: "", headers: {}) end - it 'expects to receive successfull answer' do + it 'expects to receive successful answer' do expect(subject.ping(options)).to be true end end diff --git a/spec/migrations/delete_inconsistent_internal_id_records_spec.rb b/spec/migrations/delete_inconsistent_internal_id_records_spec.rb index becb71cf427..4af51217031 100644 --- a/spec/migrations/delete_inconsistent_internal_id_records_spec.rb +++ b/spec/migrations/delete_inconsistent_internal_id_records_spec.rb @@ -65,6 +65,21 @@ describe DeleteInconsistentInternalIdRecords, :migration do context 'for deployments' do let(:scope) { :deployment } + let(:deployments) { table(:deployments) } + let(:internal_ids) { table(:internal_ids) } + + before do + internal_ids.create!(project_id: project1.id, usage: 2, last_value: 2) + internal_ids.create!(project_id: project2.id, usage: 2, last_value: 2) + internal_ids.create!(project_id: project3.id, usage: 2, last_value: 2) + end + + let(:create_models) do + 3.times { |i| deployments.create!(project_id: project1.id, iid: i, environment_id: 1, ref: 'master', sha: 'a', tag: false) } + 3.times { |i| deployments.create!(project_id: project2.id, iid: i, environment_id: 1, ref: 'master', sha: 'a', tag: false) } + 3.times { |i| deployments.create!(project_id: project3.id, iid: i, environment_id: 1, ref: 'master', sha: 'a', tag: false) } + end + it_behaves_like 'deleting inconsistent internal_id records' end diff --git a/spec/migrations/fill_empty_finished_at_in_deployments_spec.rb b/spec/migrations/fill_empty_finished_at_in_deployments_spec.rb new file mode 100644 index 00000000000..cf5c10f77e1 --- /dev/null +++ b/spec/migrations/fill_empty_finished_at_in_deployments_spec.rb @@ -0,0 +1,70 @@ +require 'spec_helper' +require Rails.root.join('db', 'post_migrate', '20181030135124_fill_empty_finished_at_in_deployments') + +describe FillEmptyFinishedAtInDeployments, :migration do + let(:namespaces) { table(:namespaces) } + let(:projects) { table(:projects) } + let(:environments) { table(:environments) } + let(:deployments) { table(:deployments) } + + context 'when a deployment row does not have a value on finished_at' do + context 'when a deployment succeeded' do + before do + namespaces.create!(id: 123, name: 'gitlab1', path: 'gitlab1') + projects.create!(id: 1, name: 'gitlab1', path: 'gitlab1', namespace_id: 123) + environments.create!(id: 1, name: 'production', slug: 'production', project_id: 1) + deployments.create!(id: 1, iid: 1, project_id: 1, environment_id: 1, ref: 'master', sha: 'xxx', tag: false) + end + + it 'correctly replicates finished_at by created_at' do + expect(deployments.last.created_at).not_to be_nil + expect(deployments.last.finished_at).to be_nil + + migrate! + + expect(deployments.last.created_at).not_to be_nil + expect(deployments.last.finished_at).to eq(deployments.last.created_at) + end + end + + context 'when a deployment is running' do + before do + namespaces.create!(id: 123, name: 'gitlab1', path: 'gitlab1') + projects.create!(id: 1, name: 'gitlab1', path: 'gitlab1', namespace_id: 123) + environments.create!(id: 1, name: 'production', slug: 'production', project_id: 1) + deployments.create!(id: 1, iid: 1, project_id: 1, environment_id: 1, ref: 'master', sha: 'xxx', tag: false, status: 1) + end + + it 'does not fill finished_at' do + expect(deployments.last.created_at).not_to be_nil + expect(deployments.last.finished_at).to be_nil + + migrate! + + expect(deployments.last.created_at).not_to be_nil + expect(deployments.last.finished_at).to be_nil + end + end + end + + context 'when a deployment row does has a value on finished_at' do + let(:finished_at) { '2018-10-30 11:12:02 UTC' } + + before do + namespaces.create!(id: 123, name: 'gitlab1', path: 'gitlab1') + projects.create!(id: 1, name: 'gitlab1', path: 'gitlab1', namespace_id: 123) + environments.create!(id: 1, name: 'production', slug: 'production', project_id: 1) + deployments.create!(id: 1, iid: 1, project_id: 1, environment_id: 1, ref: 'master', sha: 'xxx', tag: false, finished_at: finished_at) + end + + it 'does not affect existing value' do + expect(deployments.last.created_at).not_to be_nil + expect(deployments.last.finished_at).not_to be_nil + + migrate! + + expect(deployments.last.created_at).not_to be_nil + expect(deployments.last.finished_at).to eq(finished_at) + end + end +end diff --git a/spec/migrations/migrate_old_artifacts_spec.rb b/spec/migrations/migrate_old_artifacts_spec.rb index 4187ab149a5..af77d64fdbf 100644 --- a/spec/migrations/migrate_old_artifacts_spec.rb +++ b/spec/migrations/migrate_old_artifacts_spec.rb @@ -76,7 +76,7 @@ describe MigrateOldArtifacts do end end - context 'when there are aritfacts in old and new directory' do + context 'when there are artifacts in old and new directory' do before do store_artifacts_in_legacy_path(build2) diff --git a/spec/models/application_setting_spec.rb b/spec/models/application_setting_spec.rb index 87b91286168..95ae7bd21ab 100644 --- a/spec/models/application_setting_spec.rb +++ b/spec/models/application_setting_spec.rb @@ -594,4 +594,24 @@ describe ApplicationSetting do end end end + + describe '#archive_builds_older_than' do + subject { setting.archive_builds_older_than } + + context 'when the archive_builds_in_seconds is set' do + before do + setting.archive_builds_in_seconds = 3600 + end + + it { is_expected.to be_within(1.minute).of(1.hour.ago) } + end + + context 'when the archive_builds_in_seconds is set' do + before do + setting.archive_builds_in_seconds = nil + end + + it { is_expected.to be_nil } + end + end end diff --git a/spec/models/ci/build_spec.rb b/spec/models/ci/build_spec.rb index 65e06f27f35..2e65a6a2a0f 100644 --- a/spec/models/ci/build_spec.rb +++ b/spec/models/ci/build_spec.rb @@ -17,8 +17,8 @@ describe Ci::Build do it { is_expected.to belong_to(:runner) } it { is_expected.to belong_to(:trigger_request) } it { is_expected.to belong_to(:erased_by) } - it { is_expected.to have_many(:deployments) } it { is_expected.to have_many(:trace_sections)} + it { is_expected.to have_one(:deployment) } it { is_expected.to have_one(:runner_session)} it { is_expected.to validate_presence_of(:ref) } it { is_expected.to respond_to(:has_trace?) } @@ -216,14 +216,6 @@ describe Ci::Build do let(:build) { create(:ci_build, :created, :schedulable, project: project) } it { expect(subject).to be_truthy } - - context 'when feature flag is diabled' do - before do - stub_feature_flags(ci_enable_scheduled_build: false) - end - - it { expect(subject).to be_falsy } - end end context 'when build is not schedulable' do @@ -327,10 +319,6 @@ describe Ci::Build do describe '#enqueue_scheduled' do subject { build.enqueue_scheduled } - before do - stub_feature_flags(ci_enable_scheduled_build: true) - end - context 'when build is scheduled and the right time has not come yet' do let(:build) { create(:ci_build, :scheduled, pipeline: pipeline) } @@ -811,17 +799,100 @@ describe Ci::Build do end end + describe 'state transition as a deployable' do + let!(:build) { create(:ci_build, :start_review_app) } + let(:deployment) { build.deployment } + let(:environment) { deployment.environment } + + it 'has deployments record with created status' do + expect(deployment).to be_created + expect(environment.name).to eq('review/master') + end + + context 'when transits to running' do + before do + build.run! + end + + it 'transits deployment status to running' do + expect(deployment).to be_running + end + end + + context 'when transits to success' do + before do + allow(Deployments::SuccessWorker).to receive(:perform_async) + build.success! + end + + it 'transits deployment status to success' do + expect(deployment).to be_success + end + end + + context 'when transits to failed' do + before do + build.drop! + end + + it 'transits deployment status to failed' do + expect(deployment).to be_failed + end + end + + context 'when transits to skipped' do + before do + build.skip! + end + + it 'transits deployment status to canceled' do + expect(deployment).to be_canceled + end + end + + context 'when transits to canceled' do + before do + build.cancel! + end + + it 'transits deployment status to canceled' do + expect(deployment).to be_canceled + end + end + end + + describe '#on_stop' do + subject { build.on_stop } + + context 'when a job has a specification that it can be stopped from the other job' do + let(:build) { create(:ci_build, :start_review_app) } + + it 'returns the other job name' do + is_expected.to eq('stop_review_app') + end + end + + context 'when a job does not have environment information' do + let(:build) { create(:ci_build) } + + it 'returns nil' do + is_expected.to be_nil + end + end + end + describe 'deployment' do - describe '#last_deployment' do - subject { build.last_deployment } + describe '#has_deployment?' do + subject { build.has_deployment? } + + context 'when build has a deployment' do + let!(:deployment) { create(:deployment, deployable: build) } - context 'when multiple deployments are created' do - let!(:deployment1) { create(:deployment, deployable: build) } - let!(:deployment2) { create(:deployment, deployable: build) } + it { is_expected.to be_truthy } + end - it 'returns the latest one' do - is_expected.to eq(deployment2) - end + context 'when build does not have a deployment' do + it { is_expected.to be_falsy } end end @@ -830,14 +901,14 @@ describe Ci::Build do context 'when build succeeded' do let(:build) { create(:ci_build, :success) } - let!(:deployment) { create(:deployment, deployable: build) } + let!(:deployment) { create(:deployment, :success, deployable: build) } context 'current deployment is latest' do it { is_expected.to be_falsey } end context 'current deployment is not latest on environment' do - let!(:deployment2) { create(:deployment, environment: deployment.environment) } + let!(:deployment2) { create(:deployment, :success, environment: deployment.environment) } it { is_expected.to be_truthy } end @@ -1326,6 +1397,14 @@ describe Ci::Build do it { is_expected.not_to be_retryable } end + + context 'when build is degenerated' do + before do + build.degenerate! + end + + it { is_expected.not_to be_retryable } + end end end @@ -1408,6 +1487,14 @@ describe Ci::Build do expect(subject.retries_max).to eq 0 end end + + context 'when build is degenerated' do + subject { create(:ci_build, :degenerated) } + + it 'returns zero' do + expect(subject.retries_max).to eq 0 + end + end end end @@ -1523,11 +1610,11 @@ describe Ci::Build do end end - describe '#other_actions' do + describe '#other_manual_actions' do let(:build) { create(:ci_build, :manual, pipeline: pipeline) } let!(:other_build) { create(:ci_build, :manual, pipeline: pipeline, name: 'other action') } - subject { build.other_actions } + subject { build.other_manual_actions } before do project.add_developer(user) @@ -1558,6 +1645,48 @@ describe Ci::Build do end end + describe '#other_scheduled_actions' do + let(:build) { create(:ci_build, :scheduled, pipeline: pipeline) } + + subject { build.other_scheduled_actions } + + before do + project.add_developer(user) + end + + context "when other build's status is success" do + let!(:other_build) { create(:ci_build, :schedulable, :success, pipeline: pipeline, name: 'other action') } + + it 'returns other actions' do + is_expected.to contain_exactly(other_build) + end + end + + context "when other build's status is failed" do + let!(:other_build) { create(:ci_build, :schedulable, :failed, pipeline: pipeline, name: 'other action') } + + it 'returns other actions' do + is_expected.to contain_exactly(other_build) + end + end + + context "when other build's status is running" do + let!(:other_build) { create(:ci_build, :schedulable, :running, pipeline: pipeline, name: 'other action') } + + it 'does not return other actions' do + is_expected.to be_empty + end + end + + context "when other build's status is scheduled" do + let!(:other_build) { create(:ci_build, :scheduled, pipeline: pipeline, name: 'other action') } + + it 'does not return other actions' do + is_expected.to contain_exactly(other_build) + end + end + end + describe '#persisted_environment' do let!(:environment) do create(:environment, project: project, name: "foo-#{project.default_branch}") @@ -1629,6 +1758,12 @@ describe Ci::Build do it { is_expected.to be_playable } end + + context 'when build is a manual and degenerated' do + subject { build_stubbed(:ci_build, :manual, :degenerated, status: :manual) } + + it { is_expected.not_to be_playable } + end end context 'when build is scheduled' do @@ -3157,10 +3292,14 @@ describe Ci::Build do end describe '#deployment_status' do + before do + allow_any_instance_of(described_class).to receive(:create_deployment) + end + context 'when build is a last deployment' do let(:build) { create(:ci_build, :success, environment: 'production') } let(:environment) { create(:environment, name: 'production', project: build.project) } - let!(:deployment) { create(:deployment, environment: environment, project: environment.project, deployable: build) } + let!(:deployment) { create(:deployment, :success, environment: environment, project: environment.project, deployable: build) } it { expect(build.deployment_status).to eq(:last) } end @@ -3168,8 +3307,8 @@ describe Ci::Build do context 'when there is a newer build with deployment' do let(:build) { create(:ci_build, :success, environment: 'production') } let(:environment) { create(:environment, name: 'production', project: build.project) } - let!(:deployment) { create(:deployment, environment: environment, project: environment.project, deployable: build) } - let!(:last_deployment) { create(:deployment, environment: environment, project: environment.project) } + let!(:deployment) { create(:deployment, :success, environment: environment, project: environment.project, deployable: build) } + let!(:last_deployment) { create(:deployment, :success, environment: environment, project: environment.project) } it { expect(build.deployment_status).to eq(:out_of_date) } end @@ -3177,7 +3316,7 @@ describe Ci::Build do context 'when build with deployment has failed' do let(:build) { create(:ci_build, :failed, environment: 'production') } let(:environment) { create(:environment, name: 'production', project: build.project) } - let!(:deployment) { create(:deployment, environment: environment, project: environment.project, deployable: build) } + let!(:deployment) { create(:deployment, :success, environment: environment, project: environment.project, deployable: build) } it { expect(build.deployment_status).to eq(:failed) } end @@ -3185,16 +3324,59 @@ describe Ci::Build do context 'when build with deployment is running' do let(:build) { create(:ci_build, environment: 'production') } let(:environment) { create(:environment, name: 'production', project: build.project) } - let!(:deployment) { create(:deployment, environment: environment, project: environment.project, deployable: build) } + let!(:deployment) { create(:deployment, :success, environment: environment, project: environment.project, deployable: build) } it { expect(build.deployment_status).to eq(:creating) } end + end - context 'when build is successful but deployment is not ready yet' do - let(:build) { create(:ci_build, :success, environment: 'production') } - let(:environment) { create(:environment, name: 'production', project: build.project) } + describe '#degenerated?' do + context 'when build is degenerated' do + subject { create(:ci_build, :degenerated) } - it { expect(build.deployment_status).to eq(:creating) } + it { is_expected.to be_degenerated } + end + + context 'when build is valid' do + subject { create(:ci_build) } + + it { is_expected.not_to be_degenerated } + + context 'and becomes degenerated' do + before do + subject.degenerate! + end + + it { is_expected.to be_degenerated } + end + end + end + + describe '#archived?' do + context 'when build is degenerated' do + subject { create(:ci_build, :degenerated) } + + it { is_expected.to be_archived } + end + + context 'for old build' do + subject { create(:ci_build, created_at: 1.day.ago) } + + context 'when archive_builds_in is set' do + before do + stub_application_setting(archive_builds_in_seconds: 3600) + end + + it { is_expected.to be_archived } + end + + context 'when archive_builds_in is not set' do + before do + stub_application_setting(archive_builds_in_seconds: nil) + end + + it { is_expected.not_to be_archived } + end end end end diff --git a/spec/models/ci/pipeline_spec.rb b/spec/models/ci/pipeline_spec.rb index 153244b2159..9e6146b8a44 100644 --- a/spec/models/ci/pipeline_spec.rb +++ b/spec/models/ci/pipeline_spec.rb @@ -1043,6 +1043,11 @@ describe Ci::Pipeline, :mailer do expect(described_class.newest_first.pluck(:status)) .to eq(%w[skipped failed success canceled]) end + + it 'searches limited backlog' do + expect(described_class.newest_first(limit: 1).pluck(:status)) + .to eq(%w[skipped]) + end end describe '.latest_status' do @@ -1148,6 +1153,19 @@ describe Ci::Pipeline, :mailer do end end + describe '.latest_successful_ids_per_project' do + let(:projects) { create_list(:project, 2) } + let!(:pipeline1) { create(:ci_pipeline, :success, project: projects[0]) } + let!(:pipeline2) { create(:ci_pipeline, :success, project: projects[0]) } + let!(:pipeline3) { create(:ci_pipeline, :failed, project: projects[0]) } + let!(:pipeline4) { create(:ci_pipeline, :success, project: projects[1]) } + + it 'returns expected pipeline ids' do + expect(described_class.latest_successful_ids_per_project) + .to contain_exactly(pipeline2, pipeline4) + end + end + describe '.internal_sources' do subject { described_class.internal_sources } diff --git a/spec/models/clusters/applications/prometheus_spec.rb b/spec/models/clusters/applications/prometheus_spec.rb index f9776acd4c8..48ba163b38c 100644 --- a/spec/models/clusters/applications/prometheus_spec.rb +++ b/spec/models/clusters/applications/prometheus_spec.rb @@ -109,14 +109,20 @@ describe Clusters::Applications::Prometheus do end context 'cluster has kubeclient' do + let(:cluster) { create(:cluster, :project, :provided_by_gcp) } let(:kubernetes_url) { subject.cluster.platform_kubernetes.api_url } let(:kube_client) { subject.cluster.kubeclient.core_client } - subject { create(:clusters_applications_prometheus) } + subject { create(:clusters_applications_prometheus, cluster: cluster) } before do subject.cluster.platform_kubernetes.namespace = 'a-namespace' - stub_kubeclient_discover(subject.cluster.platform_kubernetes.api_url) + stub_kubeclient_discover(cluster.platform_kubernetes.api_url) + + create(:cluster_kubernetes_namespace, + cluster: cluster, + cluster_project: cluster.cluster_project, + project: cluster.cluster_project.project) end it 'creates proxy prometheus rest client' do diff --git a/spec/models/clusters/cluster_spec.rb b/spec/models/clusters/cluster_spec.rb index c245e8df815..19b76ca8cfb 100644 --- a/spec/models/clusters/cluster_spec.rb +++ b/spec/models/clusters/cluster_spec.rb @@ -16,6 +16,7 @@ describe Clusters::Cluster do it { is_expected.to have_one(:application_runner) } it { is_expected.to have_many(:kubernetes_namespaces) } it { is_expected.to have_one(:kubernetes_namespace) } + it { is_expected.to have_one(:cluster_project) } it { is_expected.to delegate_method(:status).to(:provider) } it { is_expected.to delegate_method(:status_reason).to(:provider) } diff --git a/spec/models/clusters/kubernetes_namespace_spec.rb b/spec/models/clusters/kubernetes_namespace_spec.rb index dea58fa26c7..0dfeea5cd2f 100644 --- a/spec/models/clusters/kubernetes_namespace_spec.rb +++ b/spec/models/clusters/kubernetes_namespace_spec.rb @@ -10,23 +10,15 @@ RSpec.describe Clusters::KubernetesNamespace, type: :model do describe 'namespace uniqueness validation' do let(:cluster_project) { create(:cluster_project) } - - let(:kubernetes_namespace) do - build(:cluster_kubernetes_namespace, - cluster: cluster_project.cluster, - project: cluster_project.project, - cluster_project: cluster_project) - end + let(:kubernetes_namespace) { build(:cluster_kubernetes_namespace, namespace: 'my-namespace') } subject { kubernetes_namespace } context 'when cluster is using the namespace' do before do create(:cluster_kubernetes_namespace, - cluster: cluster_project.cluster, - project: cluster_project.project, - cluster_project: cluster_project, - namespace: kubernetes_namespace.namespace) + cluster: kubernetes_namespace.cluster, + namespace: 'my-namespace') end it { is_expected.not_to be_valid } @@ -37,48 +29,79 @@ RSpec.describe Clusters::KubernetesNamespace, type: :model do end end - describe '#set_namespace_and_service_account_to_default' do - let(:cluster) { platform.cluster } - let(:cluster_project) { create(:cluster_project, cluster: cluster) } - let(:kubernetes_namespace) do - create(:cluster_kubernetes_namespace, - cluster: cluster_project.cluster, - project: cluster_project.project, - cluster_project: cluster_project) - end + describe '#configure_predefined_variables' do + let(:kubernetes_namespace) { build(:cluster_kubernetes_namespace) } + let(:cluster) { kubernetes_namespace.cluster } + let(:platform) { kubernetes_namespace.platform_kubernetes } - describe 'namespace' do - let(:platform) { create(:cluster_platform_kubernetes, namespace: namespace) } + subject { kubernetes_namespace.configure_predefined_credentials } - subject { kubernetes_namespace.namespace } + describe 'namespace' do + before do + platform.update_column(:namespace, namespace) + end context 'when platform has a namespace assigned' do let(:namespace) { 'platform-namespace' } it 'should copy the namespace' do - is_expected.to eq('platform-namespace') + subject + + expect(kubernetes_namespace.namespace).to eq('platform-namespace') end end context 'when platform does not have namespace assigned' do + let(:project) { kubernetes_namespace.project } let(:namespace) { nil } + let(:project_slug) { "#{project.path}-#{project.id}" } - it 'should set default namespace' do - project_slug = "#{cluster_project.project.path}-#{cluster_project.project_id}" + it 'should fallback to project namespace' do + subject - is_expected.to eq(project_slug) + expect(kubernetes_namespace.namespace).to eq(project_slug) end end end describe 'service_account_name' do - let(:platform) { create(:cluster_platform_kubernetes) } - - subject { kubernetes_namespace.service_account_name } + let(:service_account_name) { "#{kubernetes_namespace.namespace}-service-account" } it 'should set a service account name based on namespace' do - is_expected.to eq("#{kubernetes_namespace.namespace}-service-account") + subject + + expect(kubernetes_namespace.service_account_name).to eq(service_account_name) end end end + + describe '#predefined_variables' do + let(:kubernetes_namespace) { create(:cluster_kubernetes_namespace, cluster: cluster, service_account_token: token) } + let(:cluster) { create(:cluster, :project, platform_kubernetes: platform) } + let(:platform) { create(:cluster_platform_kubernetes, api_url: api_url, ca_cert: ca_pem, token: token) } + + let(:api_url) { 'https://kube.domain.com' } + let(:ca_pem) { 'CA PEM DATA' } + let(:token) { 'token' } + + let(:kubeconfig) do + config_file = expand_fixture_path('config/kubeconfig.yml') + config = YAML.safe_load(File.read(config_file)) + config.dig('users', 0, 'user')['token'] = token + config.dig('contexts', 0, 'context')['namespace'] = kubernetes_namespace.namespace + config.dig('clusters', 0, 'cluster')['certificate-authority-data'] = + Base64.strict_encode64(ca_pem) + + YAML.dump(config) + end + + it 'sets the variables' do + expect(kubernetes_namespace.predefined_variables).to include( + { key: 'KUBE_SERVICE_ACCOUNT', value: kubernetes_namespace.service_account_name, public: true }, + { key: 'KUBE_NAMESPACE', value: kubernetes_namespace.namespace, public: true }, + { key: 'KUBE_TOKEN', value: kubernetes_namespace.service_account_token, public: false }, + { key: 'KUBECONFIG', value: kubeconfig, public: false, file: true } + ) + end + end end diff --git a/spec/models/clusters/platforms/kubernetes_spec.rb b/spec/models/clusters/platforms/kubernetes_spec.rb index e13eb554add..2bcccc8184a 100644 --- a/spec/models/clusters/platforms/kubernetes_spec.rb +++ b/spec/models/clusters/platforms/kubernetes_spec.rb @@ -124,9 +124,17 @@ describe Clusters::Platforms::Kubernetes, :use_clean_rails_memory_store_caching end describe '#kubeclient' do + let(:cluster) { create(:cluster, :project) } + let(:kubernetes) { build(:cluster_platform_kubernetes, :configured, namespace: 'a-namespace', cluster: cluster) } + subject { kubernetes.kubeclient } - let(:kubernetes) { build(:cluster_platform_kubernetes, :configured, namespace: 'a-namespace') } + before do + create(:cluster_kubernetes_namespace, + cluster: kubernetes.cluster, + cluster_project: kubernetes.cluster.cluster_project, + project: kubernetes.cluster.cluster_project.project) + end it { is_expected.to be_an_instance_of(Gitlab::Kubernetes::KubeClient) } end @@ -186,29 +194,14 @@ describe Clusters::Platforms::Kubernetes, :use_clean_rails_memory_store_caching describe '#predefined_variables' do let!(:cluster) { create(:cluster, :project, platform_kubernetes: kubernetes) } - let(:kubernetes) { create(:cluster_platform_kubernetes, api_url: api_url, ca_cert: ca_pem, token: token) } + let(:kubernetes) { create(:cluster_platform_kubernetes, api_url: api_url, ca_cert: ca_pem) } let(:api_url) { 'https://kube.domain.com' } let(:ca_pem) { 'CA PEM DATA' } - let(:token) { 'token' } - - let(:kubeconfig) do - config_file = expand_fixture_path('config/kubeconfig.yml') - config = YAML.load(File.read(config_file)) - config.dig('users', 0, 'user')['token'] = token - config.dig('contexts', 0, 'context')['namespace'] = namespace - config.dig('clusters', 0, 'cluster')['certificate-authority-data'] = - Base64.strict_encode64(ca_pem) - - YAML.dump(config) - end shared_examples 'setting variables' do it 'sets the variables' do - expect(kubernetes.predefined_variables).to include( + expect(kubernetes.predefined_variables(project: cluster.project)).to include( { key: 'KUBE_URL', value: api_url, public: true }, - { key: 'KUBE_TOKEN', value: token, public: false }, - { key: 'KUBE_NAMESPACE', value: namespace, public: true }, - { key: 'KUBECONFIG', value: kubeconfig, public: false, file: true }, { key: 'KUBE_CA_PEM', value: ca_pem, public: true }, { key: 'KUBE_CA_PEM_FILE', value: ca_pem, public: true, file: true } ) @@ -229,13 +222,6 @@ describe Clusters::Platforms::Kubernetes, :use_clean_rails_memory_store_caching let(:namespace) { kubernetes.actual_namespace } it_behaves_like 'setting variables' - - it 'sets the KUBE_NAMESPACE' do - kube_namespace = kubernetes.predefined_variables.find { |h| h[:key] == 'KUBE_NAMESPACE' } - - expect(kube_namespace).not_to be_nil - expect(kube_namespace[:value]).to match(/\A#{Gitlab::PathRegex::PATH_REGEX_STR}-\d+\z/) - end end end @@ -319,4 +305,27 @@ describe Clusters::Platforms::Kubernetes, :use_clean_rails_memory_store_caching it { is_expected.to include(pods: []) } end end + + describe '#update_kubernetes_namespace' do + let(:cluster) { create(:cluster, :provided_by_gcp) } + let(:platform) { cluster.platform } + + context 'when namespace is updated' do + it 'should call ConfigureWorker' do + expect(ClusterPlatformConfigureWorker).to receive(:perform_async).with(cluster.id).once + + platform.namespace = 'new-namespace' + platform.save + end + end + + context 'when namespace is not updated' do + it 'should not call ConfigureWorker' do + expect(ClusterPlatformConfigureWorker).not_to receive(:perform_async) + + platform.username = "new-username" + platform.save + end + end + end end diff --git a/spec/models/concerns/awardable_spec.rb b/spec/models/concerns/awardable_spec.rb index 69083bdc125..5713106418d 100644 --- a/spec/models/concerns/awardable_spec.rb +++ b/spec/models/concerns/awardable_spec.rb @@ -24,13 +24,29 @@ describe Awardable do end end - describe ".awarded" do + describe "#awarded" do it "filters by user and emoji name" do expect(Issue.awarded(award_emoji.user, "thumbsup")).to be_empty expect(Issue.awarded(award_emoji.user, "thumbsdown")).to eq [issue] expect(Issue.awarded(award_emoji2.user, "thumbsup")).to eq [issue2] expect(Issue.awarded(award_emoji2.user, "thumbsdown")).to be_empty end + + it "filters by user and any emoji" do + issue3 = create(:issue) + create(:award_emoji, awardable: issue3, name: "star", user: award_emoji.user) + create(:award_emoji, awardable: issue3, name: "star", user: award_emoji2.user) + + expect(Issue.awarded(award_emoji.user)).to contain_exactly(issue, issue3) + expect(Issue.awarded(award_emoji2.user)).to contain_exactly(issue2, issue3) + end + end + + describe "#not_awarded" do + it "returns issues not awarded by user" do + expect(Issue.not_awarded(award_emoji.user)).to eq [issue2] + expect(Issue.not_awarded(award_emoji2.user)).to eq [issue] + end end end diff --git a/spec/models/concerns/cacheable_attributes_spec.rb b/spec/models/concerns/cacheable_attributes_spec.rb index f8c2e29fadd..827fbc9d7d5 100644 --- a/spec/models/concerns/cacheable_attributes_spec.rb +++ b/spec/models/concerns/cacheable_attributes_spec.rb @@ -41,7 +41,7 @@ describe CacheableAttributes do expect(minimal_test_class.current_without_cache).to eq(minimal_test_class.last) end - it 'can be overriden' do + it 'can be overridden' do minimal_test_class.define_singleton_method(:current_without_cache) do first end @@ -64,7 +64,7 @@ describe CacheableAttributes do context 'with defaults defined' do include_context 'with defaults' - it 'can be overriden' do + it 'can be overridden' do expect(minimal_test_class.defaults).to eq({ foo: 'a', bar: 'b', baz: 'c' }) end end diff --git a/spec/models/concerns/deployable_spec.rb b/spec/models/concerns/deployable_spec.rb new file mode 100644 index 00000000000..ac79c75a55e --- /dev/null +++ b/spec/models/concerns/deployable_spec.rb @@ -0,0 +1,53 @@ +require 'rails_helper' + +describe Deployable do + describe '#create_deployment' do + let(:deployment) { job.deployment } + let(:environment) { deployment&.environment } + + before do + job.reload + end + + context 'when the deployable object will deploy to production' do + let!(:job) { create(:ci_build, :start_review_app) } + + it 'creates a deployment and environment record' do + expect(deployment.project).to eq(job.project) + expect(deployment.ref).to eq(job.ref) + expect(deployment.tag).to eq(job.tag) + expect(deployment.sha).to eq(job.sha) + expect(deployment.user).to eq(job.user) + expect(deployment.deployable).to eq(job) + expect(deployment.on_stop).to eq('stop_review_app') + expect(environment.name).to eq('review/master') + end + end + + context 'when the deployable object will stop an environment' do + let!(:job) { create(:ci_build, :stop_review_app) } + + it 'does not create a deployment record' do + expect(deployment).to be_nil + end + end + + context 'when the deployable object has already had a deployment' do + let!(:job) { create(:ci_build, :start_review_app, deployment: race_deployment) } + let!(:race_deployment) { create(:deployment, :success) } + + it 'does not create a new deployment' do + expect(deployment).to eq(race_deployment) + end + end + + context 'when the deployable object will not deploy' do + let!(:job) { create(:ci_build) } + + it 'does not create a deployment and environment record' do + expect(deployment).to be_nil + expect(environment).to be_nil + end + end + end +end diff --git a/spec/models/concerns/issuable_spec.rb b/spec/models/concerns/issuable_spec.rb index ec6374f3963..a4bf3e2350a 100644 --- a/spec/models/concerns/issuable_spec.rb +++ b/spec/models/concerns/issuable_spec.rb @@ -519,7 +519,7 @@ describe Issuable do end end - context 'substracting time' do + context 'subtracting time' do before do spend_time(1800) end @@ -530,7 +530,7 @@ describe Issuable do expect(issue.total_time_spent).to eq(900) end - context 'when time to substract exceeds the total time spent' do + context 'when time to subtract exceeds the total time spent' do it 'raise a validation error' do Timecop.travel(1.minute.from_now) do expect do diff --git a/spec/models/deployment_spec.rb b/spec/models/deployment_spec.rb index b8364e0cf88..06c1e9c8c6a 100644 --- a/spec/models/deployment_spec.rb +++ b/spec/models/deployment_spec.rb @@ -16,6 +16,22 @@ describe Deployment do it { is_expected.to validate_presence_of(:ref) } it { is_expected.to validate_presence_of(:sha) } + describe '#scheduled_actions' do + subject { deployment.scheduled_actions } + + let(:project) { create(:project, :repository) } + let(:pipeline) { create(:ci_pipeline, project: project) } + let(:build) { create(:ci_build, :success, pipeline: pipeline) } + let(:deployment) { create(:deployment, deployable: build) } + + it 'delegates to other_scheduled_actions' do + expect_any_instance_of(Ci::Build) + .to receive(:other_scheduled_actions) + + subject + end + end + describe 'modules' do it_behaves_like 'AtomicInternalId' do let(:internal_id_attribute) { :iid } @@ -26,16 +42,174 @@ describe Deployment do end end - describe 'after_create callbacks' do - let(:environment) { create(:environment) } - let(:store) { Gitlab::EtagCaching::Store.new } + describe '.success' do + subject { described_class.success } + + context 'when deployment status is success' do + let(:deployment) { create(:deployment, :success) } + + it { is_expected.to eq([deployment]) } + end + + context 'when deployment status is created' do + let(:deployment) { create(:deployment, :created) } + + it { is_expected.to be_empty } + end + + context 'when deployment status is running' do + let(:deployment) { create(:deployment, :running) } + + it { is_expected.to be_empty } + end + end + + describe 'state machine' do + context 'when deployment runs' do + let(:deployment) { create(:deployment) } + + before do + deployment.run! + end + + it 'starts running' do + Timecop.freeze do + expect(deployment).to be_running + expect(deployment.finished_at).to be_nil + end + end + end + + context 'when deployment succeeded' do + let(:deployment) { create(:deployment, :running) } + + it 'has correct status' do + Timecop.freeze do + deployment.succeed! + + expect(deployment).to be_success + expect(deployment.finished_at).to eq(Time.now) + end + end + + it 'executes Deployments::SuccessWorker asynchronously' do + expect(Deployments::SuccessWorker) + .to receive(:perform_async).with(deployment.id) + + deployment.succeed! + end + end + + context 'when deployment failed' do + let(:deployment) { create(:deployment, :running) } + + it 'has correct status' do + Timecop.freeze do + deployment.drop! + + expect(deployment).to be_failed + expect(deployment.finished_at).to eq(Time.now) + end + end + end + + context 'when deployment was canceled' do + let(:deployment) { create(:deployment, :running) } + + it 'has correct status' do + Timecop.freeze do + deployment.cancel! + + expect(deployment).to be_canceled + expect(deployment.finished_at).to eq(Time.now) + end + end + end + end + + describe '#success?' do + subject { deployment.success? } + + context 'when deployment status is success' do + let(:deployment) { create(:deployment, :success) } + + it { is_expected.to be_truthy } + end + + context 'when deployment status is failed' do + let(:deployment) { create(:deployment, :failed) } + + it { is_expected.to be_falsy } + end + end + + describe '#status_name' do + subject { deployment.status_name } + + context 'when deployment status is success' do + let(:deployment) { create(:deployment, :success) } + + it { is_expected.to eq(:success) } + end + + context 'when deployment status is failed' do + let(:deployment) { create(:deployment, :failed) } + + it { is_expected.to eq(:failed) } + end + end + + describe '#finished_at' do + subject { deployment.finished_at } - it 'invalidates the environment etag cache' do - old_value = store.get(environment.etag_cache_key) + context 'when deployment status is created' do + let(:deployment) { create(:deployment) } - create(:deployment, environment: environment) + it { is_expected.to be_nil } + end + + context 'when deployment status is success' do + let(:deployment) { create(:deployment, :success) } + + it { is_expected.to eq(deployment.read_attribute(:finished_at)) } + end - expect(store.get(environment.etag_cache_key)).not_to eq(old_value) + context 'when deployment status is success' do + let(:deployment) { create(:deployment, :success, finished_at: nil) } + + before do + deployment.update_column(:finished_at, nil) + end + + it { is_expected.to eq(deployment.read_attribute(:created_at)) } + end + + context 'when deployment status is running' do + let(:deployment) { create(:deployment, :running) } + + it { is_expected.to be_nil } + end + end + + describe '#deployed_at' do + subject { deployment.deployed_at } + + context 'when deployment status is created' do + let(:deployment) { create(:deployment) } + + it { is_expected.to be_nil } + end + + context 'when deployment status is success' do + let(:deployment) { create(:deployment, :success) } + + it { is_expected.to eq(deployment.read_attribute(:finished_at)) } + end + + context 'when deployment status is running' do + let(:deployment) { create(:deployment, :running) } + + it { is_expected.to be_nil } end end @@ -96,7 +270,7 @@ describe Deployment do end describe '#metrics' do - let(:deployment) { create(:deployment) } + let(:deployment) { create(:deployment, :success) } let(:prometheus_adapter) { double('prometheus_adapter', can_query?: true) } subject { deployment.metrics } @@ -125,7 +299,7 @@ describe Deployment do describe '#additional_metrics' do let(:project) { create(:project, :repository) } - let(:deployment) { create(:deployment, project: project) } + let(:deployment) { create(:deployment, :succeed, project: project) } subject { deployment.additional_metrics } diff --git a/spec/models/environment_spec.rb b/spec/models/environment_spec.rb index 1de95d881a7..e121369f6ac 100644 --- a/spec/models/environment_spec.rb +++ b/spec/models/environment_spec.rb @@ -95,7 +95,7 @@ describe Environment do context 'with a last deployment' do let!(:deployment) do - create(:deployment, environment: environment, sha: project.commit('master').id) + create(:deployment, :success, environment: environment, sha: project.commit('master').id) end context 'in the same branch' do @@ -136,8 +136,8 @@ describe Environment do describe '#first_deployment_for' do let(:project) { create(:project, :repository) } - let!(:deployment) { create(:deployment, environment: environment, ref: commit.parent.id) } - let!(:deployment1) { create(:deployment, environment: environment, ref: commit.id) } + let!(:deployment) { create(:deployment, :succeed, environment: environment, ref: commit.parent.id) } + let!(:deployment1) { create(:deployment, :succeed, environment: environment, ref: commit.id) } let(:head_commit) { project.commit } let(:commit) { project.commit.parent } @@ -181,7 +181,8 @@ describe Environment do let(:build) { create(:ci_build) } let!(:deployment) do - create(:deployment, environment: environment, + create(:deployment, :success, + environment: environment, deployable: build, on_stop: 'close_app') end @@ -249,7 +250,8 @@ describe Environment do let(:build) { create(:ci_build, pipeline: pipeline) } let!(:deployment) do - create(:deployment, environment: environment, + create(:deployment, :success, + environment: environment, deployable: build, on_stop: 'close_app') end @@ -304,7 +306,7 @@ describe Environment do context 'when last deployment to environment is the most recent one' do before do - create(:deployment, environment: environment, ref: 'feature') + create(:deployment, :success, environment: environment, ref: 'feature') end it { is_expected.to be true } @@ -312,8 +314,8 @@ describe Environment do context 'when last deployment to environment is not the most recent' do before do - create(:deployment, environment: environment, ref: 'feature') - create(:deployment, environment: environment, ref: 'master') + create(:deployment, :success, environment: environment, ref: 'feature') + create(:deployment, :success, environment: environment, ref: 'master') end it { is_expected.to be false } @@ -321,7 +323,7 @@ describe Environment do end describe '#actions_for' do - let(:deployment) { create(:deployment, environment: environment) } + let(:deployment) { create(:deployment, :success, environment: environment) } let(:pipeline) { deployment.deployable.pipeline } let!(:review_action) { create(:ci_build, :manual, name: 'review-apps', pipeline: pipeline, environment: 'review/$CI_COMMIT_REF_NAME' )} let!(:production_action) { create(:ci_build, :manual, name: 'production', pipeline: pipeline, environment: 'production' )} @@ -331,6 +333,70 @@ describe Environment do end end + describe '.deployments' do + subject { environment.deployments } + + context 'when there is a deployment record with created status' do + let(:deployment) { create(:deployment, :created, environment: environment) } + + it 'does not return the record' do + is_expected.to be_empty + end + end + + context 'when there is a deployment record with running status' do + let(:deployment) { create(:deployment, :running, environment: environment) } + + it 'does not return the record' do + is_expected.to be_empty + end + end + + context 'when there is a deployment record with success status' do + let(:deployment) { create(:deployment, :success, environment: environment) } + + it 'returns the record' do + is_expected.to eq([deployment]) + end + end + end + + describe '.last_deployment' do + subject { environment.last_deployment } + + before do + allow_any_instance_of(Deployment).to receive(:create_ref) + end + + context 'when there is an old deployment record' do + let!(:previous_deployment) { create(:deployment, :success, environment: environment) } + + context 'when there is a deployment record with created status' do + let!(:deployment) { create(:deployment, environment: environment) } + + it 'returns the previous deployment' do + is_expected.to eq(previous_deployment) + end + end + + context 'when there is a deployment record with running status' do + let!(:deployment) { create(:deployment, :running, environment: environment) } + + it 'returns the previous deployment' do + is_expected.to eq(previous_deployment) + end + end + + context 'when there is a deployment record with success status' do + let!(:deployment) { create(:deployment, :success, environment: environment) } + + it 'returns the latest successful deployment' do + is_expected.to eq(deployment) + end + end + end + end + describe '#has_terminals?' do subject { environment.has_terminals? } @@ -338,7 +404,7 @@ describe Environment do context 'with a deployment service' do shared_examples 'same behavior between KubernetesService and Platform::Kubernetes' do context 'and a deployment' do - let!(:deployment) { create(:deployment, environment: environment) } + let!(:deployment) { create(:deployment, :success, environment: environment) } it { is_expected.to be_truthy } end diff --git a/spec/models/environment_status_spec.rb b/spec/models/environment_status_spec.rb index e7805d52d75..52b98552184 100644 --- a/spec/models/environment_status_spec.rb +++ b/spec/models/environment_status_spec.rb @@ -1,7 +1,7 @@ require 'spec_helper' describe EnvironmentStatus do - let(:deployment) { create(:deployment, :review_app) } + let(:deployment) { create(:deployment, :succeed, :review_app) } let(:environment) { deployment.environment} let(:project) { deployment.project } let(:merge_request) { create(:merge_request, :deployed_review_app, deployment: deployment) } @@ -12,7 +12,7 @@ describe EnvironmentStatus do it { is_expected.to delegate_method(:id).to(:environment) } it { is_expected.to delegate_method(:name).to(:environment) } it { is_expected.to delegate_method(:project).to(:environment) } - it { is_expected.to delegate_method(:deployed_at).to(:deployment).as(:created_at) } + it { is_expected.to delegate_method(:deployed_at).to(:deployment) } it { is_expected.to delegate_method(:status).to(:deployment) } describe '#project' do diff --git a/spec/models/lfs_object_spec.rb b/spec/models/lfs_object_spec.rb index 911f85d7b28..3f929710862 100644 --- a/spec/models/lfs_object_spec.rb +++ b/spec/models/lfs_object_spec.rb @@ -8,7 +8,7 @@ describe LfsObject do expect(subject.local_store?).to eq true end - it 'returns false whe file_store is equal to LfsObjectUploader::Store::REMOTE' do + it 'returns false when file_store is equal to LfsObjectUploader::Store::REMOTE' do subject.file_store = LfsObjectUploader::Store::REMOTE expect(subject.local_store?).to eq false diff --git a/spec/models/merge_request_diff_spec.rb b/spec/models/merge_request_diff_spec.rb index 90cce826b6c..47e8f04e728 100644 --- a/spec/models/merge_request_diff_spec.rb +++ b/spec/models/merge_request_diff_spec.rb @@ -52,9 +52,9 @@ describe MergeRequestDiff do context 'when it was not cleaned by the system' do it 'returns persisted diffs' do - expect(diff).to receive(:load_diffs) + expect(diff).to receive(:load_diffs).and_call_original - diff.diffs + diff.diffs.diff_files end end @@ -76,19 +76,19 @@ describe MergeRequestDiff do end it 'returns persisted diffs if cannot compare with diff refs' do - expect(diff).to receive(:load_diffs) + expect(diff).to receive(:load_diffs).and_call_original diff.update!(head_commit_sha: 'invalid-sha') - diff.diffs + diff.diffs.diff_files end it 'returns persisted diffs if diff refs does not exist' do - expect(diff).to receive(:load_diffs) + expect(diff).to receive(:load_diffs).and_call_original diff.update!(start_commit_sha: nil, base_commit_sha: nil) - diff.diffs + diff.diffs.diff_files end end end @@ -211,4 +211,25 @@ describe MergeRequestDiff do expect(diff_with_commits.commits_count).to eq(29) end end + + describe '#commits_by_shas' do + let(:commit_shas) { diff_with_commits.commit_shas } + + it 'returns empty if no SHAs were provided' do + expect(diff_with_commits.commits_by_shas([])).to be_empty + end + + it 'returns one SHA' do + commits = diff_with_commits.commits_by_shas([commit_shas.first, Gitlab::Git::BLANK_SHA]) + + expect(commits.count).to eq(1) + end + + it 'returns all matching SHAs' do + commits = diff_with_commits.commits_by_shas(commit_shas) + + expect(commits.count).to eq(commit_shas.count) + expect(commits.map(&:sha)).to match_array(commit_shas) + end + end end diff --git a/spec/models/merge_request_spec.rb b/spec/models/merge_request_spec.rb index c8943f2d86f..3a54725c7ec 100644 --- a/spec/models/merge_request_spec.rb +++ b/spec/models/merge_request_spec.rb @@ -13,6 +13,20 @@ describe MergeRequest do it { is_expected.to belong_to(:merge_user).class_name("User") } it { is_expected.to belong_to(:assignee) } it { is_expected.to have_many(:merge_request_diffs) } + + context 'for forks' do + let!(:project) { create(:project) } + let!(:fork) { fork_project(project) } + let!(:merge_request) { create(:merge_request, target_project: project, source_project: fork) } + + it 'does not load another project due to inverse relationship' do + expect(project.merge_requests.first.target_project.object_id).to eq(project.object_id) + end + + it 'finds the associated merge request' do + expect(project.merge_requests.find(merge_request.id)).to eq(merge_request) + end + end end describe '#squash_in_progress?' do @@ -538,9 +552,9 @@ describe MergeRequest do it 'delegates to the MR diffs' do merge_request.save - expect(merge_request.merge_request_diff).to receive(:raw_diffs).with(hash_including(options)) + expect(merge_request.merge_request_diff).to receive(:raw_diffs).with(hash_including(options)).and_call_original - merge_request.diffs(options) + merge_request.diffs(options).diff_files end end @@ -1822,8 +1836,8 @@ describe MergeRequest do let(:environments) { create_list(:environment, 3, project: project) } before do - create(:deployment, environment: environments.first, ref: 'master', sha: project.commit('master').id) - create(:deployment, environment: environments.second, ref: 'feature', sha: project.commit('feature').id) + create(:deployment, :success, environment: environments.first, ref: 'master', sha: project.commit('master').id) + create(:deployment, :success, environment: environments.second, ref: 'feature', sha: project.commit('feature').id) end it 'selects deployed environments' do @@ -1843,7 +1857,7 @@ describe MergeRequest do let(:source_environment) { create(:environment, project: source_project) } before do - create(:deployment, environment: source_environment, ref: 'feature', sha: merge_request.diff_head_sha) + create(:deployment, :success, environment: source_environment, ref: 'feature', sha: merge_request.diff_head_sha) end it 'selects deployed environments' do @@ -1854,7 +1868,7 @@ describe MergeRequest do let(:target_environment) { create(:environment, project: project) } before do - create(:deployment, environment: target_environment, tag: true, sha: merge_request.diff_head_sha) + create(:deployment, :success, environment: target_environment, tag: true, sha: merge_request.diff_head_sha) end it 'selects deployed environments' do @@ -2597,6 +2611,32 @@ describe MergeRequest do end end + describe '#includes_any_commits?' do + it 'returns false' do + expect(subject.includes_any_commits?([Gitlab::Git::BLANK_SHA])).to be_falsey + end + + it 'returns true' do + expect(subject.includes_any_commits?([subject.merge_request_diff.head_commit_sha])).to be_truthy + end + + it 'returns true even when there is a non-existent comit' do + expect(subject.includes_any_commits?([Gitlab::Git::BLANK_SHA, subject.merge_request_diff.head_commit_sha])).to be_truthy + end + + context 'unpersisted merge request' do + let(:new_mr) { build(:merge_request) } + + it 'returns false' do + expect(new_mr.includes_any_commits?([Gitlab::Git::BLANK_SHA])).to be_falsey + end + + it 'returns true' do + expect(new_mr.includes_any_commits?([subject.merge_request_diff.head_commit_sha])).to be_truthy + end + end + end + describe '#can_allow_collaboration?' do let(:target_project) { create(:project, :public) } let(:source_project) { fork_project(target_project) } diff --git a/spec/models/namespace_spec.rb b/spec/models/namespace_spec.rb index 8913644a3ce..2db42fe802a 100644 --- a/spec/models/namespace_spec.rb +++ b/spec/models/namespace_spec.rb @@ -562,6 +562,17 @@ describe Namespace do it { expect(group.all_projects.to_a).to match_array([project2, project1]) } end + describe '#all_pipelines' do + let(:group) { create(:group) } + let(:child) { create(:group, parent: group) } + let!(:project1) { create(:project_empty_repo, namespace: group) } + let!(:project2) { create(:project_empty_repo, namespace: child) } + let!(:pipeline1) { create(:ci_empty_pipeline, project: project1) } + let!(:pipeline2) { create(:ci_empty_pipeline, project: project2) } + + it { expect(group.all_pipelines.to_a).to match_array([pipeline1, pipeline2]) } + end + describe '#share_with_group_lock with subgroups', :nested_groups do context 'when creating a subgroup' do let(:subgroup) { create(:group, parent: root_group )} diff --git a/spec/models/postgresql/replication_slot_spec.rb b/spec/models/postgresql/replication_slot_spec.rb index 919a7526803..e100af7ddc7 100644 --- a/spec/models/postgresql/replication_slot_spec.rb +++ b/spec/models/postgresql/replication_slot_spec.rb @@ -3,7 +3,27 @@ require 'spec_helper' describe Postgresql::ReplicationSlot, :postgresql do + describe '.in_use?' do + it 'returns true when replication slots are present' do + expect(described_class).to receive(:exists?).and_return(true) + expect(described_class.in_use?).to be_truthy + end + + it 'returns false when replication slots are not present' do + expect(described_class.in_use?).to be_falsey + end + + it 'returns false if the existence check is invalid' do + expect(described_class).to receive(:exists?).and_raise(ActiveRecord::StatementInvalid.new('PG::FeatureNotSupported')) + expect(described_class.in_use?).to be_falsey + end + end + describe '.lag_too_great?' do + before do + expect(described_class).to receive(:in_use?).and_return(true) + end + it 'returns true when replication lag is too great' do expect(described_class) .to receive(:pluck) diff --git a/spec/models/project_services/kubernetes_service_spec.rb b/spec/models/project_services/kubernetes_service_spec.rb index 68ab9fd08ec..9c27357ffaf 100644 --- a/spec/models/project_services/kubernetes_service_spec.rb +++ b/spec/models/project_services/kubernetes_service_spec.rb @@ -253,7 +253,7 @@ describe KubernetesService, :use_clean_rails_memory_store_caching do end end - describe '#predefined_variables' do + describe '#predefined_variable' do let(:kubeconfig) do config_file = expand_fixture_path('config/kubeconfig.yml') config = YAML.load(File.read(config_file)) @@ -274,7 +274,7 @@ describe KubernetesService, :use_clean_rails_memory_store_caching do shared_examples 'setting variables' do it 'sets the variables' do - expect(subject.predefined_variables).to include( + expect(subject.predefined_variables(project: project)).to include( { key: 'KUBE_URL', value: 'https://kube.domain.com', public: true }, { key: 'KUBE_TOKEN', value: 'token', public: false }, { key: 'KUBE_NAMESPACE', value: namespace, public: true }, @@ -301,7 +301,7 @@ describe KubernetesService, :use_clean_rails_memory_store_caching do it_behaves_like 'setting variables' it 'sets the KUBE_NAMESPACE' do - kube_namespace = subject.predefined_variables.find { |h| h[:key] == 'KUBE_NAMESPACE' } + kube_namespace = subject.predefined_variables(project: project).find { |h| h[:key] == 'KUBE_NAMESPACE' } expect(kube_namespace).not_to be_nil expect(kube_namespace[:value]).to match(/\A#{Gitlab::PathRegex::PATH_REGEX_STR}-\d+\z/) diff --git a/spec/models/project_spec.rb b/spec/models/project_spec.rb index e66838edd1a..f020557e4af 100644 --- a/spec/models/project_spec.rb +++ b/spec/models/project_spec.rb @@ -8,6 +8,7 @@ describe Project do it { is_expected.to belong_to(:group) } it { is_expected.to belong_to(:namespace) } it { is_expected.to belong_to(:creator).class_name('User') } + it { is_expected.to belong_to(:pool_repository) } it { is_expected.to have_many(:users) } it { is_expected.to have_many(:services) } it { is_expected.to have_many(:events) } @@ -88,6 +89,10 @@ describe Project do it { is_expected.to have_many(:project_deploy_tokens) } it { is_expected.to have_many(:deploy_tokens).through(:project_deploy_tokens) } + it 'has an inverse relationship with merge requests' do + expect(described_class.reflect_on_association(:merge_requests).has_inverse?).to eq(:target_project) + end + context 'after initialized' do it "has a project_feature" do expect(described_class.new.project_feature).to be_present @@ -2401,12 +2406,24 @@ describe Project do it_behaves_like 'same behavior between KubernetesService and Platform::Kubernetes' end - context 'when user configured kubernetes from CI/CD > Clusters' do + context 'when user configured kubernetes from CI/CD > Clusters and KubernetesNamespace migration has not been executed' do let!(:cluster) { create(:cluster, :project, :provided_by_gcp) } let(:project) { cluster.project } it_behaves_like 'same behavior between KubernetesService and Platform::Kubernetes' end + + context 'when user configured kubernetes from CI/CD > Clusters and KubernetesNamespace migration has been executed' do + let!(:kubernetes_namespace) { create(:cluster_kubernetes_namespace) } + let!(:cluster) { kubernetes_namespace.cluster } + let(:project) { kubernetes_namespace.project } + + it 'should return token from kubernetes namespace' do + expect(project.deployment_variables).to include( + { key: 'KUBE_TOKEN', value: kubernetes_namespace.service_account_token, public: false } + ) + end + end end end @@ -2746,7 +2763,7 @@ describe Project do .to raise_error(ActiveRecord::RecordNotSaved, error_message) end - it 'updates the project succesfully' do + it 'updates the project successfully' do merge_request = create(:merge_request, target_project: project, source_project: project) expect { project.append_or_update_attribute(:merge_requests, [merge_request]) } @@ -3314,7 +3331,7 @@ describe Project do end end - context 'when explicitely enabled' do + context 'when explicitly enabled' do context 'when domain is empty' do before do create(:project_auto_devops, project: project, domain: nil) @@ -3959,6 +3976,40 @@ describe Project do end end + describe '.deployments' do + subject { project.deployments } + + let(:project) { create(:project) } + + before do + allow_any_instance_of(Deployment).to receive(:create_ref) + end + + context 'when there is a deployment record with created status' do + let(:deployment) { create(:deployment, :created, project: project) } + + it 'does not return the record' do + is_expected.to be_empty + end + end + + context 'when there is a deployment record with running status' do + let(:deployment) { create(:deployment, :running, project: project) } + + it 'does not return the record' do + is_expected.to be_empty + end + end + + context 'when there is a deployment record with success status' do + let(:deployment) { create(:deployment, :success, project: project) } + + it 'returns the record' do + is_expected.to eq([deployment]) + end + end + end + def rugged_config rugged_repo(project.repository).config end diff --git a/spec/models/project_wiki_spec.rb b/spec/models/project_wiki_spec.rb index f38fc191943..cc5e34782ec 100644 --- a/spec/models/project_wiki_spec.rb +++ b/spec/models/project_wiki_spec.rb @@ -145,7 +145,7 @@ describe ProjectWiki do end it "returns nil if the page does not exist" do - expect(subject.find_page("non-existant")).to eq(nil) + expect(subject.find_page("non-existent")).to eq(nil) end it "can find a page by slug" do @@ -226,7 +226,7 @@ describe ProjectWiki do end it 'returns nil if the page does not exist' do - expect(subject.find_file('non-existant')).to eq(nil) + expect(subject.find_file('non-existent')).to eq(nil) end it 'returns a Gitlab::Git::WikiFile instance' do diff --git a/spec/models/shard_spec.rb b/spec/models/shard_spec.rb new file mode 100644 index 00000000000..83104711b55 --- /dev/null +++ b/spec/models/shard_spec.rb @@ -0,0 +1,50 @@ +# frozen_string_literals: true +require 'spec_helper' + +describe Shard do + describe '.populate!' do + it 'creates shards based on the config file' do + expect(described_class.all).to be_empty + + stub_storage_settings(foo: {}, bar: {}, baz: {}) + + described_class.populate! + + expect(described_class.all.map(&:name)).to match_array(%w[default foo bar baz]) + end + end + + describe '.by_name' do + let(:default_shard) { described_class.find_by(name: 'default') } + + before do + described_class.populate! + end + + it 'returns an existing shard' do + expect(described_class.by_name('default')).to eq(default_shard) + end + + it 'creates a new shard' do + result = described_class.by_name('foo') + + expect(result).not_to eq(default_shard) + expect(result.name).to eq('foo') + end + + it 'retries if creation races' do + expect(described_class) + .to receive(:find_or_create_by) + .with(name: 'default') + .and_raise(ActiveRecord::RecordNotUnique, 'fail') + .once + + expect(described_class) + .to receive(:find_or_create_by) + .with(name: 'default') + .and_call_original + + expect(described_class.by_name('default')).to eq(default_shard) + end + end +end diff --git a/spec/models/upload_spec.rb b/spec/models/upload_spec.rb index 36b8e5d304f..3c89e99abf0 100644 --- a/spec/models/upload_spec.rb +++ b/spec/models/upload_spec.rb @@ -91,7 +91,7 @@ describe Upload do .to change { upload.checksum }.from(nil).to(expected) end - it 'sets `checksum` to nil for a non-existant file' do + it 'sets `checksum` to nil for a non-existent file' do expect(upload).to receive(:exist?).and_return(false) checksum = Digest::SHA256.file(__FILE__).hexdigest diff --git a/spec/models/user_preference_spec.rb b/spec/models/user_preference_spec.rb index 64d9d9a78b4..2898613545c 100644 --- a/spec/models/user_preference_spec.rb +++ b/spec/models/user_preference_spec.rb @@ -6,22 +6,43 @@ describe UserPreference do describe '#set_notes_filter' do let(:issuable) { build_stubbed(:issue) } let(:user_preference) { create(:user_preference) } - let(:only_comments) { described_class::NOTES_FILTERS[:only_comments] } - it 'returns updated discussion filter' do - filter_name = - user_preference.set_notes_filter(only_comments, issuable) + shared_examples 'setting system notes' do + it 'returns updated discussion filter' do + filter_name = + user_preference.set_notes_filter(filter, issuable) + + expect(filter_name).to eq(filter) + end + + it 'updates discussion filter for issuable class' do + user_preference.set_notes_filter(filter, issuable) + + expect(user_preference.reload.issue_notes_filter).to eq(filter) + end + end + + context 'when filter is set to all notes' do + let(:filter) { described_class::NOTES_FILTERS[:all_notes] } + + it_behaves_like 'setting system notes' + end + + context 'when filter is set to only comments' do + let(:filter) { described_class::NOTES_FILTERS[:only_comments] } - expect(filter_name).to eq(only_comments) + it_behaves_like 'setting system notes' end - it 'updates discussion filter for issuable class' do - user_preference.set_notes_filter(only_comments, issuable) + context 'when filter is set to only activity' do + let(:filter) { described_class::NOTES_FILTERS[:only_activity] } - expect(user_preference.reload.issue_notes_filter).to eq(only_comments) + it_behaves_like 'setting system notes' end context 'when notes_filter parameter is invalid' do + let(:only_comments) { described_class::NOTES_FILTERS[:only_comments] } + it 'returns the current notes filter' do user_preference.set_notes_filter(only_comments, issuable) diff --git a/spec/presenters/ci/build_presenter_spec.rb b/spec/presenters/ci/build_presenter_spec.rb index d7992f0a4a9..676835b3880 100644 --- a/spec/presenters/ci/build_presenter_spec.rb +++ b/spec/presenters/ci/build_presenter_spec.rb @@ -267,7 +267,7 @@ describe Ci::BuildPresenter do let(:build) { create(:ci_build, :failed, :script_failure) } context 'when is a script or missing dependency failure' do - let(:failure_reasons) { %w(script_failure missing_dependency_failure) } + let(:failure_reasons) { %w(script_failure missing_dependency_failure archived_failure) } it 'should return false' do failure_reasons.each do |failure_reason| diff --git a/spec/presenters/clusterable_presenter_spec.rb b/spec/presenters/clusterable_presenter_spec.rb new file mode 100644 index 00000000000..4f4ae5e07c5 --- /dev/null +++ b/spec/presenters/clusterable_presenter_spec.rb @@ -0,0 +1,17 @@ +# frozen_string_literal: true + +require 'spec_helper' + +describe ClusterablePresenter do + include Gitlab::Routing.url_helpers + + describe '.fabricate' do + let(:project) { create(:project) } + + subject { described_class.fabricate(project) } + + it 'creates an object from a descendant presenter' do + expect(subject).to be_kind_of(ProjectClusterablePresenter) + end + end +end diff --git a/spec/presenters/clusters/cluster_presenter_spec.rb b/spec/presenters/clusters/cluster_presenter_spec.rb index e96dbfb73c0..7af181f37d5 100644 --- a/spec/presenters/clusters/cluster_presenter_spec.rb +++ b/spec/presenters/clusters/cluster_presenter_spec.rb @@ -1,7 +1,9 @@ require 'spec_helper' describe Clusters::ClusterPresenter do - let(:cluster) { create(:cluster, :provided_by_gcp) } + include Gitlab::Routing.url_helpers + + let(:cluster) { create(:cluster, :provided_by_gcp, :project) } subject(:presenter) do described_class.new(cluster) @@ -71,4 +73,14 @@ describe Clusters::ClusterPresenter do it { is_expected.to eq(false) } end end + + describe '#show_path' do + subject { described_class.new(cluster).show_path } + + context 'project_type cluster' do + let(:project) { cluster.project } + + it { is_expected.to eq(project_cluster_path(project, cluster)) } + end + end end diff --git a/spec/presenters/project_clusterable_presenter_spec.rb b/spec/presenters/project_clusterable_presenter_spec.rb new file mode 100644 index 00000000000..c50d90ae1e8 --- /dev/null +++ b/spec/presenters/project_clusterable_presenter_spec.rb @@ -0,0 +1,77 @@ +# frozen_string_literal: true + +require 'spec_helper' + +describe ProjectClusterablePresenter do + include Gitlab::Routing.url_helpers + + let(:presenter) { described_class.new(project) } + let(:project) { create(:project) } + let(:cluster) { create(:cluster, :provided_by_gcp, projects: [project]) } + + describe '#can_create_cluster?' do + let(:user) { create(:user) } + + subject { presenter.can_create_cluster? } + + before do + allow(presenter).to receive(:current_user).and_return(user) + end + + context 'when user can create' do + before do + project.add_maintainer(user) + end + + it { is_expected.to be_truthy } + end + + context 'when user cannot create' do + it { is_expected.to be_falsey } + end + end + + describe '#index_path' do + subject { presenter.index_path } + + it { is_expected.to eq(project_clusters_path(project)) } + end + + describe '#new_path' do + subject { presenter.new_path } + + it { is_expected.to eq(new_project_cluster_path(project)) } + end + + describe '#create_user_clusters_path' do + subject { presenter.create_user_clusters_path } + + it { is_expected.to eq(create_user_project_clusters_path(project)) } + end + + describe '#create_gcp_clusters_path' do + subject { presenter.create_gcp_clusters_path } + + it { is_expected.to eq(create_gcp_project_clusters_path(project)) } + end + + describe '#cluster_status_cluster_path' do + subject { presenter.cluster_status_cluster_path(cluster) } + + it { is_expected.to eq(cluster_status_project_cluster_path(project, cluster)) } + end + + describe '#install_applications_cluster_path' do + let(:application) { :helm } + + subject { presenter.install_applications_cluster_path(cluster, application) } + + it { is_expected.to eq(install_applications_project_cluster_path(project, cluster, application)) } + end + + describe '#cluster_path' do + subject { presenter.cluster_path(cluster) } + + it { is_expected.to eq(project_cluster_path(project, cluster)) } + end +end diff --git a/spec/presenters/project_presenter_spec.rb b/spec/presenters/project_presenter_spec.rb index 3eb2f149311..7b0192fa9c8 100644 --- a/spec/presenters/project_presenter_spec.rb +++ b/spec/presenters/project_presenter_spec.rb @@ -239,7 +239,7 @@ describe ProjectPresenter do expect(presenter.new_file_anchor_data).to have_attributes(enabled: false, label: "New file", link: presenter.project_new_blob_path(project, 'master'), - class_modifier: 'new') + class_modifier: 'success') end it 'returns nil if user cannot push' do diff --git a/spec/requests/api/commits_spec.rb b/spec/requests/api/commits_spec.rb index 98399471f9a..2963dea634a 100644 --- a/spec/requests/api/commits_spec.rb +++ b/spec/requests/api/commits_spec.rb @@ -565,7 +565,7 @@ describe API::Commits do } end - it 'are commited as one in project repo' do + it 'are committed as one in project repo' do post api(url, user), valid_mo_params expect(response).to have_gitlab_http_status(201) diff --git a/spec/requests/api/deployments_spec.rb b/spec/requests/api/deployments_spec.rb index 61ae053cea7..3dac7225b7a 100644 --- a/spec/requests/api/deployments_spec.rb +++ b/spec/requests/api/deployments_spec.rb @@ -10,9 +10,9 @@ describe API::Deployments do describe 'GET /projects/:id/deployments' do let(:project) { create(:project) } - let!(:deployment_1) { create(:deployment, project: project, iid: 11, ref: 'master', created_at: Time.now) } - let!(:deployment_2) { create(:deployment, project: project, iid: 12, ref: 'feature', created_at: 1.day.ago) } - let!(:deployment_3) { create(:deployment, project: project, iid: 8, ref: 'feature', created_at: 2.days.ago) } + let!(:deployment_1) { create(:deployment, :success, project: project, iid: 11, ref: 'master', created_at: Time.now) } + let!(:deployment_2) { create(:deployment, :success, project: project, iid: 12, ref: 'feature', created_at: 1.day.ago) } + let!(:deployment_3) { create(:deployment, :success, project: project, iid: 8, ref: 'patch', created_at: 2.days.ago) } context 'as member of the project' do it 'returns projects deployments sorted by id asc' do @@ -53,8 +53,8 @@ describe API::Deployments do 'id' | 'desc' | [:deployment_3, :deployment_2, :deployment_1] 'iid' | 'asc' | [:deployment_3, :deployment_1, :deployment_2] 'iid' | 'desc' | [:deployment_2, :deployment_1, :deployment_3] - 'ref' | 'asc' | [:deployment_2, :deployment_3, :deployment_1] - 'ref' | 'desc' | [:deployment_1, :deployment_2, :deployment_3] + 'ref' | 'asc' | [:deployment_2, :deployment_1, :deployment_3] + 'ref' | 'desc' | [:deployment_3, :deployment_1, :deployment_2] end with_them do @@ -76,7 +76,7 @@ describe API::Deployments do describe 'GET /projects/:id/deployments/:deployment_id' do let(:project) { deployment.environment.project } - let!(:deployment) { create(:deployment) } + let!(:deployment) { create(:deployment, :success) } context 'as a member of the project' do it 'returns the projects deployment' do diff --git a/spec/requests/api/issues_spec.rb b/spec/requests/api/issues_spec.rb index 9cda39a569b..3d532dd83c7 100644 --- a/spec/requests/api/issues_spec.rb +++ b/spec/requests/api/issues_spec.rb @@ -55,8 +55,8 @@ describe API::Issues do end let!(:note) { create(:note_on_issue, author: user, project: project, noteable: issue) } - let(:no_milestone_title) { URI.escape(Milestone::None.title) } - let(:any_milestone_title) { URI.escape(Milestone::Any.title) } + let(:no_milestone_title) { "None" } + let(:any_milestone_title) { "Any" } before(:all) do project.add_reporter(user) @@ -196,14 +196,24 @@ describe API::Issues do expect_paginated_array_response(size: 3) end - it 'returns issues reacted by the authenticated user by the given emoji' do + it 'returns issues reacted by the authenticated user' do issue2 = create(:issue, project: project, author: user, assignees: [user]) - award_emoji = create(:award_emoji, awardable: issue2, user: user2, name: 'star') + create(:award_emoji, awardable: issue2, user: user2, name: 'star') - get api('/issues', user2), my_reaction_emoji: award_emoji.name, scope: 'all' + create(:award_emoji, awardable: issue, user: user2, name: 'thumbsup') - expect_paginated_array_response(size: 1) - expect(first_issue['id']).to eq(issue2.id) + get api('/issues', user2), my_reaction_emoji: 'Any', scope: 'all' + + expect_paginated_array_response(size: 2) + end + + it 'returns issues not reacted by the authenticated user' do + issue2 = create(:issue, project: project, author: user, assignees: [user]) + create(:award_emoji, awardable: issue2, user: user2, name: 'star') + + get api('/issues', user2), my_reaction_emoji: 'None', scope: 'all' + + expect_paginated_array_response(size: 2) end it 'returns issues matching given search string for title' do @@ -1791,6 +1801,74 @@ describe API::Issues do end end + describe 'GET :id/issues/:issue_iid/related_merge_requests' do + def get_related_merge_requests(project_id, issue_iid, user = nil) + get api("/projects/#{project_id}/issues/#{issue_iid}/related_merge_requests", user) + end + + def create_referencing_mr(user, project, issue) + attributes = { + author: user, + source_project: project, + target_project: project, + source_branch: "master", + target_branch: "test", + description: "See #{issue.to_reference}" + } + create(:merge_request, attributes).tap do |merge_request| + create(:note, :system, project: project, noteable: issue, author: user, note: merge_request.to_reference(full: true)) + end + end + + let!(:related_mr) { create_referencing_mr(user, project, issue) } + + context 'when unauthenticated' do + it 'return list of referenced merge requests from issue' do + get_related_merge_requests(project.id, issue.iid) + + expect_paginated_array_response(size: 1) + end + + it 'renders 404 if project is not visible' do + private_project = create(:project, :private) + private_issue = create(:issue, project: private_project) + create_referencing_mr(user, private_project, private_issue) + + get_related_merge_requests(private_project.id, private_issue.iid) + + expect(response).to have_gitlab_http_status(404) + end + end + + it 'returns merge requests that mentioned a issue' do + create(:merge_request, + :simple, + author: user, + source_project: project, + target_project: project, + description: "Some description") + + get_related_merge_requests(project.id, issue.iid, user) + + expect_paginated_array_response(size: 1) + expect(json_response.first['id']).to eq(related_mr.id) + end + + context 'no merge request mentioned a issue' do + it 'returns empty array' do + get_related_merge_requests(project.id, closed_issue.iid, user) + + expect_paginated_array_response(size: 0) + end + end + + it "returns 404 when issue doesn't exists" do + get_related_merge_requests(project.id, 999999, user) + + expect(response).to have_gitlab_http_status(404) + end + end + describe "GET /projects/:id/issues/:issue_iid/user_agent_detail" do let!(:user_agent_detail) { create(:user_agent_detail, subject: issue) } diff --git a/spec/serializers/build_action_entity_spec.rb b/spec/serializers/build_action_entity_spec.rb index 9e2bee2ee60..ea88951ebc6 100644 --- a/spec/serializers/build_action_entity_spec.rb +++ b/spec/serializers/build_action_entity_spec.rb @@ -26,6 +26,10 @@ describe BuildActionEntity do context 'when job is scheduled' do let(:job) { create(:ci_build, :scheduled) } + it 'returns scheduled' do + expect(subject[:scheduled]).to be_truthy + end + it 'returns scheduled_at' do expect(subject[:scheduled_at]).to eq(job.scheduled_at) end diff --git a/spec/serializers/deployment_entity_spec.rb b/spec/serializers/deployment_entity_spec.rb index 522c92ce295..8793a762f9d 100644 --- a/spec/serializers/deployment_entity_spec.rb +++ b/spec/serializers/deployment_entity_spec.rb @@ -22,4 +22,26 @@ describe DeploymentEntity do it 'exposes creation date' do expect(subject).to include(:created_at) end + + describe 'scheduled_actions' do + let(:project) { create(:project, :repository) } + let(:pipeline) { create(:ci_pipeline, project: project, user: user) } + let(:build) { create(:ci_build, :success, pipeline: pipeline) } + let(:deployment) { create(:deployment, deployable: build) } + + context 'when the same pipeline has a scheduled action' do + let(:other_build) { create(:ci_build, :schedulable, :success, pipeline: pipeline, name: 'other build') } + let!(:other_deployment) { create(:deployment, deployable: other_build) } + + it 'returns other scheduled actions' do + expect(subject[:scheduled_actions][0][:name]).to eq 'other build' + end + end + + context 'when the same pipeline does not have a scheduled action' do + it 'does not return other actions' do + expect(subject[:scheduled_actions]).to be_empty + end + end + end end diff --git a/spec/serializers/environment_serializer_spec.rb b/spec/serializers/environment_serializer_spec.rb index 0f0ab5ac796..87493a28d1f 100644 --- a/spec/serializers/environment_serializer_spec.rb +++ b/spec/serializers/environment_serializer_spec.rb @@ -14,7 +14,8 @@ describe EnvironmentSerializer do let(:project) { create(:project, :repository) } let(:deployable) { create(:ci_build) } let(:deployment) do - create(:deployment, deployable: deployable, + create(:deployment, :success, + deployable: deployable, user: user, project: project, sha: project.commit.id) diff --git a/spec/serializers/environment_status_entity_spec.rb b/spec/serializers/environment_status_entity_spec.rb index 1b4d8b70aa6..962ec919092 100644 --- a/spec/serializers/environment_status_entity_spec.rb +++ b/spec/serializers/environment_status_entity_spec.rb @@ -4,8 +4,8 @@ describe EnvironmentStatusEntity do let(:user) { create(:user) } let(:request) { double('request') } - let(:deployment) { create(:deployment, :review_app) } - let(:environment) { deployment.environment} + let(:deployment) { create(:deployment, :succeed, :review_app) } + let(:environment) { deployment.environment } let(:project) { deployment.project } let(:merge_request) { create(:merge_request, :deployed_review_app, deployment: deployment) } diff --git a/spec/serializers/job_entity_spec.rb b/spec/serializers/job_entity_spec.rb index 5fc27da4906..851b41a7f7e 100644 --- a/spec/serializers/job_entity_spec.rb +++ b/spec/serializers/job_entity_spec.rb @@ -117,6 +117,7 @@ describe JobEntity do end it 'contains scheduled_at' do + expect(subject[:scheduled]).to be_truthy expect(subject[:scheduled_at]).to eq(job.scheduled_at) end end diff --git a/spec/services/ci/process_build_service_spec.rb b/spec/services/ci/process_build_service_spec.rb index 9a53b32394d..704685417bb 100644 --- a/spec/services/ci/process_build_service_spec.rb +++ b/spec/services/ci/process_build_service_spec.rb @@ -98,47 +98,19 @@ describe Ci::ProcessBuildService, '#execute' do let(:build) { create(:ci_build, :created, :schedulable, user: user, project: project) } - context 'when ci_enable_scheduled_build is enabled' do - before do - stub_feature_flags(ci_enable_scheduled_build: true) - end - - context 'when current status is success' do - let(:current_status) { 'success' } - - it 'changes the build status' do - expect { subject }.to change { build.status }.to('scheduled') - end - end - - context 'when current status is failed' do - let(:current_status) { 'failed' } + context 'when current status is success' do + let(:current_status) { 'success' } - it 'does not change the build status' do - expect { subject }.to change { build.status }.to('skipped') - end + it 'changes the build status' do + expect { subject }.to change { build.status }.to('scheduled') end end - context 'when ci_enable_scheduled_build is disabled' do - before do - stub_feature_flags(ci_enable_scheduled_build: false) - end - - context 'when current status is success' do - let(:current_status) { 'success' } - - it 'changes the build status' do - expect { subject }.to change { build.status }.to('manual') - end - end - - context 'when current status is failed' do - let(:current_status) { 'failed' } + context 'when current status is failed' do + let(:current_status) { 'failed' } - it 'does not change the build status' do - expect { subject }.to change { build.status }.to('skipped') - end + it 'does not change the build status' do + expect { subject }.to change { build.status }.to('skipped') end end end diff --git a/spec/services/ci/register_job_service_spec.rb b/spec/services/ci/register_job_service_spec.rb index a6565709641..56e2a405bcd 100644 --- a/spec/services/ci/register_job_service_spec.rb +++ b/spec/services/ci/register_job_service_spec.rb @@ -478,6 +478,20 @@ module Ci it_behaves_like 'validation is not active' end end + + context 'when build is degenerated' do + let!(:pending_job) { create(:ci_build, :pending, :degenerated, pipeline: pipeline) } + + subject { execute(specific_runner, {}) } + + it 'does not pick the build and drops the build' do + expect(subject).to be_nil + + pending_job.reload + expect(pending_job).to be_failed + expect(pending_job).to be_archived_failure + end + end end describe '#register_success' do diff --git a/spec/services/ci/retry_build_service_spec.rb b/spec/services/ci/retry_build_service_spec.rb index 368abded448..e779675744c 100644 --- a/spec/services/ci/retry_build_service_spec.rb +++ b/spec/services/ci/retry_build_service_spec.rb @@ -32,7 +32,7 @@ describe Ci::RetryBuildService do IGNORE_ACCESSORS = %i[type lock_version target_url base_tags trace_sections - commit_id deployments erased_by_id last_deployment project_id + commit_id deployment erased_by_id project_id runner_id tag_taggings taggings tags trigger_request_id user_id auto_canceled_by_id retried failure_reason artifacts_file_store artifacts_metadata_store diff --git a/spec/services/ci/run_scheduled_build_service_spec.rb b/spec/services/ci/run_scheduled_build_service_spec.rb index 2c921dac238..be2aad33ef4 100644 --- a/spec/services/ci/run_scheduled_build_service_spec.rb +++ b/spec/services/ci/run_scheduled_build_service_spec.rb @@ -7,10 +7,6 @@ describe Ci::RunScheduledBuildService do subject { described_class.new(project, user).execute(build) } - before do - stub_feature_flags(ci_enable_scheduled_build: true) - end - context 'when user can update build' do before do project.add_developer(user) diff --git a/spec/services/clusters/create_service_spec.rb b/spec/services/clusters/create_service_spec.rb index 3959295c13e..274880f2c49 100644 --- a/spec/services/clusters/create_service_spec.rb +++ b/spec/services/clusters/create_service_spec.rb @@ -5,18 +5,43 @@ describe Clusters::CreateService do let(:project) { create(:project) } let(:user) { create(:user) } - subject { described_class.new(user, params).execute(project: project, access_token: access_token) } + subject { described_class.new(user, params).execute(access_token: access_token) } context 'when provider is gcp' do context 'when project has no clusters' do context 'when correct params' do - include_context 'valid cluster create params' + let(:params) do + { + name: 'test-cluster', + provider_type: :gcp, + provider_gcp_attributes: { + gcp_project_id: 'gcp-project', + zone: 'us-central1-a', + num_nodes: 1, + machine_type: 'machine_type-a', + legacy_abac: 'true' + }, + clusterable: project + } + end include_examples 'create cluster service success' end context 'when invalid params' do - include_context 'invalid cluster create params' + let(:params) do + { + name: 'test-cluster', + provider_type: :gcp, + provider_gcp_attributes: { + gcp_project_id: '!!!!!!!', + zone: 'us-central1-a', + num_nodes: 1, + machine_type: 'machine_type-a' + }, + clusterable: project + } + end include_examples 'create cluster service error' end diff --git a/spec/services/clusters/gcp/fetch_operation_service_spec.rb b/spec/services/clusters/gcp/fetch_operation_service_spec.rb index e2fa93904c5..55f123ee786 100644 --- a/spec/services/clusters/gcp/fetch_operation_service_spec.rb +++ b/spec/services/clusters/gcp/fetch_operation_service_spec.rb @@ -24,7 +24,7 @@ describe Clusters::Gcp::FetchOperationService do end end - context 'when suceeded to fetch operation' do + context 'when succeeded to fetch operation' do before do stub_cloud_platform_get_zone_operation(gcp_project_id, zone, operation_id) end diff --git a/spec/services/clusters/gcp/finalize_creation_service_spec.rb b/spec/services/clusters/gcp/finalize_creation_service_spec.rb index 0f484222228..7fbb6cf2cf5 100644 --- a/spec/services/clusters/gcp/finalize_creation_service_spec.rb +++ b/spec/services/clusters/gcp/finalize_creation_service_spec.rb @@ -1,156 +1,176 @@ +# frozen_string_literal: true + require 'spec_helper' -describe Clusters::Gcp::FinalizeCreationService do +describe Clusters::Gcp::FinalizeCreationService, '#execute' do include GoogleApi::CloudPlatformHelpers include KubernetesHelpers - describe '#execute' do - let(:cluster) { create(:cluster, :project, :providing_by_gcp) } - let(:provider) { cluster.provider } - let(:platform) { cluster.platform } - let(:gcp_project_id) { provider.gcp_project_id } - let(:zone) { provider.zone } - let(:cluster_name) { cluster.name } + let(:cluster) { create(:cluster, :project, :providing_by_gcp) } + let(:provider) { cluster.provider } + let(:platform) { cluster.platform } + let(:endpoint) { '111.111.111.111' } + let(:api_url) { 'https://' + endpoint } + let(:username) { 'sample-username' } + let(:password) { 'sample-password' } + let(:secret_name) { 'gitlab-token' } + let(:token) { 'sample-token' } + let(:namespace) { "#{cluster.project.path}-#{cluster.project.id}" } - subject { described_class.new.execute(provider) } + subject { described_class.new.execute(provider) } - shared_examples 'success' do - it 'configures provider and kubernetes' do - subject + shared_examples 'success' do + it 'configures provider and kubernetes' do + subject - expect(provider).to be_created - end + expect(provider).to be_created end - shared_examples 'error' do - it 'sets an error to provider object' do - subject + it 'properly configures database models' do + subject - expect(provider.reload).to be_errored - end + cluster.reload + + expect(provider.endpoint).to eq(endpoint) + expect(platform.api_url).to eq(api_url) + expect(platform.ca_cert).to eq(Base64.decode64(load_sample_cert)) + expect(platform.username).to eq(username) + expect(platform.password).to eq(password) + expect(platform.token).to eq(token) + end + + it 'creates kubernetes namespace model' do + subject + + kubernetes_namespace = cluster.reload.kubernetes_namespace + expect(kubernetes_namespace).to be_persisted + expect(kubernetes_namespace.namespace).to eq(namespace) + expect(kubernetes_namespace.service_account_name).to eq("#{namespace}-service-account") + expect(kubernetes_namespace.service_account_token).to be_present end + end + + shared_examples 'error' do + it 'sets an error to provider object' do + subject - context 'when suceeded to fetch gke cluster info' do - let(:endpoint) { '111.111.111.111' } - let(:api_url) { 'https://' + endpoint } - let(:username) { 'sample-username' } - let(:password) { 'sample-password' } - let(:secret_name) { 'gitlab-token' } + expect(provider.reload).to be_errored + end + end + shared_examples 'kubernetes information not successfully fetched' do + context 'when failed to fetch gke cluster info' do before do - stub_cloud_platform_get_zone_cluster( - gcp_project_id, zone, cluster_name, - { - endpoint: endpoint, - username: username, - password: password - } - ) + stub_cloud_platform_get_zone_cluster_error(provider.gcp_project_id, provider.zone, cluster.name) end - context 'service account and token created' do - before do - stub_kubeclient_discover(api_url) - stub_kubeclient_create_service_account(api_url) - stub_kubeclient_create_secret(api_url) - end - - shared_context 'kubernetes token successfully fetched' do - let(:token) { 'sample-token' } - - before do - stub_kubeclient_get_secret( - api_url, - { - metadata_name: secret_name, - token: Base64.encode64(token) - } ) - end - end - - context 'provider legacy_abac is enabled' do - include_context 'kubernetes token successfully fetched' - - it_behaves_like 'success' - - it 'properly configures database models' do - subject - - cluster.reload - - expect(provider.endpoint).to eq(endpoint) - expect(platform.api_url).to eq(api_url) - expect(platform.ca_cert).to eq(Base64.decode64(load_sample_cert)) - expect(platform.username).to eq(username) - expect(platform.password).to eq(password) - expect(platform).to be_abac - expect(platform.authorization_type).to eq('abac') - expect(platform.token).to eq(token) - end - end - - context 'provider legacy_abac is disabled' do - before do - provider.legacy_abac = false - end - - include_context 'kubernetes token successfully fetched' - - context 'cluster role binding created' do - before do - stub_kubeclient_create_cluster_role_binding(api_url) - end - - it_behaves_like 'success' - - it 'properly configures database models' do - subject - - cluster.reload - - expect(provider.endpoint).to eq(endpoint) - expect(platform.api_url).to eq(api_url) - expect(platform.ca_cert).to eq(Base64.decode64(load_sample_cert)) - expect(platform.username).to eq(username) - expect(platform.password).to eq(password) - expect(platform).to be_rbac - expect(platform.token).to eq(token) - end - end - end - - context 'when token is empty' do - before do - stub_kubeclient_get_secret(api_url, token: '', metadata_name: secret_name) - end - - it_behaves_like 'error' - end - - context 'when failed to fetch kubernetes token' do - before do - stub_kubeclient_get_secret_error(api_url, secret_name) - end - - it_behaves_like 'error' - end - - context 'when service account fails to create' do - before do - stub_kubeclient_create_service_account_error(api_url) - end - - it_behaves_like 'error' - end + it_behaves_like 'error' + end + + context 'when token is empty' do + let(:token) { '' } + + it_behaves_like 'error' + end + + context 'when failed to fetch kubernetes token' do + before do + stub_kubeclient_get_secret_error(api_url, secret_name, namespace: 'default') end + + it_behaves_like 'error' end - context 'when failed to fetch gke cluster info' do + context 'when service account fails to create' do before do - stub_cloud_platform_get_zone_cluster_error(gcp_project_id, zone, cluster_name) + stub_kubeclient_create_service_account_error(api_url, namespace: 'default') end it_behaves_like 'error' end end + + shared_context 'kubernetes information successfully fetched' do + before do + stub_cloud_platform_get_zone_cluster( + provider.gcp_project_id, provider.zone, cluster.name, + { + endpoint: endpoint, + username: username, + password: password + } + ) + + stub_kubeclient_discover(api_url) + stub_kubeclient_get_namespace(api_url) + stub_kubeclient_create_namespace(api_url) + stub_kubeclient_create_service_account(api_url) + stub_kubeclient_create_secret(api_url) + + stub_kubeclient_get_secret( + api_url, + { + metadata_name: secret_name, + token: Base64.encode64(token), + namespace: 'default' + } + ) + + stub_kubeclient_get_namespace(api_url, namespace: namespace) + stub_kubeclient_create_service_account(api_url, namespace: namespace) + stub_kubeclient_create_secret(api_url, namespace: namespace) + + stub_kubeclient_get_secret( + api_url, + { + metadata_name: "#{namespace}-token", + token: Base64.encode64(token), + namespace: namespace + } + ) + end + end + + context 'With a legacy ABAC cluster' do + before do + provider.legacy_abac = true + end + + include_context 'kubernetes information successfully fetched' + + it_behaves_like 'success' + + it 'uses ABAC authorization type' do + subject + cluster.reload + + expect(platform).to be_abac + expect(platform.authorization_type).to eq('abac') + end + + it_behaves_like 'kubernetes information not successfully fetched' + end + + context 'With an RBAC cluster' do + before do + provider.legacy_abac = false + + stub_kubeclient_create_cluster_role_binding(api_url) + stub_kubeclient_create_role_binding(api_url, namespace: namespace) + end + + include_context 'kubernetes information successfully fetched' + + it_behaves_like 'success' + + it 'uses RBAC authorization type' do + subject + cluster.reload + + expect(platform).to be_rbac + expect(platform.authorization_type).to eq('rbac') + end + + it_behaves_like 'kubernetes information not successfully fetched' + end end diff --git a/spec/services/clusters/gcp/kubernetes/create_or_update_namespace_service_spec.rb b/spec/services/clusters/gcp/kubernetes/create_or_update_namespace_service_spec.rb new file mode 100644 index 00000000000..fc922218ad0 --- /dev/null +++ b/spec/services/clusters/gcp/kubernetes/create_or_update_namespace_service_spec.rb @@ -0,0 +1,115 @@ +# frozen_string_literal: true + +require 'spec_helper' + +describe Clusters::Gcp::Kubernetes::CreateOrUpdateNamespaceService, '#execute' do + include KubernetesHelpers + + let(:cluster) { create(:cluster, :project, :provided_by_gcp) } + let(:platform) { cluster.platform } + let(:api_url) { 'https://kubernetes.example.com' } + let(:project) { cluster.project } + let(:cluster_project) { cluster.cluster_project } + + subject do + described_class.new( + cluster: cluster, + kubernetes_namespace: kubernetes_namespace + ).execute + end + + shared_context 'kubernetes requests' do + before do + stub_kubeclient_discover(api_url) + stub_kubeclient_get_namespace(api_url) + stub_kubeclient_create_service_account(api_url) + stub_kubeclient_create_secret(api_url) + + stub_kubeclient_get_namespace(api_url, namespace: namespace) + stub_kubeclient_create_service_account(api_url, namespace: namespace) + stub_kubeclient_create_secret(api_url, namespace: namespace) + + stub_kubeclient_get_secret( + api_url, + { + metadata_name: "#{namespace}-token", + token: Base64.encode64('sample-token'), + namespace: namespace + } + ) + end + end + + context 'when kubernetes namespace is not persisted' do + let(:namespace) { "#{project.path}-#{project.id}" } + + let(:kubernetes_namespace) do + build(:cluster_kubernetes_namespace, + cluster: cluster, + project: cluster_project.project, + cluster_project: cluster_project) + end + + include_context 'kubernetes requests' + + it 'creates a Clusters::KubernetesNamespace' do + expect do + subject + end.to change(Clusters::KubernetesNamespace, :count).by(1) + end + + it 'creates project service account' do + expect_any_instance_of(Clusters::Gcp::Kubernetes::CreateServiceAccountService).to receive(:execute).once + + subject + end + + it 'configures kubernetes token' do + subject + + kubernetes_namespace.reload + expect(kubernetes_namespace.namespace).to eq(namespace) + expect(kubernetes_namespace.service_account_name).to eq("#{namespace}-service-account") + expect(kubernetes_namespace.encrypted_service_account_token).to be_present + end + end + + context 'when there is a Kubernetes Namespace associated' do + let(:namespace) { 'new-namespace' } + + let(:kubernetes_namespace) do + create(:cluster_kubernetes_namespace, + cluster: cluster, + project: cluster_project.project, + cluster_project: cluster_project) + end + + include_context 'kubernetes requests' + + before do + platform.update_column(:namespace, 'new-namespace') + end + + it 'does not create any Clusters::KubernetesNamespace' do + subject + + expect(cluster.kubernetes_namespace).to eq(kubernetes_namespace) + end + + it 'creates project service account' do + expect_any_instance_of(Clusters::Gcp::Kubernetes::CreateServiceAccountService).to receive(:execute).once + + subject + end + + it 'updates Clusters::KubernetesNamespace' do + subject + + kubernetes_namespace.reload + + expect(kubernetes_namespace.namespace).to eq(namespace) + expect(kubernetes_namespace.service_account_name).to eq("#{namespace}-service-account") + expect(kubernetes_namespace.encrypted_service_account_token).to be_present + end + end +end diff --git a/spec/services/clusters/gcp/kubernetes/create_service_account_service_spec.rb b/spec/services/clusters/gcp/kubernetes/create_service_account_service_spec.rb index b096f1fa4fb..588edff85d4 100644 --- a/spec/services/clusters/gcp/kubernetes/create_service_account_service_spec.rb +++ b/spec/services/clusters/gcp/kubernetes/create_service_account_service_spec.rb @@ -1,94 +1,165 @@ # frozen_string_literal: true - require 'spec_helper' describe Clusters::Gcp::Kubernetes::CreateServiceAccountService do include KubernetesHelpers - let(:service) { described_class.new(kubeclient, rbac: rbac) } + let(:api_url) { 'http://111.111.111.111' } + let(:platform_kubernetes) { cluster.platform_kubernetes } + let(:cluster_project) { cluster.cluster_project } + let(:project) { cluster_project.project } + let(:cluster) do + create(:cluster, + :project, :provided_by_gcp, + platform_kubernetes: create(:cluster_platform_kubernetes, :configured)) + end + + let(:kubeclient) do + Gitlab::Kubernetes::KubeClient.new( + api_url, + auth_options: { username: 'admin', password: 'xxx' } + ) + end - describe '#execute' do - let(:rbac) { false } - let(:api_url) { 'http://111.111.111.111' } - let(:username) { 'admin' } - let(:password) { 'xxx' } + shared_examples 'creates service account and token' do + it 'creates a kubernetes service account' do + subject + + expect(WebMock).to have_requested(:post, api_url + "/api/v1/namespaces/#{namespace}/serviceaccounts").with( + body: hash_including( + kind: 'ServiceAccount', + metadata: { name: service_account_name, namespace: namespace } + ) + ) + end - let(:kubeclient) do - Gitlab::Kubernetes::KubeClient.new( - api_url, - auth_options: { username: username, password: password } + it 'creates a kubernetes secret' do + subject + + expect(WebMock).to have_requested(:post, api_url + "/api/v1/namespaces/#{namespace}/secrets").with( + body: hash_including( + kind: 'Secret', + metadata: { + name: token_name, + namespace: namespace, + annotations: { + 'kubernetes.io/service-account.name': service_account_name + } + }, + type: 'kubernetes.io/service-account-token' + ) ) end + end + + before do + stub_kubeclient_discover(api_url) + stub_kubeclient_get_namespace(api_url, namespace: namespace) + stub_kubeclient_create_service_account(api_url, namespace: namespace ) + stub_kubeclient_create_secret(api_url, namespace: namespace) + end + + describe '.gitlab_creator' do + let(:namespace) { 'default' } + let(:service_account_name) { 'gitlab' } + let(:token_name) { 'gitlab-token' } + + subject { described_class.gitlab_creator(kubeclient, rbac: rbac).execute } + + context 'with ABAC cluster' do + let(:rbac) { false } + + it_behaves_like 'creates service account and token' + end - subject { service.execute } + context 'with RBAC cluster' do + let(:rbac) { true } - context 'when params are correct' do before do - stub_kubeclient_discover(api_url) - stub_kubeclient_create_service_account(api_url) - stub_kubeclient_create_secret(api_url) - end + cluster.platform_kubernetes.rbac! - shared_examples 'creates service account and token' do - it 'creates a kubernetes service account' do - subject + stub_kubeclient_create_cluster_role_binding(api_url) + end - expect(WebMock).to have_requested(:post, api_url + '/api/v1/namespaces/default/serviceaccounts').with( - body: hash_including( - kind: 'ServiceAccount', - metadata: { name: 'gitlab', namespace: 'default' } - ) - ) - end - - it 'creates a kubernetes secret of type ServiceAccountToken' do - subject - - expect(WebMock).to have_requested(:post, api_url + '/api/v1/namespaces/default/secrets').with( - body: hash_including( - kind: 'Secret', - metadata: { - name: 'gitlab-token', - namespace: 'default', - annotations: { - 'kubernetes.io/service-account.name': 'gitlab' - } - }, - type: 'kubernetes.io/service-account-token' - ) + it_behaves_like 'creates service account and token' + + it 'should create a cluster role binding with cluster-admin access' do + subject + + expect(WebMock).to have_requested(:post, api_url + "/apis/rbac.authorization.k8s.io/v1/clusterrolebindings").with( + body: hash_including( + kind: 'ClusterRoleBinding', + metadata: { name: 'gitlab-admin' }, + roleRef: { + apiGroup: 'rbac.authorization.k8s.io', + kind: 'ClusterRole', + name: 'cluster-admin' + }, + subjects: [ + { + kind: 'ServiceAccount', + name: service_account_name, + namespace: namespace + } + ] ) - end + ) end + end + end + + describe '.namespace_creator' do + let(:namespace) { "#{project.path}-#{project.id}" } + let(:service_account_name) { "#{namespace}-service-account" } + let(:token_name) { "#{namespace}-token" } + + subject do + described_class.namespace_creator( + kubeclient, + service_account_name: service_account_name, + service_account_namespace: namespace, + rbac: rbac + ).execute + end + + context 'with ABAC cluster' do + let(:rbac) { false } + + it_behaves_like 'creates service account and token' + end + + context 'With RBAC enabled cluster' do + let(:rbac) { true } + + before do + cluster.platform_kubernetes.rbac! - context 'abac enabled cluster' do - it_behaves_like 'creates service account and token' + stub_kubeclient_create_role_binding(api_url, namespace: namespace) end - context 'rbac enabled cluster' do - let(:rbac) { true } - - before do - stub_kubeclient_create_cluster_role_binding(api_url) - end - - it_behaves_like 'creates service account and token' - - it 'creates a kubernetes cluster role binding' do - subject - - expect(WebMock).to have_requested(:post, api_url + '/apis/rbac.authorization.k8s.io/v1/clusterrolebindings').with( - body: hash_including( - kind: 'ClusterRoleBinding', - metadata: { name: 'gitlab-admin' }, - roleRef: { - apiGroup: 'rbac.authorization.k8s.io', - kind: 'ClusterRole', - name: 'cluster-admin' - }, - subjects: [{ kind: 'ServiceAccount', namespace: 'default', name: 'gitlab' }] - ) + it_behaves_like 'creates service account and token' + + it 'creates a namespaced role binding with edit access' do + subject + + expect(WebMock).to have_requested(:post, api_url + "/apis/rbac.authorization.k8s.io/v1/namespaces/#{namespace}/rolebindings").with( + body: hash_including( + kind: 'RoleBinding', + metadata: { name: "gitlab-#{namespace}", namespace: "#{namespace}" }, + roleRef: { + apiGroup: 'rbac.authorization.k8s.io', + kind: 'ClusterRole', + name: 'edit' + }, + subjects: [ + { + kind: 'ServiceAccount', + name: service_account_name, + namespace: namespace + } + ] ) - end + ) end end end diff --git a/spec/services/clusters/gcp/kubernetes/fetch_kubernetes_token_service_spec.rb b/spec/services/clusters/gcp/kubernetes/fetch_kubernetes_token_service_spec.rb index 2355827fa5a..4d1a6bb7b3a 100644 --- a/spec/services/clusters/gcp/kubernetes/fetch_kubernetes_token_service_spec.rb +++ b/spec/services/clusters/gcp/kubernetes/fetch_kubernetes_token_service_spec.rb @@ -1,56 +1,48 @@ # frozen_string_literal: true -require 'fast_spec_helper' +require 'spec_helper' describe Clusters::Gcp::Kubernetes::FetchKubernetesTokenService do + include KubernetesHelpers + describe '#execute' do let(:api_url) { 'http://111.111.111.111' } - let(:username) { 'admin' } - let(:password) { 'xxx' } + let(:namespace) { 'my-namespace' } + let(:service_account_token_name) { 'gitlab-token' } let(:kubeclient) do Gitlab::Kubernetes::KubeClient.new( api_url, - auth_options: { username: username, password: password } + auth_options: { username: 'admin', password: 'xxx' } ) end - subject { described_class.new(kubeclient).execute } + subject { described_class.new(kubeclient, service_account_token_name, namespace).execute } context 'when params correct' do let(:decoded_token) { 'xxx.token.xxx' } let(:token) { Base64.encode64(decoded_token) } - let(:secret_json) do - { - 'metadata': { - name: 'gitlab-token' - }, - 'data': { - 'token': token - } - } - end - - before do - allow_any_instance_of(Kubeclient::Client) - .to receive(:get_secret).and_return(secret_json) - end - context 'when gitlab-token exists' do - let(:metadata_name) { 'gitlab-token' } + before do + stub_kubeclient_discover(api_url) + stub_kubeclient_get_secret( + api_url, + { + metadata_name: service_account_token_name, + namespace: namespace, + token: token + } + ) + end it { is_expected.to eq(decoded_token) } end context 'when gitlab-token does not exist' do - let(:secret_json) { {} } - - it { is_expected.to be_nil } - end - - context 'when token is nil' do - let(:token) { nil } + before do + allow(kubeclient).to receive(:get_secret).and_raise(Kubeclient::HttpError.new(404, 'Not found', nil)) + end it { is_expected.to be_nil } end diff --git a/spec/services/clusters/gcp/provision_service_spec.rb b/spec/services/clusters/gcp/provision_service_spec.rb index f48afdc83b2..c0bdac40938 100644 --- a/spec/services/clusters/gcp/provision_service_spec.rb +++ b/spec/services/clusters/gcp/provision_service_spec.rb @@ -26,7 +26,7 @@ describe Clusters::Gcp::ProvisionService do end end - context 'when suceeded to request provision' do + context 'when succeeded to request provision' do before do stub_cloud_platform_create_cluster(gcp_project_id, zone) end diff --git a/spec/services/clusters/update_service_spec.rb b/spec/services/clusters/update_service_spec.rb index dcd75b6912d..a1b20c61116 100644 --- a/spec/services/clusters/update_service_spec.rb +++ b/spec/services/clusters/update_service_spec.rb @@ -1,6 +1,8 @@ require 'spec_helper' describe Clusters::UpdateService do + include KubernetesHelpers + describe '#execute' do subject { described_class.new(cluster.user, params).execute(cluster) } @@ -34,6 +36,11 @@ describe Clusters::UpdateService do } end + before do + allow(ClusterPlatformConfigureWorker).to receive(:perform_async) + stub_kubeclient_get_namespace('https://kubernetes.example.com', namespace: 'my-namespace') + end + it 'updates namespace' do is_expected.to eq(true) expect(cluster.platform.namespace).to eq('custom-namespace') diff --git a/spec/services/create_deployment_service_spec.rb b/spec/services/create_deployment_service_spec.rb deleted file mode 100644 index b9bfbb11511..00000000000 --- a/spec/services/create_deployment_service_spec.rb +++ /dev/null @@ -1,335 +0,0 @@ -require 'spec_helper' - -describe CreateDeploymentService do - let(:user) { create(:user) } - let(:options) { nil } - - let(:job) do - create(:ci_build, - ref: 'master', - tag: false, - environment: 'production', - options: { environment: options }) - end - - let(:project) { job.project } - - let!(:environment) do - create(:environment, project: project, name: 'production') - end - - let(:service) { described_class.new(job) } - - before do - allow_any_instance_of(Deployment).to receive(:create_ref) - end - - describe '#execute' do - subject { service.execute } - - context 'when environment exists' do - it 'creates a deployment' do - expect(subject).to be_persisted - end - end - - context 'when environment does not exist' do - let(:environment) {} - - it 'does not create a deployment' do - expect do - expect(subject).to be_nil - end.not_to change { Deployment.count } - end - end - - context 'when start action is defined' do - let(:options) { { action: 'start' } } - - context 'and environment is stopped' do - before do - environment.stop - end - - it 'makes environment available' do - subject - - expect(environment.reload).to be_available - end - - it 'creates a deployment' do - expect(subject).to be_persisted - end - end - end - - context 'when stop action is defined' do - let(:options) { { action: 'stop' } } - - context 'and environment is available' do - before do - environment.start - end - - it 'makes environment stopped' do - subject - - expect(environment.reload).to be_stopped - end - - it 'does not create a deployment' do - expect(subject).to be_nil - end - end - end - - context 'when variables are used' do - let(:options) do - { name: 'review-apps/$CI_COMMIT_REF_NAME', - url: 'http://$CI_COMMIT_REF_NAME.review-apps.gitlab.com' } - end - - before do - environment.update(name: 'review-apps/master') - job.update(environment: 'review-apps/$CI_COMMIT_REF_NAME') - end - - it 'creates a new deployment' do - expect(subject).to be_persisted - end - - it 'does not create a new environment' do - expect { subject }.not_to change { Environment.count } - end - - it 'updates external url' do - subject - - expect(subject.environment.name).to eq('review-apps/master') - expect(subject.environment.external_url).to eq('http://master.review-apps.gitlab.com') - end - end - - context 'when project was removed' do - let(:environment) {} - - before do - job.update(project: nil) - end - - it 'does not create deployment or environment' do - expect { subject }.not_to raise_error - - expect(Environment.count).to be_zero - expect(Deployment.count).to be_zero - end - end - end - - describe '#expanded_environment_url' do - subject { service.send(:expanded_environment_url) } - - context 'when yaml environment uses $CI_COMMIT_REF_NAME' do - let(:job) do - create(:ci_build, - ref: 'master', - options: { environment: { url: 'http://review/$CI_COMMIT_REF_NAME' } }) - end - - it { is_expected.to eq('http://review/master') } - end - - context 'when yaml environment uses $CI_ENVIRONMENT_SLUG' do - let(:job) do - create(:ci_build, - ref: 'master', - environment: 'production', - options: { environment: { url: 'http://review/$CI_ENVIRONMENT_SLUG' } }) - end - - let!(:environment) do - create(:environment, - project: job.project, - name: 'production', - slug: 'prod-slug', - external_url: 'http://review/old') - end - - it { is_expected.to eq('http://review/prod-slug') } - end - - context 'when yaml environment uses yaml_variables containing symbol keys' do - let(:job) do - create(:ci_build, - yaml_variables: [{ key: :APP_HOST, value: 'host' }], - options: { environment: { url: 'http://review/$APP_HOST' } }) - end - - it { is_expected.to eq('http://review/host') } - end - - context 'when yaml environment does not have url' do - let(:job) { create(:ci_build, environment: 'staging') } - - let!(:environment) do - create(:environment, project: job.project, name: job.environment) - end - - it 'returns the external_url from persisted environment' do - is_expected.to be_nil - end - end - end - - describe 'processing of builds' do - shared_examples 'does not create deployment' do - it 'does not create a new deployment' do - expect { subject }.not_to change { Deployment.count } - end - - it 'does not call a service' do - expect_any_instance_of(described_class).not_to receive(:execute) - - subject - end - end - - shared_examples 'creates deployment' do - it 'creates a new deployment' do - expect { subject }.to change { Deployment.count }.by(1) - end - - it 'calls a service' do - expect_any_instance_of(described_class).to receive(:execute) - - subject - end - - it 'is set as deployable' do - subject - - expect(Deployment.last.deployable).to eq(deployable) - end - - it 'updates environment URL' do - subject - - expect(Deployment.last.environment.external_url).not_to be_nil - end - end - - context 'without environment specified' do - let(:job) { create(:ci_build) } - - it_behaves_like 'does not create deployment' do - subject { job.success } - end - end - - context 'when environment is specified' do - let(:deployable) { job } - - let(:options) do - { environment: { name: 'production', url: 'http://gitlab.com' } } - end - - context 'when job succeeds' do - it_behaves_like 'creates deployment' do - subject { job.success } - end - end - - context 'when job fails' do - it_behaves_like 'does not create deployment' do - subject { job.drop } - end - end - - context 'when job is retried' do - it_behaves_like 'creates deployment' do - before do - stub_not_protect_default_branch - - project.add_developer(user) - end - - let(:deployable) { Ci::Build.retry(job, user) } - - subject { deployable.success } - end - end - end - end - - describe "merge request metrics" do - let(:merge_request) { create(:merge_request, target_branch: 'master', source_branch: 'feature', source_project: project) } - - context "while updating the 'first_deployed_to_production_at' time" do - before do - merge_request.metrics.update!(merged_at: Time.now) - end - - context "for merge requests merged before the current deploy" do - it "sets the time if the deploy's environment is 'production'" do - time = Time.now - Timecop.freeze(time) { service.execute } - - expect(merge_request.reload.metrics.first_deployed_to_production_at).to be_like_time(time) - end - - it "doesn't set the time if the deploy's environment is not 'production'" do - job.update(environment: 'staging') - service = described_class.new(job) - service.execute - - expect(merge_request.reload.metrics.first_deployed_to_production_at).to be_nil - end - - it 'does not raise errors if the merge request does not have a metrics record' do - merge_request.metrics.destroy - - expect(merge_request.reload.metrics).to be_nil - expect { service.execute }.not_to raise_error - end - end - - context "for merge requests merged before the previous deploy" do - context "if the 'first_deployed_to_production_at' time is already set" do - it "does not overwrite the older 'first_deployed_to_production_at' time" do - # Previous deploy - time = Time.now - Timecop.freeze(time) { service.execute } - - expect(merge_request.reload.metrics.first_deployed_to_production_at).to be_like_time(time) - - # Current deploy - service = described_class.new(job) - Timecop.freeze(time + 12.hours) { service.execute } - - expect(merge_request.reload.metrics.first_deployed_to_production_at).to be_like_time(time) - end - end - - context "if the 'first_deployed_to_production_at' time is not already set" do - it "does not overwrite the older 'first_deployed_to_production_at' time" do - # Previous deploy - time = 5.minutes.from_now - Timecop.freeze(time) { service.execute } - - expect(merge_request.reload.metrics.merged_at).to be < merge_request.reload.metrics.first_deployed_to_production_at - - merge_request.reload.metrics.update(first_deployed_to_production_at: nil) - - expect(merge_request.reload.metrics.first_deployed_to_production_at).to be_nil - - # Current deploy - service = described_class.new(job) - Timecop.freeze(time + 12.hours) { service.execute } - - expect(merge_request.reload.metrics.first_deployed_to_production_at).to be_nil - end - end - end - end - end -end diff --git a/spec/services/groups/transfer_service_spec.rb b/spec/services/groups/transfer_service_spec.rb index 1289d3ce01f..dd8a1cee074 100644 --- a/spec/services/groups/transfer_service_spec.rb +++ b/spec/services/groups/transfer_service_spec.rb @@ -347,7 +347,7 @@ describe Groups::TransferService, :postgresql do end end - context 'when transfering a group with nested groups and projects' do + context 'when transferring a group with nested groups and projects' do let!(:group) { create(:group, :public) } let!(:project1) { create(:project, :repository, :private, namespace: group) } let!(:subgroup1) { create(:group, :private, parent: group) } diff --git a/spec/services/issues/update_service_spec.rb b/spec/services/issues/update_service_spec.rb index 07aa8449a66..bd519e7f077 100644 --- a/spec/services/issues/update_service_spec.rb +++ b/spec/services/issues/update_service_spec.rb @@ -343,7 +343,42 @@ describe Issues::UpdateService, :mailer do end end - context 'when the milestone change' do + context 'when the milestone is removed' do + let!(:non_subscriber) { create(:user) } + + let!(:subscriber) do + create(:user) do |u| + issue.toggle_subscription(u, project) + project.add_developer(u) + end + end + + it_behaves_like 'system notes for milestones' + + it 'sends notifications for subscribers of changed milestone' do + issue.milestone = create(:milestone) + + issue.save + + perform_enqueued_jobs do + update_issue(milestone_id: "") + end + + should_email(subscriber) + should_not_email(non_subscriber) + end + end + + context 'when the milestone is changed' do + let!(:non_subscriber) { create(:user) } + + let!(:subscriber) do + create(:user) do |u| + issue.toggle_subscription(u, project) + project.add_developer(u) + end + end + it 'marks todos as done' do update_issue(milestone: create(:milestone)) @@ -351,6 +386,15 @@ describe Issues::UpdateService, :mailer do end it_behaves_like 'system notes for milestones' + + it 'sends notifications for subscribers of changed milestone' do + perform_enqueued_jobs do + update_issue(milestone: create(:milestone)) + end + + should_email(subscriber) + should_not_email(non_subscriber) + end end context 'when the labels change' do @@ -374,7 +418,7 @@ describe Issues::UpdateService, :mailer do let!(:non_subscriber) { create(:user) } let!(:subscriber) do - create(:user).tap do |u| + create(:user) do |u| label.toggle_subscription(u, project) project.add_developer(u) end diff --git a/spec/services/merge_requests/reload_diffs_service_spec.rb b/spec/services/merge_requests/reload_diffs_service_spec.rb index 21f369a3818..546c9f277c5 100644 --- a/spec/services/merge_requests/reload_diffs_service_spec.rb +++ b/spec/services/merge_requests/reload_diffs_service_spec.rb @@ -60,6 +60,17 @@ describe MergeRequests::ReloadDiffsService, :use_clean_rails_memory_store_cachin subject.execute end + + it 'avoids N+1 queries', :request_store do + current_user + merge_request + + control_count = ActiveRecord::QueryRecorder.new do + subject.execute + end.count + + expect { subject.execute }.not_to exceed_query_limit(control_count) + end end end end diff --git a/spec/services/merge_requests/update_service_spec.rb b/spec/services/merge_requests/update_service_spec.rb index 55dfab81c26..1b599ba11b6 100644 --- a/spec/services/merge_requests/update_service_spec.rb +++ b/spec/services/merge_requests/update_service_spec.rb @@ -315,7 +315,42 @@ describe MergeRequests::UpdateService, :mailer do end end - context 'when the milestone change' do + context 'when the milestone is removed' do + let!(:non_subscriber) { create(:user) } + + let!(:subscriber) do + create(:user) do |u| + merge_request.toggle_subscription(u, project) + project.add_developer(u) + end + end + + it_behaves_like 'system notes for milestones' + + it 'sends notifications for subscribers of changed milestone' do + merge_request.milestone = create(:milestone) + + merge_request.save + + perform_enqueued_jobs do + update_merge_request(milestone_id: "") + end + + should_email(subscriber) + should_not_email(non_subscriber) + end + end + + context 'when the milestone is changed' do + let!(:non_subscriber) { create(:user) } + + let!(:subscriber) do + create(:user) do |u| + merge_request.toggle_subscription(u, project) + project.add_developer(u) + end + end + it 'marks pending todos as done' do update_merge_request({ milestone: create(:milestone) }) @@ -323,6 +358,15 @@ describe MergeRequests::UpdateService, :mailer do end it_behaves_like 'system notes for milestones' + + it 'sends notifications for subscribers of changed milestone' do + perform_enqueued_jobs do + update_merge_request(milestone: create(:milestone)) + end + + should_email(subscriber) + should_not_email(non_subscriber) + end end context 'when the labels change' do diff --git a/spec/services/notification_service_spec.rb b/spec/services/notification_service_spec.rb index 68a361fa882..2d8da7673dc 100644 --- a/spec/services/notification_service_spec.rb +++ b/spec/services/notification_service_spec.rb @@ -13,6 +13,54 @@ describe NotificationService, :mailer do end end + shared_examples 'altered milestone notification on issue' do + it 'sends the email to the correct people' do + should_email(subscriber_to_new_milestone) + issue.assignees.each do |a| + should_email(a) + end + should_email(@u_watcher) + should_email(@u_guest_watcher) + should_email(@u_participant_mentioned) + should_email(@subscriber) + should_email(@subscribed_participant) + should_email(@watcher_and_subscriber) + should_not_email(@u_guest_custom) + should_not_email(@u_committer) + should_not_email(@unsubscriber) + should_not_email(@u_participating) + should_not_email(@u_lazy_participant) + should_not_email(issue.author) + should_not_email(@u_disabled) + should_not_email(@u_custom_global) + should_not_email(@u_mentioned) + end + end + + shared_examples 'altered milestone notification on merge request' do + it 'sends the email to the correct people' do + should_email(subscriber_to_new_milestone) + merge_request.assignees.each do |a| + should_email(a) + end + should_email(@u_watcher) + should_email(@u_guest_watcher) + should_email(@u_participant_mentioned) + should_email(@subscriber) + should_email(@subscribed_participant) + should_email(@watcher_and_subscriber) + should_not_email(@u_guest_custom) + should_not_email(@u_committer) + should_not_email(@unsubscriber) + should_not_email(@u_participating) + should_not_email(@u_lazy_participant) + should_not_email(merge_request.author) + should_not_email(@u_disabled) + should_not_email(@u_custom_global) + should_not_email(@u_mentioned) + end + end + shared_examples 'notifications for new mentions' do it 'sends no emails when no new mentions are present' do send_notifications @@ -952,6 +1000,96 @@ describe NotificationService, :mailer do end end + describe '#removed_milestone_issue' do + it_behaves_like 'altered milestone notification on issue' do + let(:milestone) { create(:milestone, project: project, issues: [issue]) } + let!(:subscriber_to_new_milestone) { create(:user) { |u| issue.toggle_subscription(u, project) } } + + before do + notification.removed_milestone_issue(issue, issue.author) + end + end + + context 'confidential issues' do + let(:author) { create(:user) } + let(:assignee) { create(:user) } + let(:non_member) { create(:user) } + let(:member) { create(:user) } + let(:guest) { create(:user) } + let(:admin) { create(:admin) } + let(:confidential_issue) { create(:issue, :confidential, project: project, title: 'Confidential issue', author: author, assignees: [assignee]) } + let(:milestone) { create(:milestone, project: project, issues: [confidential_issue]) } + + it "emails subscribers of the issue's milestone that can read the issue" do + project.add_developer(member) + project.add_guest(guest) + + confidential_issue.subscribe(non_member, project) + confidential_issue.subscribe(author, project) + confidential_issue.subscribe(assignee, project) + confidential_issue.subscribe(member, project) + confidential_issue.subscribe(guest, project) + confidential_issue.subscribe(admin, project) + + reset_delivered_emails! + + notification.removed_milestone_issue(confidential_issue, @u_disabled) + + should_not_email(non_member) + should_not_email(guest) + should_email(author) + should_email(assignee) + should_email(member) + should_email(admin) + end + end + end + + describe '#changed_milestone_issue' do + it_behaves_like 'altered milestone notification on issue' do + let(:new_milestone) { create(:milestone, project: project, issues: [issue]) } + let!(:subscriber_to_new_milestone) { create(:user) { |u| issue.toggle_subscription(u, project) } } + + before do + notification.changed_milestone_issue(issue, new_milestone, issue.author) + end + end + + context 'confidential issues' do + let(:author) { create(:user) } + let(:assignee) { create(:user) } + let(:non_member) { create(:user) } + let(:member) { create(:user) } + let(:guest) { create(:user) } + let(:admin) { create(:admin) } + let(:confidential_issue) { create(:issue, :confidential, project: project, title: 'Confidential issue', author: author, assignees: [assignee]) } + let(:new_milestone) { create(:milestone, project: project, issues: [confidential_issue]) } + + it "emails subscribers of the issue's milestone that can read the issue" do + project.add_developer(member) + project.add_guest(guest) + + confidential_issue.subscribe(non_member, project) + confidential_issue.subscribe(author, project) + confidential_issue.subscribe(assignee, project) + confidential_issue.subscribe(member, project) + confidential_issue.subscribe(guest, project) + confidential_issue.subscribe(admin, project) + + reset_delivered_emails! + + notification.changed_milestone_issue(confidential_issue, new_milestone, @u_disabled) + + should_not_email(non_member) + should_not_email(guest) + should_email(author) + should_email(assignee) + should_email(member) + should_email(admin) + end + end + end + describe '#close_issue' do before do update_custom_notification(:close_issue, @u_guest_custom, resource: project) @@ -1304,6 +1442,28 @@ describe NotificationService, :mailer do end end + describe '#removed_milestone_merge_request' do + it_behaves_like 'altered milestone notification on merge request' do + let(:milestone) { create(:milestone, project: project, merge_requests: [merge_request]) } + let!(:subscriber_to_new_milestone) { create(:user) { |u| merge_request.toggle_subscription(u, project) } } + + before do + notification.removed_milestone_merge_request(merge_request, merge_request.author) + end + end + end + + describe '#changed_milestone_merge_request' do + it_behaves_like 'altered milestone notification on merge request' do + let(:new_milestone) { create(:milestone, project: project, merge_requests: [merge_request]) } + let!(:subscriber_to_new_milestone) { create(:user) { |u| merge_request.toggle_subscription(u, project) } } + + before do + notification.changed_milestone_merge_request(merge_request, new_milestone, merge_request.author) + end + end + end + describe '#merge_request_unmergeable' do it "sends email to merge request author" do notification.merge_request_unmergeable(merge_request) diff --git a/spec/services/projects/import_service_spec.rb b/spec/services/projects/import_service_spec.rb index e6ffa2b957b..06f865dc848 100644 --- a/spec/services/projects/import_service_spec.rb +++ b/spec/services/projects/import_service_spec.rb @@ -125,7 +125,7 @@ describe Projects::ImportService do project.import_type = 'bitbucket' end - it 'succeeds if repository import is successfull' do + it 'succeeds if repository import is successful' do expect_any_instance_of(Gitlab::Shell).to receive(:import_repository).and_return(true) expect_any_instance_of(Gitlab::BitbucketImport::Importer).to receive(:execute).and_return(true) expect_any_instance_of(Projects::LfsPointers::LfsImportService).to receive(:execute).and_return({}) diff --git a/spec/services/projects/transfer_service_spec.rb b/spec/services/projects/transfer_service_spec.rb index 1411723fb9e..2e07d4f8013 100644 --- a/spec/services/projects/transfer_service_spec.rb +++ b/spec/services/projects/transfer_service_spec.rb @@ -125,7 +125,7 @@ describe Projects::TransferService do it { expect(project.errors.messages[:new_namespace].first).to eq 'Please select a new namespace for your project.' } end - context 'disallow transfering of project with tags' do + context 'disallow transferring of project with tags' do let(:container_repository) { create(:container_repository) } before do diff --git a/spec/services/quick_actions/interpret_service_spec.rb b/spec/services/quick_actions/interpret_service_spec.rb index 41a170e4f25..e513ee7ae44 100644 --- a/spec/services/quick_actions/interpret_service_spec.rb +++ b/spec/services/quick_actions/interpret_service_spec.rb @@ -315,7 +315,7 @@ describe QuickActions::InterpretService do end shared_examples 'award command' do - it 'toggle award 100 emoji if content containts /award :100:' do + it 'toggle award 100 emoji if content contains /award :100:' do _, updates = service.execute(content, issuable) expect(updates).to eq(emoji_award: "100") @@ -1395,7 +1395,7 @@ describe QuickActions::InterpretService do it 'includes the formatted duration and proper verb' do _, explanations = service.explain(content, issue) - expect(explanations).to eq(['Substracts 2h spent time.']) + expect(explanations).to eq(['Subtracts 2h spent time.']) end end diff --git a/spec/services/update_deployment_service_spec.rb b/spec/services/update_deployment_service_spec.rb new file mode 100644 index 00000000000..3c55dd9659a --- /dev/null +++ b/spec/services/update_deployment_service_spec.rb @@ -0,0 +1,217 @@ +require 'spec_helper' + +describe UpdateDeploymentService do + let(:user) { create(:user) } + let(:options) { { name: 'production' } } + + let(:job) do + create(:ci_build, + ref: 'master', + tag: false, + environment: 'production', + options: { environment: options }, + project: project) + end + + let(:project) { create(:project, :repository) } + let(:environment) { deployment.environment } + let(:deployment) { job.deployment } + let(:service) { described_class.new(deployment) } + + before do + job.success! # Create/Succeed deployment + end + + describe '#execute' do + subject { service.execute } + + let(:store) { Gitlab::EtagCaching::Store.new } + + it 'invalidates the environment etag cache' do + old_value = store.get(environment.etag_cache_key) + + subject + + expect(store.get(environment.etag_cache_key)).not_to eq(old_value) + end + + it 'creates ref' do + expect_any_instance_of(Repository) + .to receive(:create_ref) + .with(deployment.ref, deployment.send(:ref_path)) + + subject + end + + it 'updates merge request metrics' do + expect_any_instance_of(Deployment) + .to receive(:update_merge_request_metrics!) + + subject + end + + context 'when start action is defined' do + let(:options) { { name: 'production', action: 'start' } } + + context 'and environment is stopped' do + before do + environment.stop + end + + it 'makes environment available' do + subject + + expect(environment.reload).to be_available + end + end + end + + context 'when variables are used' do + let(:options) do + { name: 'review-apps/$CI_COMMIT_REF_NAME', + url: 'http://$CI_COMMIT_REF_NAME.review-apps.gitlab.com' } + end + + before do + environment.update(name: 'review-apps/master') + job.update(environment: 'review-apps/$CI_COMMIT_REF_NAME') + end + + it 'does not create a new environment' do + expect { subject }.not_to change { Environment.count } + end + + it 'updates external url' do + subject + + expect(subject.environment.name).to eq('review-apps/master') + expect(subject.environment.external_url).to eq('http://master.review-apps.gitlab.com') + end + end + end + + describe '#expanded_environment_url' do + subject { service.send(:expanded_environment_url) } + + context 'when yaml environment uses $CI_COMMIT_REF_NAME' do + let(:job) do + create(:ci_build, + ref: 'master', + environment: 'production', + project: project, + options: { environment: { name: 'production', url: 'http://review/$CI_COMMIT_REF_NAME' } }) + end + + it { is_expected.to eq('http://review/master') } + end + + context 'when yaml environment uses $CI_ENVIRONMENT_SLUG' do + let(:job) do + create(:ci_build, + ref: 'master', + environment: 'prod-slug', + project: project, + options: { environment: { name: 'prod-slug', url: 'http://review/$CI_ENVIRONMENT_SLUG' } }) + end + + it { is_expected.to eq('http://review/prod-slug') } + end + + context 'when yaml environment uses yaml_variables containing symbol keys' do + let(:job) do + create(:ci_build, + yaml_variables: [{ key: :APP_HOST, value: 'host' }], + environment: 'production', + project: project, + options: { environment: { name: 'production', url: 'http://review/$APP_HOST' } }) + end + + it { is_expected.to eq('http://review/host') } + end + + context 'when yaml environment does not have url' do + let(:job) { create(:ci_build, environment: 'staging', project: project) } + + it 'returns the external_url from persisted environment' do + is_expected.to be_nil + end + end + end + + describe "merge request metrics" do + let(:merge_request) { create(:merge_request, target_branch: 'master', source_branch: 'feature', source_project: project) } + + context "while updating the 'first_deployed_to_production_at' time" do + before do + merge_request.metrics.update!(merged_at: 1.hour.ago) + end + + context "for merge requests merged before the current deploy" do + it "sets the time if the deploy's environment is 'production'" do + service.execute + + expect(merge_request.reload.metrics.first_deployed_to_production_at).to be_like_time(deployment.finished_at) + end + + context 'when job deploys to staging' do + let(:job) do + create(:ci_build, + ref: 'master', + tag: false, + environment: 'staging', + options: { environment: { name: 'staging' } }, + project: project) + end + + it "doesn't set the time if the deploy's environment is not 'production'" do + service.execute + + expect(merge_request.reload.metrics.first_deployed_to_production_at).to be_nil + end + end + + it 'does not raise errors if the merge request does not have a metrics record' do + merge_request.metrics.destroy + + expect(merge_request.reload.metrics).to be_nil + expect { service.execute }.not_to raise_error + end + end + + context "for merge requests merged before the previous deploy" do + context "if the 'first_deployed_to_production_at' time is already set" do + it "does not overwrite the older 'first_deployed_to_production_at' time" do + # Previous deploy + service.execute + + expect(merge_request.reload.metrics.first_deployed_to_production_at).to be_like_time(deployment.finished_at) + + # Current deploy + Timecop.travel(12.hours.from_now) do + service.execute + + expect(merge_request.reload.metrics.first_deployed_to_production_at).to be_like_time(deployment.finished_at) + end + end + end + + context "if the 'first_deployed_to_production_at' time is not already set" do + it "does not overwrite the older 'first_deployed_to_production_at' time" do + # Previous deploy + time = 5.minutes.from_now + Timecop.freeze(time) { service.execute } + + expect(merge_request.reload.metrics.merged_at).to be < merge_request.reload.metrics.first_deployed_to_production_at + + previous_time = merge_request.reload.metrics.first_deployed_to_production_at + + # Current deploy + Timecop.freeze(time + 12.hours) { service.execute } + + expect(merge_request.reload.metrics.first_deployed_to_production_at).to eq(previous_time) + end + end + end + end + end +end diff --git a/spec/support/capybara.rb b/spec/support/capybara.rb index c0ceb0f6605..18a7a392c12 100644 --- a/spec/support/capybara.rb +++ b/spec/support/capybara.rb @@ -100,7 +100,7 @@ RSpec.configure do |config| # capybara/rspec already calls Capybara.reset_sessions! in an `after` hook, # but `block_and_wait_for_requests_complete` is called before it so by - # calling it explicitely here, we prevent any new requests from being fired + # calling it explicitly here, we prevent any new requests from being fired # See https://github.com/teamcapybara/capybara/blob/ffb41cfad620de1961bb49b1562a9fa9b28c0903/lib/capybara/rspec.rb#L20-L25 # We don't reset the session when the example failed, because we need capybara-screenshot to have access to it. Capybara.reset_sessions! unless example.exception diff --git a/spec/support/helpers/cycle_analytics_helpers.rb b/spec/support/helpers/cycle_analytics_helpers.rb index 83035788a56..ecefdc23811 100644 --- a/spec/support/helpers/cycle_analytics_helpers.rb +++ b/spec/support/helpers/cycle_analytics_helpers.rb @@ -85,7 +85,7 @@ module CycleAnalyticsHelpers raise ArgumentError end - CreateDeploymentService.new(dummy_job).execute + dummy_job.success! # State machine automatically update associated deployment/environment record end def dummy_production_job(user, project) @@ -97,7 +97,7 @@ module CycleAnalyticsHelpers end def dummy_pipeline(project) - Ci::Pipeline.new( + create(:ci_pipeline, sha: project.repository.commit('master').sha, ref: 'master', source: :push, @@ -106,9 +106,7 @@ module CycleAnalyticsHelpers end def new_dummy_job(user, project, environment) - project.environments.find_or_create_by(name: environment) - - Ci::Build.new( + create(:ci_build, project: project, user: user, environment: environment, diff --git a/spec/support/helpers/filtered_search_helpers.rb b/spec/support/helpers/filtered_search_helpers.rb index 5f42ff77fb2..6569feec39b 100644 --- a/spec/support/helpers/filtered_search_helpers.rb +++ b/spec/support/helpers/filtered_search_helpers.rb @@ -120,8 +120,12 @@ module FilteredSearchHelpers create_token('Label', label_name, symbol) end - def emoji_token(emoji_name = nil) - { name: 'My-Reaction', emoji_name: emoji_name } + def reaction_token(reaction_name = nil, is_emoji = true) + if is_emoji + { name: 'My-Reaction', emoji_name: reaction_name } + else + create_token('My-Reaction', reaction_name) + end end def default_placeholder diff --git a/spec/support/helpers/project_forks_helper.rb b/spec/support/helpers/project_forks_helper.rb index 6a7132c3093..9a86560da2a 100644 --- a/spec/support/helpers/project_forks_helper.rb +++ b/spec/support/helpers/project_forks_helper.rb @@ -35,7 +35,7 @@ module ProjectForksHelper if create_repository # The call to project.repository.after_import in RepositoryForkWorker does # not reset the @exists variable of this forked_project.repository - # so we have to explicitely call this method to clear the @exists variable. + # so we have to explicitly call this method to clear the @exists variable. # of the instance we're returning here. forked_project.repository.after_import 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 9c9d7ad781e..95e69328080 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 @@ -34,12 +34,24 @@ shared_examples 'issuable notes filter' do expect(user.reload.notes_filter_for(issuable)).to eq(0) end - it 'returns no system note' do + it 'returns only user comments' do user.set_notes_filter(UserPreference::NOTES_FILTERS[:only_comments], issuable) get :discussions, namespace_id: project.namespace, project_id: project, id: issuable.iid + discussions = JSON.parse(response.body) - expect(JSON.parse(response.body).count).to eq(1) + expect(discussions.count).to eq(1) + expect(discussions.first["notes"].first["system"]).to be(false) + end + + it 'returns only activity notes' do + user.set_notes_filter(UserPreference::NOTES_FILTERS[:only_activity], issuable) + + get :discussions, namespace_id: project.namespace, project_id: project, id: issuable.iid + discussions = JSON.parse(response.body) + + expect(discussions.count).to eq(1) + expect(discussions.first["notes"].first["system"]).to be(true) end context 'when filter is set to "only_comments"' do diff --git a/spec/support/shared_examples/helm_generated_script.rb b/spec/support/shared_examples/helm_generated_script.rb index ef9bb7f5533..361d4220c6e 100644 --- a/spec/support/shared_examples/helm_generated_script.rb +++ b/spec/support/shared_examples/helm_generated_script.rb @@ -3,12 +3,6 @@ shared_examples 'helm commands' do let(:helm_setup) do <<~EOS set -eo pipefail - ALPINE_VERSION=$(cat /etc/alpine-release | cut -d '.' -f 1,2) - echo http://mirror.clarkson.edu/alpine/v$ALPINE_VERSION/main >> /etc/apk/repositories - echo http://mirror1.hs-esslingen.de/pub/Mirrors/alpine/v$ALPINE_VERSION/main >> /etc/apk/repositories - apk add -U wget ca-certificates openssl >/dev/null - wget -q -O - https://kubernetes-helm.storage.googleapis.com/helm-v2.7.2-linux-amd64.tar.gz | tar zxC /tmp >/dev/null - mv /tmp/linux-amd64/helm /usr/bin/ EOS end diff --git a/spec/support/shared_examples/services/boards/issues_move_service.rb b/spec/support/shared_examples/services/boards/issues_move_service.rb index 6d29a97c56d..ec44b99d10e 100644 --- a/spec/support/shared_examples/services/boards/issues_move_service.rb +++ b/spec/support/shared_examples/services/boards/issues_move_service.rb @@ -34,7 +34,7 @@ shared_examples 'issues move service' do |group| described_class.new(parent, user, params).execute(issue) issue.reload - expect(issue.labels).to contain_exactly(bug) + expect(issue.labels).to contain_exactly(bug, regression) expect(issue).to be_closed end end diff --git a/spec/support/shared_examples/services/boards/lists_move_service.rb b/spec/support/shared_examples/services/boards/lists_move_service.rb index 07c98cb29b7..2cdb968a45d 100644 --- a/spec/support/shared_examples/services/boards/lists_move_service.rb +++ b/spec/support/shared_examples/services/boards/lists_move_service.rb @@ -14,7 +14,7 @@ shared_examples 'lists move service' do expect(current_list_positions).to eq [0, 1, 2, 3] end - it 'keeps position of lists when new positon is equal to old position' do + it 'keeps position of lists when new position is equal to old position' do service = described_class.new(parent, user, position: planning.position) service.execute(planning) @@ -22,7 +22,7 @@ shared_examples 'lists move service' do expect(current_list_positions).to eq [0, 1, 2, 3] end - it 'keeps position of lists when new positon is negative' do + it 'keeps position of lists when new position is negative' do service = described_class.new(parent, user, position: -1) service.execute(planning) @@ -30,7 +30,7 @@ shared_examples 'lists move service' do expect(current_list_positions).to eq [0, 1, 2, 3] end - it 'keeps position of lists when new positon is equal to number of labels lists' do + it 'keeps position of lists when new position is equal to number of labels lists' do service = described_class.new(parent, user, position: board.lists.label.size) service.execute(planning) @@ -38,7 +38,7 @@ shared_examples 'lists move service' do expect(current_list_positions).to eq [0, 1, 2, 3] end - it 'keeps position of lists when new positon is greater than number of labels lists' do + it 'keeps position of lists when new position is greater than number of labels lists' do service = described_class.new(parent, user, position: board.lists.label.size + 1) service.execute(planning) @@ -46,7 +46,7 @@ shared_examples 'lists move service' do expect(current_list_positions).to eq [0, 1, 2, 3] end - it 'increments position of intermediate lists when new positon is equal to first position' do + it 'increments position of intermediate lists when new position is equal to first position' do service = described_class.new(parent, user, position: 0) service.execute(staging) @@ -54,7 +54,7 @@ shared_examples 'lists move service' do expect(current_list_positions).to eq [1, 2, 3, 0] end - it 'decrements position of intermediate lists when new positon is equal to last position' do + it 'decrements position of intermediate lists when new position is equal to last position' do service = described_class.new(parent, user, position: board.lists.label.last.position) service.execute(planning) diff --git a/spec/tasks/gitlab/backup_rake_spec.rb b/spec/tasks/gitlab/backup_rake_spec.rb index 3ba6caf1337..8c4360d4cf0 100644 --- a/spec/tasks/gitlab/backup_rake_spec.rb +++ b/spec/tasks/gitlab/backup_rake_spec.rb @@ -251,7 +251,7 @@ describe 'gitlab:app namespace rake task' do allow(Gitlab.config.repositories).to receive(:storages).and_return(storages) - # Avoid asking gitaly about the root ref (which will fail beacuse of the + # Avoid asking gitaly about the root ref (which will fail because of the # mocked storages) allow_any_instance_of(Repository).to receive(:empty?).and_return(false) end diff --git a/spec/views/projects/tree/_blob_item.html.haml_spec.rb b/spec/views/projects/tree/_tree_row.html.haml_spec.rb index 6a477c712ff..3353b7665e2 100644 --- a/spec/views/projects/tree/_blob_item.html.haml_spec.rb +++ b/spec/views/projects/tree/_tree_row.html.haml_spec.rb @@ -1,6 +1,6 @@ require 'spec_helper' -describe 'projects/tree/_blob_item' do +describe 'projects/tree/_tree_row' do let(:project) { create(:project, :repository) } let(:repository) { project.repository } let(:blob_item) { Gitlab::Git::Tree.where(repository, SeedRepo::Commit::ID, 'files/ruby').first } @@ -31,10 +31,7 @@ describe 'projects/tree/_blob_item' do end end - def render_partial(blob_item) - render partial: 'projects/tree/blob_item', locals: { - blob_item: blob_item, - type: 'blob' - } + def render_partial(items) + render partial: 'projects/tree/tree_row', collection: [items].flatten end end diff --git a/spec/workers/build_success_worker_spec.rb b/spec/workers/build_success_worker_spec.rb index dba70883130..5eb9709ded9 100644 --- a/spec/workers/build_success_worker_spec.rb +++ b/spec/workers/build_success_worker_spec.rb @@ -2,15 +2,39 @@ require 'spec_helper' describe BuildSuccessWorker do describe '#perform' do + subject { described_class.new.perform(build.id) } + + before do + allow_any_instance_of(Deployment).to receive(:create_ref) + end + context 'when build exists' do - context 'when build belogs to the environment' do - let!(:build) { create(:ci_build, environment: 'production') } + context 'when deployment was not created with the build creation' do # An edge case during the transition period + let!(:build) { create(:ci_build, :deploy_to_production) } + + before do + Deployment.delete_all + build.reload + end - it 'executes deployment service' do - expect_any_instance_of(CreateDeploymentService) - .to receive(:execute) + it 'creates a successful deployment' do + expect(build).not_to be_has_deployment - described_class.new.perform(build.id) + subject + + build.reload + expect(build).to be_has_deployment + expect(build.deployment).to be_success + end + end + + context 'when deployment was created with the build creation' do # Counter part of the above edge case + let!(:build) { create(:ci_build, :deploy_to_production) } + + it 'does not create a new deployment' do + expect(build).to be_has_deployment + + expect { subject }.not_to change { Deployment.count } end end @@ -18,10 +42,22 @@ describe BuildSuccessWorker do let!(:build) { create(:ci_build, project: nil) } it 'does not create deployment' do - expect_any_instance_of(CreateDeploymentService) - .not_to receive(:execute) + subject + + expect(build.reload).not_to be_has_deployment + end + end + + context 'when the build will stop an environment' do + let!(:build) { create(:ci_build, :stop_review_app, environment: environment.name, project: environment.project) } + let(:environment) { create(:environment, state: :available) } + + it 'stops the environment' do + expect(environment).to be_available + + subject - described_class.new.perform(build.id) + expect(environment.reload).to be_stopped end end end diff --git a/spec/workers/cluster_platform_configure_worker_spec.rb b/spec/workers/cluster_platform_configure_worker_spec.rb new file mode 100644 index 00000000000..1a7ad8923f6 --- /dev/null +++ b/spec/workers/cluster_platform_configure_worker_spec.rb @@ -0,0 +1,33 @@ +# frozen_string_literal: true + +require 'spec_helper' + +describe ClusterPlatformConfigureWorker, '#execute' do + context 'when provider type is gcp' do + let(:cluster) { create(:cluster, :project, :provided_by_gcp) } + + it 'configures kubernetes platform' do + expect_any_instance_of(Clusters::Gcp::Kubernetes::CreateOrUpdateNamespaceService).to receive(:execute) + + described_class.new.perform(cluster.id) + end + end + + context 'when provider type is user' do + let(:cluster) { create(:cluster, :project, :provided_by_user) } + + it 'configures kubernetes platform' do + expect_any_instance_of(Clusters::Gcp::Kubernetes::CreateOrUpdateNamespaceService).to receive(:execute) + + described_class.new.perform(cluster.id) + end + end + + context 'when cluster does not exist' do + it 'does not provision a cluster' do + expect_any_instance_of(Clusters::Gcp::Kubernetes::CreateOrUpdateNamespaceService).not_to receive(:execute) + + described_class.new.perform(123) + end + end +end diff --git a/spec/workers/cluster_provision_worker_spec.rb b/spec/workers/cluster_provision_worker_spec.rb index 8054ec11a48..0a2dfef36a4 100644 --- a/spec/workers/cluster_provision_worker_spec.rb +++ b/spec/workers/cluster_provision_worker_spec.rb @@ -14,18 +14,25 @@ describe ClusterProvisionWorker do end context 'when provider type is user' do - let(:cluster) { create(:cluster, provider_type: :user) } + let(:cluster) { create(:cluster, :provided_by_user) } it 'does not provision a cluster' do expect_any_instance_of(Clusters::Gcp::ProvisionService).not_to receive(:execute) described_class.new.perform(cluster.id) end + + it 'configures kubernetes platform' do + expect(ClusterPlatformConfigureWorker).to receive(:perform_async).with(cluster.id) + + described_class.new.perform(cluster.id) + end end context 'when cluster does not exist' do it 'does not provision a cluster' do expect_any_instance_of(Clusters::Gcp::ProvisionService).not_to receive(:execute) + expect(ClusterPlatformConfigureWorker).not_to receive(:perform_async) described_class.new.perform(123) end diff --git a/spec/workers/deployments/success_worker_spec.rb b/spec/workers/deployments/success_worker_spec.rb new file mode 100644 index 00000000000..ba7d45eca01 --- /dev/null +++ b/spec/workers/deployments/success_worker_spec.rb @@ -0,0 +1,36 @@ +require 'spec_helper' + +describe Deployments::SuccessWorker do + subject { described_class.new.perform(deployment&.id) } + + context 'when successful deployment' do + let(:deployment) { create(:deployment, :success) } + + it 'executes UpdateDeploymentService' do + expect(UpdateDeploymentService) + .to receive(:new).with(deployment).and_call_original + + subject + end + end + + context 'when canceled deployment' do + let(:deployment) { create(:deployment, :canceled) } + + it 'does not execute UpdateDeploymentService' do + expect(UpdateDeploymentService).not_to receive(:new) + + subject + end + end + + context 'when deploy record does not exist' do + let(:deployment) { nil } + + it 'does not execute UpdateDeploymentService' do + expect(UpdateDeploymentService).not_to receive(:new) + + subject + end + end +end diff --git a/vendor/licenses.csv b/vendor/licenses.csv index 9083fe076c3..5a7f7c0ebd1 100644 --- a/vendor/licenses.csv +++ b/vendor/licenses.csv @@ -67,7 +67,7 @@ @babel/template,7.1.2,MIT @babel/traverse,7.1.0,MIT @babel/types,7.1.2,MIT -@gitlab-org/gitlab-svgs,1.31.0,MIT +@gitlab/svgs,1.35.0,MIT @gitlab-org/gitlab-ui,1.8.0,MIT @sindresorhus/is,0.7.0,MIT @types/jquery,2.0.48,MIT diff --git a/yarn.lock b/yarn.lock index 0124ee0572d..38e0f9d6201 100644 --- a/yarn.lock +++ b/yarn.lock @@ -621,11 +621,6 @@ resolved "https://registry.yarnpkg.com/@gitlab-org/gitlab-svgs/-/gitlab-svgs-1.32.0.tgz#a65ab7724fa7d55be8e5cc9b2dbe3f0757432fd3" integrity sha512-L3o8dFUd2nSkVZBwh2hCJWzNzADJ3dTBZxamND8NLosZK9/ohNhccmsQOZGyMCUHaOzm4vifaaXkAXh04UtMKA== -"@gitlab-org/gitlab-svgs@^1.33.0": - version "1.33.0" - resolved "https://registry.yarnpkg.com/@gitlab-org/gitlab-svgs/-/gitlab-svgs-1.33.0.tgz#068566e8ee00795f6f09f58236f08e1716f9f04a" - integrity sha512-8ajtUHk6gQ1xosL/CO5IzHSFM/t18hx5pfzQ3cd0VuQXcyR6QKGuXTLwbYdmJDYOw1Etoo5DqDWxPEClHyZpiA== - "@gitlab-org/gitlab-ui@^1.10.0": version "1.10.0" resolved "https://registry.yarnpkg.com/@gitlab-org/gitlab-ui/-/gitlab-ui-1.10.0.tgz#3ac54ecaa25ea558324f0b382c97fcf9e3c4f0a5" @@ -648,6 +643,11 @@ eslint-plugin-promise "^4.0.1" eslint-plugin-vue "^5.0.0-beta.3" +"@gitlab/svgs@^1.35.0": + version "1.35.0" + resolved "https://registry.yarnpkg.com/@gitlab/svgs/-/svgs-1.35.0.tgz#01b6a0948bb3897fbbac9f50ce23c559c514ea0e" + integrity sha512-XKrTniSYKG5U8+8ZqDJqoW8ORahuPBfHrfsC1dHBPvo1xA/QGJxlpUdeqSFw2O19h481ut4yW1dF+OFpIa/mrw== + "@sindresorhus/is@^0.7.0": version "0.7.0" resolved "https://registry.yarnpkg.com/@sindresorhus/is/-/is-0.7.0.tgz#9a06f4f137ee84d7df0460c1fdb1135ffa6c50fd" |