diff options
283 files changed, 4728 insertions, 1802 deletions
diff --git a/.gitlab/CODEOWNERS b/.gitlab/CODEOWNERS index 5b6e5a719fa..7fd32563696 100644 --- a/.gitlab/CODEOWNERS +++ b/.gitlab/CODEOWNERS @@ -13,3 +13,5 @@ db/ @abrandl @NikolayS # Feature specific owners /ee/lib/gitlab/code_owners/ @reprazent +/ee/lib/ee/gitlab/auth/ldap/ @dblessing @mkozono +/lib/gitlab/auth/ldap/ @dblessing @mkozono diff --git a/GITALY_SERVER_VERSION b/GITALY_SERVER_VERSION index f34340fc21c..99e0d1ed987 100644 --- a/GITALY_SERVER_VERSION +++ b/GITALY_SERVER_VERSION @@ -1 +1 @@ -0.119.0 +0.120.0 diff --git a/GITLAB_SHELL_VERSION b/GITLAB_SHELL_VERSION index 0e79152459e..2b0aa21219d 100644 --- a/GITLAB_SHELL_VERSION +++ b/GITLAB_SHELL_VERSION @@ -1 +1 @@ -8.1.1 +8.2.1 diff --git a/Gemfile.lock b/Gemfile.lock index 02f30b9d686..3dce80deb87 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -208,7 +208,7 @@ GEM fast_blank (1.0.0) fast_gettext (1.6.0) ffaker (2.4.0) - ffi (1.9.18) + ffi (1.9.25) flipper (0.13.0) flipper-active_record (0.13.0) activerecord (>= 3.2, < 6) diff --git a/Gemfile.rails5.lock b/Gemfile.rails5.lock index 2bdb1e035d8..e1295e1ff9b 100644 --- a/Gemfile.rails5.lock +++ b/Gemfile.rails5.lock @@ -211,7 +211,7 @@ GEM fast_blank (1.0.0) fast_gettext (1.6.0) ffaker (2.4.0) - ffi (1.9.18) + ffi (1.9.25) flipper (0.13.0) flipper-active_record (0.13.0) activerecord (>= 3.2, < 6) diff --git a/app/assets/javascripts/boards/components/modal/index.vue b/app/assets/javascripts/boards/components/modal/index.vue index 33e72a6782e..7b33a7573e7 100644 --- a/app/assets/javascripts/boards/components/modal/index.vue +++ b/app/assets/javascripts/boards/components/modal/index.vue @@ -1,6 +1,6 @@ <script> /* global ListIssue */ - import queryData from '~/boards/utils/query_data'; + import { urlParamsToObject } from '~/lib/utils/common_utils'; import loadingIcon from '~/vue_shared/components/loading_icon.vue'; import ModalHeader from './header.vue'; import ModalList from './list.vue'; @@ -109,13 +109,11 @@ loadIssues(clearIssues = false) { if (!this.showAddIssuesModal) return false; - return gl.boardService - .getBacklog( - queryData(this.filter.path, { - page: this.page, - per: this.perPage, - }), - ) + return gl.boardService.getBacklog({ + ...urlParamsToObject(this.filter.path), + page: this.page, + per: this.perPage, + }) .then(res => res.data) .then(data => { if (clearIssues) { diff --git a/app/assets/javascripts/boards/models/list.js b/app/assets/javascripts/boards/models/list.js index ad473404c29..d416b76f0f4 100644 --- a/app/assets/javascripts/boards/models/list.js +++ b/app/assets/javascripts/boards/models/list.js @@ -4,7 +4,7 @@ import { __ } from '~/locale'; import ListLabel from '~/vue_shared/models/label'; import ListAssignee from '~/vue_shared/models/assignee'; -import queryData from '../utils/query_data'; +import { urlParamsToObject } from '~/lib/utils/common_utils'; const PER_PAGE = 20; @@ -115,7 +115,10 @@ class List { } getIssues(emptyIssues = true) { - const data = queryData(gl.issueBoards.BoardsStore.filter.path, { page: this.page }); + const data = { + ...urlParamsToObject(gl.issueBoards.BoardsStore.filter.path), + page: this.page, + }; if (this.label && data.label_name) { data.label_name = data.label_name.filter(label => label !== this.label.title); diff --git a/app/assets/javascripts/boards/utils/query_data.js b/app/assets/javascripts/boards/utils/query_data.js deleted file mode 100644 index 65315979df7..00000000000 --- a/app/assets/javascripts/boards/utils/query_data.js +++ /dev/null @@ -1,21 +0,0 @@ -export default (path, extraData) => path.split('&').reduce((dataParam, filterParam) => { - if (filterParam === '') return dataParam; - - const data = dataParam; - const paramSplit = filterParam.split('='); - const paramKeyNormalized = paramSplit[0].replace('[]', ''); - const isArray = paramSplit[0].indexOf('[]'); - const value = decodeURIComponent(paramSplit[1].replace(/\+/g, ' ')); - - if (isArray !== -1) { - if (!data[paramKeyNormalized]) { - data[paramKeyNormalized] = []; - } - - data[paramKeyNormalized].push(value); - } else { - data[paramKeyNormalized] = value; - } - - return data; -}, extraData); diff --git a/app/assets/javascripts/groups/components/group_item.vue b/app/assets/javascripts/groups/components/group_item.vue index 154ad2ea607..44d6fa26914 100644 --- a/app/assets/javascripts/groups/components/group_item.vue +++ b/app/assets/javascripts/groups/components/group_item.vue @@ -100,7 +100,7 @@ export default { </div> <div :class="{ 'content-loading': group.isChildrenLoading }" - class="avatar-container s24 d-none d-sm-block" + class="avatar-container s24 d-none d-sm-flex" > <a :href="group.relativePath" diff --git a/app/assets/javascripts/ide/components/commit_sidebar/editor_header.vue b/app/assets/javascripts/ide/components/commit_sidebar/editor_header.vue new file mode 100644 index 00000000000..c3ca147e850 --- /dev/null +++ b/app/assets/javascripts/ide/components/commit_sidebar/editor_header.vue @@ -0,0 +1,78 @@ +<script> +import $ from 'jquery'; +import { mapActions } from 'vuex'; +import { __ } from '~/locale'; +import FileIcon from '~/vue_shared/components/file_icon.vue'; +import ChangedFileIcon from '../changed_file_icon.vue'; + +export default { + components: { + FileIcon, + ChangedFileIcon, + }, + props: { + activeFile: { + type: Object, + required: true, + }, + }, + computed: { + activeButtonText() { + return this.activeFile.staged ? __('Unstage') : __('Stage'); + }, + isStaged() { + return !this.activeFile.changed && this.activeFile.staged; + }, + }, + methods: { + ...mapActions(['stageChange', 'unstageChange']), + actionButtonClicked() { + if (this.activeFile.staged) { + this.unstageChange(this.activeFile.path); + } else { + this.stageChange(this.activeFile.path); + } + }, + showDiscardModal() { + $(document.getElementById(`discard-file-${this.activeFile.path}`)).modal('show'); + }, + }, +}; +</script> + +<template> + <div class="d-flex ide-commit-editor-header align-items-center"> + <file-icon + :file-name="activeFile.name" + :size="16" + class="mr-2" + /> + <strong class="mr-2"> + {{ activeFile.path }} + </strong> + <changed-file-icon + :file="activeFile" + /> + <div class="ml-auto"> + <button + v-if="!isStaged" + type="button" + class="btn btn-remove btn-inverted append-right-8" + @click="showDiscardModal" + > + {{ __('Discard') }} + </button> + <button + :class="{ + 'btn-success': !isStaged, + 'btn-warning': isStaged + }" + type="button" + class="btn btn-inverted" + @click="actionButtonClicked" + > + {{ activeButtonText }} + </button> + </div> + </div> +</template> diff --git a/app/assets/javascripts/ide/components/commit_sidebar/list.vue b/app/assets/javascripts/ide/components/commit_sidebar/list.vue index d0fb0e3d99e..3fdd35ad228 100644 --- a/app/assets/javascripts/ide/components/commit_sidebar/list.vue +++ b/app/assets/javascripts/ide/components/commit_sidebar/list.vue @@ -1,7 +1,9 @@ <script> +import $ from 'jquery'; import { mapActions } from 'vuex'; import { __, sprintf } from '~/locale'; import Icon from '~/vue_shared/components/icon.vue'; +import GlModal from '~/vue_shared/components/gl_modal.vue'; import tooltip from '~/vue_shared/directives/tooltip'; import ListItem from './list_item.vue'; @@ -9,6 +11,7 @@ export default { components: { Icon, ListItem, + GlModal, }, directives: { tooltip, @@ -56,6 +59,11 @@ export default { type: String, required: true, }, + emptyStateText: { + type: String, + required: false, + default: __('No changes'), + }, }, computed: { titleText() { @@ -68,11 +76,19 @@ export default { }, }, methods: { - ...mapActions(['stageAllChanges', 'unstageAllChanges']), + ...mapActions(['stageAllChanges', 'unstageAllChanges', 'discardAllChanges']), actionBtnClicked() { this[this.action](); + + $(this.$refs.actionBtn).tooltip('hide'); + }, + openDiscardModal() { + $('#discard-all-changes').modal('show'); }, }, + discardModalText: __( + "You will loose all the unstaged changes you've made in this project. This action cannot be undone.", + ), }; </script> @@ -81,27 +97,32 @@ export default { class="ide-commit-list-container" > <header - class="multi-file-commit-panel-header" + class="multi-file-commit-panel-header d-flex mb-0" > <div - class="multi-file-commit-panel-header-title" + class="d-flex align-items-center flex-fill" > <icon v-once :name="iconName" :size="18" + class="append-right-8" /> - {{ titleText }} + <strong> + {{ titleText }} + </strong> <div class="d-flex ml-auto"> <button v-tooltip - v-show="filesLength" + ref="actionBtn" + :title="actionBtnText" + :aria-label="actionBtnText" + :disabled="!filesLength" :class="{ - 'd-flex': filesLength + 'disabled-content': !filesLength }" - :title="actionBtnText" type="button" - class="btn btn-default ide-staged-action-btn p-0 order-1 align-items-center" + class="d-flex ide-staged-action-btn p-0 border-0 align-items-center" data-placement="bottom" data-container="body" data-boundary="viewport" @@ -109,18 +130,32 @@ export default { > <icon :name="actionBtnIcon" - :size="12" + :size="16" class="ml-auto mr-auto" /> </button> - <span + <button + v-tooltip + v-if="!stagedList" + :title="__('Discard all changes')" + :aria-label="__('Discard all changes')" + :disabled="!filesLength" :class="{ - 'rounded-right': !filesLength + 'disabled-content': !filesLength }" - class="ide-commit-file-count order-0 rounded-left text-center" + type="button" + class="d-flex ide-staged-action-btn p-0 border-0 align-items-center" + data-placement="bottom" + data-container="body" + data-boundary="viewport" + @click="openDiscardModal" > - {{ filesLength }} - </span> + <icon + :size="16" + name="remove-all" + class="ml-auto mr-auto" + /> + </button> </div> </div> </header> @@ -143,9 +178,19 @@ export default { </ul> <p v-else - class="multi-file-commit-list form-text text-muted" + class="multi-file-commit-list form-text text-muted text-center" > - {{ __('No changes') }} + {{ emptyStateText }} </p> + <gl-modal + v-if="!stagedList" + id="discard-all-changes" + :footer-primary-button-text="__('Discard all changes')" + :header-title-text="__('Discard all unstaged changes?')" + footer-primary-button-variant="danger" + @submit="discardAllChanges" + > + {{ $options.discardModalText }} + </gl-modal> </div> </template> diff --git a/app/assets/javascripts/ide/components/commit_sidebar/list_item.vue b/app/assets/javascripts/ide/components/commit_sidebar/list_item.vue index 391004dcd3c..10c78a80302 100644 --- a/app/assets/javascripts/ide/components/commit_sidebar/list_item.vue +++ b/app/assets/javascripts/ide/components/commit_sidebar/list_item.vue @@ -2,6 +2,7 @@ import { mapActions } from 'vuex'; import tooltip from '~/vue_shared/directives/tooltip'; import Icon from '~/vue_shared/components/icon.vue'; +import FileIcon from '~/vue_shared/components/file_icon.vue'; import StageButton from './stage_button.vue'; import UnstageButton from './unstage_button.vue'; import { viewerTypes } from '../../constants'; @@ -12,6 +13,7 @@ export default { Icon, StageButton, UnstageButton, + FileIcon, }, directives: { tooltip, @@ -48,7 +50,7 @@ export default { return `${getCommitIconMap(this.file).icon}${suffix}`; }, iconClass() { - return `${getCommitIconMap(this.file).class} append-right-8`; + return `${getCommitIconMap(this.file).class} ml-auto mr-auto`; }, fullKey() { return `${this.keyPrefix}-${this.file.key}`; @@ -105,17 +107,24 @@ export default { @click="openFileInEditor" > <span class="multi-file-commit-list-file-path d-flex align-items-center"> - <icon - :name="iconName" - :size="16" - :css-classes="iconClass" + <file-icon + :file-name="file.name" + class="append-right-8" />{{ file.name }} </span> + <div class="ml-auto d-flex align-items-center"> + <div class="d-flex align-items-center ide-commit-list-changed-icon"> + <icon + :name="iconName" + :size="16" + :css-classes="iconClass" + /> + </div> + <component + :is="actionComponent" + :path="file.path" + /> + </div> </div> - <component - :is="actionComponent" - :path="file.path" - class="d-flex position-absolute" - /> </div> </template> diff --git a/app/assets/javascripts/ide/components/commit_sidebar/stage_button.vue b/app/assets/javascripts/ide/components/commit_sidebar/stage_button.vue index e6044401c9f..8a1836a5c92 100644 --- a/app/assets/javascripts/ide/components/commit_sidebar/stage_button.vue +++ b/app/assets/javascripts/ide/components/commit_sidebar/stage_button.vue @@ -1,11 +1,15 @@ <script> +import $ from 'jquery'; import { mapActions } from 'vuex'; +import { sprintf, __ } from '~/locale'; import Icon from '~/vue_shared/components/icon.vue'; import tooltip from '~/vue_shared/directives/tooltip'; +import GlModal from '~/vue_shared/components/gl_modal.vue'; export default { components: { Icon, + GlModal, }, directives: { tooltip, @@ -16,8 +20,22 @@ export default { required: true, }, }, + computed: { + modalId() { + return `discard-file-${this.path}`; + }, + modalTitle() { + return sprintf( + __('Discard changes to %{path}?'), + { path: this.path }, + ); + }, + }, methods: { ...mapActions(['stageChange', 'discardFileChanges']), + showDiscardModal() { + $(document.getElementById(this.modalId)).modal('show'); + }, }, }; </script> @@ -25,51 +43,50 @@ export default { <template> <div v-once - class="multi-file-discard-btn dropdown" + class="multi-file-discard-btn d-flex" > <button v-tooltip :aria-label="__('Stage changes')" :title="__('Stage changes')" type="button" - class="btn btn-blank append-right-5 d-flex align-items-center" + class="btn btn-blank align-items-center" data-container="body" data-boundary="viewport" data-placement="bottom" - @click.stop="stageChange(path)" + @click.stop.prevent="stageChange(path)" > <icon - :size="12" + :size="16" name="mobile-issue-close" + class="ml-auto mr-auto" /> </button> <button v-tooltip - :title="__('More actions')" + :aria-label="__('Discard changes')" + :title="__('Discard changes')" type="button" - class="btn btn-blank d-flex align-items-center" + class="btn btn-blank align-items-center" data-container="body" data-boundary="viewport" data-placement="bottom" - data-toggle="dropdown" - data-display="static" + @click.stop.prevent="showDiscardModal" > <icon - :size="12" - name="ellipsis_h" + :size="16" + name="remove" + class="ml-auto mr-auto" /> </button> - <div class="dropdown-menu dropdown-menu-right"> - <ul> - <li> - <button - type="button" - @click.stop="discardFileChanges(path)" - > - {{ __('Discard changes') }} - </button> - </li> - </ul> - </div> + <gl-modal + :id="modalId" + :header-title-text="modalTitle" + :footer-primary-button-text="__('Discard changes')" + footer-primary-button-variant="danger" + @submit="discardFileChanges(path)" + > + {{ __("You will loose all changes you've made to this file. This action cannot be undone.") }} + </gl-modal> </div> </template> diff --git a/app/assets/javascripts/ide/components/commit_sidebar/unstage_button.vue b/app/assets/javascripts/ide/components/commit_sidebar/unstage_button.vue index 9cec73ec00e..86c40602074 100644 --- a/app/assets/javascripts/ide/components/commit_sidebar/unstage_button.vue +++ b/app/assets/javascripts/ide/components/commit_sidebar/unstage_button.vue @@ -25,22 +25,23 @@ export default { <template> <div v-once - class="multi-file-discard-btn" + class="multi-file-discard-btn d-flex" > <button v-tooltip :aria-label="__('Unstage changes')" :title="__('Unstage changes')" type="button" - class="btn btn-blank d-flex align-items-center" + class="btn btn-blank align-items-center" data-container="body" data-boundary="viewport" data-placement="bottom" - @click="unstageChange(path)" + @click.stop.prevent="unstageChange(path)" > <icon - :size="12" - name="history" + :size="16" + name="redo" + class="ml-auto mr-auto" /> </button> </div> diff --git a/app/assets/javascripts/ide/components/file_templates/bar.vue b/app/assets/javascripts/ide/components/file_templates/bar.vue new file mode 100644 index 00000000000..23be5f45f16 --- /dev/null +++ b/app/assets/javascripts/ide/components/file_templates/bar.vue @@ -0,0 +1,80 @@ +<script> +import { mapActions, mapGetters, mapState } from 'vuex'; +import Dropdown from './dropdown.vue'; + +export default { + components: { + Dropdown, + }, + computed: { + ...mapGetters(['activeFile']), + ...mapGetters('fileTemplates', ['templateTypes']), + ...mapState('fileTemplates', ['selectedTemplateType', 'updateSuccess']), + showTemplatesDropdown() { + return Object.keys(this.selectedTemplateType).length > 0; + }, + }, + watch: { + activeFile: 'setInitialType', + }, + mounted() { + this.setInitialType(); + }, + methods: { + ...mapActions('fileTemplates', [ + 'setSelectedTemplateType', + 'fetchTemplate', + 'undoFileTemplate', + ]), + setInitialType() { + const initialTemplateType = this.templateTypes.find(t => t.name === this.activeFile.name); + + if (initialTemplateType) { + this.setSelectedTemplateType(initialTemplateType); + } + }, + selectTemplateType(templateType) { + this.setSelectedTemplateType(templateType); + }, + selectTemplate(template) { + this.fetchTemplate(template); + }, + undo() { + this.undoFileTemplate(); + }, + }, +}; +</script> + +<template> + <div class="d-flex align-items-center ide-file-templates"> + <strong class="append-right-default"> + {{ __('File templates') }} + </strong> + <dropdown + :data="templateTypes" + :label="selectedTemplateType.name || __('Choose a type...')" + class="mr-2" + @click="selectTemplateType" + /> + <dropdown + v-if="showTemplatesDropdown" + :label="__('Choose a template...')" + :is-async-data="true" + :searchable="true" + :title="__('File templates')" + class="mr-2" + @click="selectTemplate" + /> + <transition name="fade"> + <button + v-show="updateSuccess" + type="button" + class="btn btn-default" + @click="undo" + > + {{ __('Undo') }} + </button> + </transition> + </div> +</template> diff --git a/app/assets/javascripts/ide/components/file_templates/dropdown.vue b/app/assets/javascripts/ide/components/file_templates/dropdown.vue new file mode 100644 index 00000000000..13059937f85 --- /dev/null +++ b/app/assets/javascripts/ide/components/file_templates/dropdown.vue @@ -0,0 +1,125 @@ +<script> +import $ from 'jquery'; +import { mapActions, mapState } from 'vuex'; +import LoadingIcon from '~/vue_shared/components/loading_icon.vue'; +import DropdownButton from '~/vue_shared/components/dropdown/dropdown_button.vue'; + +export default { + components: { + DropdownButton, + LoadingIcon, + }, + props: { + data: { + type: Array, + required: false, + default: () => [], + }, + label: { + type: String, + required: true, + }, + title: { + type: String, + required: false, + default: null, + }, + isAsyncData: { + type: Boolean, + required: false, + default: false, + }, + searchable: { + type: Boolean, + required: false, + default: false, + }, + }, + data() { + return { + search: '', + }; + }, + computed: { + ...mapState('fileTemplates', ['templates', 'isLoading']), + outputData() { + return (this.isAsyncData ? this.templates : this.data).filter(t => { + if (!this.searchable) return true; + + return t.name.toLowerCase().indexOf(this.search.toLowerCase()) >= 0; + }); + }, + showLoading() { + return this.isAsyncData ? this.isLoading : false; + }, + }, + mounted() { + $(this.$el).on('show.bs.dropdown', this.fetchTemplatesIfAsync); + }, + beforeDestroy() { + $(this.$el).off('show.bs.dropdown', this.fetchTemplatesIfAsync); + }, + methods: { + ...mapActions('fileTemplates', ['fetchTemplateTypes']), + fetchTemplatesIfAsync() { + if (this.isAsyncData) { + this.fetchTemplateTypes(); + } + }, + clickItem(item) { + this.$emit('click', item); + }, + }, +}; +</script> + +<template> + <div class="dropdown"> + <dropdown-button + :toggle-text="label" + data-display="static" + /> + <div class="dropdown-menu pb-0"> + <div + v-if="title" + class="dropdown-title ml-0 mr-0" + > + {{ title }} + </div> + <div + v-if="!showLoading && searchable" + class="dropdown-input" + > + <input + v-model="search" + :placeholder="__('Filter...')" + type="search" + class="dropdown-input-field" + /> + <i + aria-hidden="true" + class="fa fa-search dropdown-input-search" + ></i> + </div> + <div class="dropdown-content"> + <loading-icon + v-if="showLoading" + size="2" + /> + <ul v-else> + <li + v-for="(item, index) in outputData" + :key="index" + > + <button + type="button" + @click="clickItem(item)" + > + {{ item.name }} + </button> + </li> + </ul> + </div> + </div> + </div> +</template> diff --git a/app/assets/javascripts/ide/components/ide.vue b/app/assets/javascripts/ide/components/ide.vue index 6a5ab35a16a..a3add3b778f 100644 --- a/app/assets/javascripts/ide/components/ide.vue +++ b/app/assets/javascripts/ide/components/ide.vue @@ -10,6 +10,7 @@ import RepoEditor from './repo_editor.vue'; import FindFile from './file_finder/index.vue'; import RightPane from './panes/right.vue'; import ErrorMessage from './error_message.vue'; +import CommitEditorHeader from './commit_sidebar/editor_header.vue'; const originalStopCallback = Mousetrap.stopCallback; @@ -23,6 +24,7 @@ export default { FindFile, RightPane, ErrorMessage, + CommitEditorHeader, }, computed: { ...mapState([ @@ -34,7 +36,7 @@ export default { 'currentProjectId', 'errorMessage', ]), - ...mapGetters(['activeFile', 'hasChanges', 'someUncommitedChanges']), + ...mapGetters(['activeFile', 'hasChanges', 'someUncommitedChanges', 'isCommitModeActive']), }, mounted() { window.onbeforeunload = e => this.onBeforeUnload(e); @@ -96,7 +98,12 @@ export default { <template v-if="activeFile" > + <commit-editor-header + v-if="isCommitModeActive" + :active-file="activeFile" + /> <repo-tabs + v-else :active-file="activeFile" :files="openFiles" :viewer="viewer" diff --git a/app/assets/javascripts/ide/components/new_dropdown/modal.vue b/app/assets/javascripts/ide/components/new_dropdown/modal.vue index e500ef0e1b5..bcd53ac1ba2 100644 --- a/app/assets/javascripts/ide/components/new_dropdown/modal.vue +++ b/app/assets/javascripts/ide/components/new_dropdown/modal.vue @@ -1,6 +1,7 @@ <script> +import $ from 'jquery'; import { __ } from '~/locale'; -import { mapActions, mapState } from 'vuex'; +import { mapActions, mapState, mapGetters } from 'vuex'; import GlModal from '~/vue_shared/components/gl_modal.vue'; import { modalTypes } from '../../constants'; @@ -15,6 +16,7 @@ export default { }, computed: { ...mapState(['entryModal']), + ...mapGetters('fileTemplates', ['templateTypes']), entryName: { get() { if (this.entryModal.type === modalTypes.rename) { @@ -31,7 +33,9 @@ export default { if (this.entryModal.type === modalTypes.tree) { return __('Create new directory'); } else if (this.entryModal.type === modalTypes.rename) { - return this.entryModal.entry.type === modalTypes.tree ? __('Rename folder') : __('Rename file'); + return this.entryModal.entry.type === modalTypes.tree + ? __('Rename folder') + : __('Rename file'); } return __('Create new file'); @@ -40,11 +44,16 @@ export default { if (this.entryModal.type === modalTypes.tree) { return __('Create directory'); } else if (this.entryModal.type === modalTypes.rename) { - return this.entryModal.entry.type === modalTypes.tree ? __('Rename folder') : __('Rename file'); + return this.entryModal.entry.type === modalTypes.tree + ? __('Rename folder') + : __('Rename file'); } return __('Create file'); }, + isCreatingNew() { + return this.entryModal.type !== modalTypes.rename; + }, }, methods: { ...mapActions(['createTempEntry', 'renameEntry']), @@ -61,6 +70,14 @@ export default { }); } }, + createFromTemplate(template) { + this.createTempEntry({ + name: template.name, + type: this.entryModal.type, + }); + + $('#ide-new-entry').modal('toggle'); + }, focusInput() { this.$refs.fieldName.focus(); }, @@ -77,6 +94,7 @@ export default { :header-title-text="modalTitle" :footer-primary-button-text="buttonLabel" footer-primary-button-variant="success" + modal-size="lg" @submit="submitForm" @open="focusInput" @closed="closedModal" @@ -84,16 +102,35 @@ export default { <div class="form-group row" > - <label class="label-bold col-form-label col-sm-3"> + <label class="label-bold col-form-label col-sm-2"> {{ __('Name') }} </label> - <div class="col-sm-9"> + <div class="col-sm-10"> <input ref="fieldName" v-model="entryName" type="text" class="form-control" + placeholder="/dir/file_name" /> + <ul + v-if="isCreatingNew" + class="prepend-top-default list-inline" + > + <li + v-for="(template, index) in templateTypes" + :key="index" + class="list-inline-item" + > + <button + type="button" + class="btn btn-missing p-1 pr-2 pl-2" + @click="createFromTemplate(template)" + > + {{ template.name }} + </button> + </li> + </ul> </div> </div> </gl-modal> diff --git a/app/assets/javascripts/ide/components/repo_commit_section.vue b/app/assets/javascripts/ide/components/repo_commit_section.vue index 6f1a941fbc4..d3b24c5b793 100644 --- a/app/assets/javascripts/ide/components/repo_commit_section.vue +++ b/app/assets/javascripts/ide/components/repo_commit_section.vue @@ -95,8 +95,9 @@ export default { :file-list="changedFiles" :action-btn-text="__('Stage all changes')" :active-file-key="activeFileKey" + :empty-state-text="__('There are no unstaged changes')" action="stageAllChanges" - action-btn-icon="mobile-issue-close" + action-btn-icon="stage-all" item-action-component="stage-button" class="is-first" icon-name="unstaged" @@ -108,8 +109,9 @@ export default { :action-btn-text="__('Unstage all changes')" :staged-list="true" :active-file-key="activeFileKey" + :empty-state-text="__('There are no staged changes')" action="unstageAllChanges" - action-btn-icon="history" + action-btn-icon="unstage-all" item-action-component="unstage-button" icon-name="staged" /> diff --git a/app/assets/javascripts/ide/components/repo_editor.vue b/app/assets/javascripts/ide/components/repo_editor.vue index f55aa843444..d3a73e84cc7 100644 --- a/app/assets/javascripts/ide/components/repo_editor.vue +++ b/app/assets/javascripts/ide/components/repo_editor.vue @@ -6,12 +6,14 @@ import DiffViewer from '~/vue_shared/components/diff_viewer/diff_viewer.vue'; import { activityBarViews, viewerTypes } from '../constants'; import Editor from '../lib/editor'; import ExternalLink from './external_link.vue'; +import FileTemplatesBar from './file_templates/bar.vue'; export default { components: { ContentViewer, DiffViewer, ExternalLink, + FileTemplatesBar, }, props: { file: { @@ -34,6 +36,7 @@ export default { 'isCommitModeActive', 'isReviewModeActive', ]), + ...mapGetters('fileTemplates', ['showFileTemplatesBar']), shouldHideEditor() { return this.file && this.file.binary && !this.file.content; }, @@ -216,7 +219,7 @@ export default { id="ide" class="blob-viewer-container blob-editor-container" > - <div class="ide-mode-tabs clearfix" > + <div class="ide-mode-tabs clearfix"> <ul v-if="!shouldHideEditor && isEditModeActive" class="nav-links float-left" @@ -249,6 +252,9 @@ export default { :file="file" /> </div> + <file-templates-bar + v-if="showFileTemplatesBar(file.name)" + /> <div v-show="!shouldHideEditor && file.viewMode ==='editor'" ref="editor" diff --git a/app/assets/javascripts/ide/stores/actions.js b/app/assets/javascripts/ide/stores/actions.js index aa02dfbddc4..b8b64aead30 100644 --- a/app/assets/javascripts/ide/stores/actions.js +++ b/app/assets/javascripts/ide/stores/actions.js @@ -4,6 +4,7 @@ import { visitUrl } from '~/lib/utils/url_utility'; import flash from '~/flash'; import * as types from './mutation_types'; import FilesDecoratorWorker from './workers/files_decorator_worker'; +import { stageKeys } from '../constants'; export const redirectToUrl = (_, url) => visitUrl(url); @@ -122,14 +123,28 @@ export const scrollToTab = () => { }); }; -export const stageAllChanges = ({ state, commit }) => { +export const stageAllChanges = ({ state, commit, dispatch }) => { + const openFile = state.openFiles[0]; + commit(types.SET_LAST_COMMIT_MSG, ''); state.changedFiles.forEach(file => commit(types.STAGE_CHANGE, file.path)); + + dispatch('openPendingTab', { + file: state.stagedFiles.find(f => f.path === openFile.path), + keyPrefix: stageKeys.staged, + }); }; -export const unstageAllChanges = ({ state, commit }) => { +export const unstageAllChanges = ({ state, commit, dispatch }) => { + const openFile = state.openFiles[0]; + state.stagedFiles.forEach(file => commit(types.UNSTAGE_CHANGE, file.path)); + + dispatch('openPendingTab', { + file: state.changedFiles.find(f => f.path === openFile.path), + keyPrefix: stageKeys.unstaged, + }); }; export const updateViewer = ({ commit }, viewer) => { @@ -206,6 +221,7 @@ export const resetOpenFiles = ({ commit }) => commit(types.RESET_OPEN_FILES); export const renameEntry = ({ dispatch, commit, state }, { path, name, entryPath = null }) => { const entry = state.entries[entryPath || path]; + commit(types.RENAME_ENTRY, { path, name, entryPath }); if (entry.type === 'tree') { @@ -214,7 +230,7 @@ export const renameEntry = ({ dispatch, commit, state }, { path, name, entryPath ); } - if (!entryPath) { + if (!entryPath && !entry.tempFile) { dispatch('deleteEntry', path); } }; diff --git a/app/assets/javascripts/ide/stores/actions/file.js b/app/assets/javascripts/ide/stores/actions/file.js index 28b9d0df201..30dcf7ef4df 100644 --- a/app/assets/javascripts/ide/stores/actions/file.js +++ b/app/assets/javascripts/ide/stores/actions/file.js @@ -5,7 +5,7 @@ import service from '../../services'; import * as types from '../mutation_types'; import router from '../../ide_router'; import { setPageTitle } from '../utils'; -import { viewerTypes } from '../../constants'; +import { viewerTypes, stageKeys } from '../../constants'; export const closeFile = ({ commit, state, dispatch }, file) => { const { path } = file; @@ -208,8 +208,9 @@ export const discardFileChanges = ({ dispatch, state, commit, getters }, path) = eventHub.$emit(`editor.update.model.dispose.unstaged-${file.key}`, file.content); }; -export const stageChange = ({ commit, state }, path) => { +export const stageChange = ({ commit, state, dispatch }, path) => { const stagedFile = state.stagedFiles.find(f => f.path === path); + const openFile = state.openFiles.find(f => f.path === path); commit(types.STAGE_CHANGE, path); commit(types.SET_LAST_COMMIT_MSG, ''); @@ -217,21 +218,39 @@ export const stageChange = ({ commit, state }, path) => { if (stagedFile) { eventHub.$emit(`editor.update.model.new.content.staged-${stagedFile.key}`, stagedFile.content); } + + if (openFile && openFile.active) { + const file = state.stagedFiles.find(f => f.path === path); + + dispatch('openPendingTab', { + file, + keyPrefix: stageKeys.staged, + }); + } }; -export const unstageChange = ({ commit }, path) => { +export const unstageChange = ({ commit, dispatch, state }, path) => { + const openFile = state.openFiles.find(f => f.path === path); + commit(types.UNSTAGE_CHANGE, path); + + if (openFile && openFile.active) { + const file = state.changedFiles.find(f => f.path === path); + + dispatch('openPendingTab', { + file, + keyPrefix: stageKeys.unstaged, + }); + } }; -export const openPendingTab = ({ commit, getters, dispatch, state }, { file, keyPrefix }) => { +export const openPendingTab = ({ commit, getters, state }, { file, keyPrefix }) => { if (getters.activeFile && getters.activeFile.key === `${keyPrefix}-${file.key}`) return false; state.openFiles.forEach(f => eventHub.$emit(`editor.update.model.dispose.${f.key}`)); commit(types.ADD_PENDING_TAB, { file, keyPrefix }); - dispatch('scrollToTab'); - router.push(`/project/${file.projectId}/tree/${state.currentBranchId}/`); return true; diff --git a/app/assets/javascripts/ide/stores/index.js b/app/assets/javascripts/ide/stores/index.js index a601dc8f5a0..877d88bb060 100644 --- a/app/assets/javascripts/ide/stores/index.js +++ b/app/assets/javascripts/ide/stores/index.js @@ -8,6 +8,7 @@ import commitModule from './modules/commit'; import pipelines from './modules/pipelines'; import mergeRequests from './modules/merge_requests'; import branches from './modules/branches'; +import fileTemplates from './modules/file_templates'; Vue.use(Vuex); @@ -22,6 +23,7 @@ export const createStore = () => pipelines, mergeRequests, branches, + fileTemplates: fileTemplates(), }, }); diff --git a/app/assets/javascripts/ide/stores/modules/file_templates/actions.js b/app/assets/javascripts/ide/stores/modules/file_templates/actions.js index 43237a29466..dd53213ed18 100644 --- a/app/assets/javascripts/ide/stores/modules/file_templates/actions.js +++ b/app/assets/javascripts/ide/stores/modules/file_templates/actions.js @@ -1,6 +1,7 @@ import Api from '~/api'; import { __ } from '~/locale'; import * as types from './mutation_types'; +import eventHub from '../../../eventhub'; export const requestTemplateTypes = ({ commit }) => commit(types.REQUEST_TEMPLATE_TYPES); export const receiveTemplateTypesError = ({ commit, dispatch }) => { @@ -31,9 +32,23 @@ export const fetchTemplateTypes = ({ dispatch, state }) => { .catch(() => dispatch('receiveTemplateTypesError')); }; -export const setSelectedTemplateType = ({ commit }, type) => +export const setSelectedTemplateType = ({ commit, dispatch, rootGetters }, type) => { commit(types.SET_SELECTED_TEMPLATE_TYPE, type); + if (rootGetters.activeFile.prevPath === type.name) { + dispatch('discardFileChanges', rootGetters.activeFile.path, { root: true }); + } else if (rootGetters.activeFile.name !== type.name) { + dispatch( + 'renameEntry', + { + path: rootGetters.activeFile.path, + name: type.name, + }, + { root: true }, + ); + } +}; + export const receiveTemplateError = ({ dispatch }, template) => { dispatch( 'setErrorMessage', @@ -69,6 +84,7 @@ export const setFileTemplate = ({ dispatch, commit, rootGetters }, template) => { root: true }, ); commit(types.SET_UPDATE_SUCCESS, true); + eventHub.$emit(`editor.update.model.new.content.${rootGetters.activeFile.key}`, template.content); }; export const undoFileTemplate = ({ dispatch, commit, rootGetters }) => { @@ -76,6 +92,12 @@ export const undoFileTemplate = ({ dispatch, commit, rootGetters }) => { dispatch('changeFileContent', { path: file.path, content: file.raw }, { root: true }); commit(types.SET_UPDATE_SUCCESS, false); + + eventHub.$emit(`editor.update.model.new.content.${file.key}`, file.raw); + + if (file.prevPath) { + dispatch('discardFileChanges', file.path, { root: true }); + } }; // prevent babel-plugin-rewire from generating an invalid default during karma tests diff --git a/app/assets/javascripts/ide/stores/modules/file_templates/getters.js b/app/assets/javascripts/ide/stores/modules/file_templates/getters.js index 38318fd49bf..628babe6a01 100644 --- a/app/assets/javascripts/ide/stores/modules/file_templates/getters.js +++ b/app/assets/javascripts/ide/stores/modules/file_templates/getters.js @@ -1,3 +1,5 @@ +import { activityBarViews } from '../../../constants'; + export const templateTypes = () => [ { name: '.gitlab-ci.yml', @@ -17,7 +19,8 @@ export const templateTypes = () => [ }, ]; -export const showFileTemplatesBar = (_, getters) => name => - getters.templateTypes.find(t => t.name === name); +export const showFileTemplatesBar = (_, getters, rootState) => name => + getters.templateTypes.find(t => t.name === name) && + rootState.currentActivityView === activityBarViews.edit; export default () => {}; diff --git a/app/assets/javascripts/ide/stores/modules/file_templates/index.js b/app/assets/javascripts/ide/stores/modules/file_templates/index.js index dfa5ef54413..383ff5db392 100644 --- a/app/assets/javascripts/ide/stores/modules/file_templates/index.js +++ b/app/assets/javascripts/ide/stores/modules/file_templates/index.js @@ -3,10 +3,10 @@ import * as actions from './actions'; import * as getters from './getters'; import mutations from './mutations'; -export default { +export default () => ({ namespaced: true, actions, state: createState(), getters, mutations, -}; +}); diff --git a/app/assets/javascripts/ide/stores/mutations.js b/app/assets/javascripts/ide/stores/mutations.js index f2bb87ac674..2c8535bda59 100644 --- a/app/assets/javascripts/ide/stores/mutations.js +++ b/app/assets/javascripts/ide/stores/mutations.js @@ -1,3 +1,4 @@ +import Vue from 'vue'; import * as types from './mutation_types'; import projectMutations from './mutations/project'; import mergeRequestMutation from './mutations/merge_request'; @@ -226,7 +227,7 @@ export default { path: newPath, name: entryPath ? oldEntry.name : name, tempFile: true, - prevPath: oldEntry.path, + prevPath: oldEntry.tempFile ? null : oldEntry.path, url: oldEntry.url.replace(new RegExp(`${oldEntry.path}/?$`), newPath), tree: [], parentPath, @@ -245,6 +246,20 @@ export default { if (newEntry.type === 'blob') { state.changedFiles = state.changedFiles.concat(newEntry); } + + if (state.entries[newPath].opened) { + state.openFiles.push(state.entries[newPath]); + } + + if (oldEntry.tempFile) { + const filterMethod = f => f.path !== oldEntry.path; + + state.openFiles = state.openFiles.filter(filterMethod); + state.changedFiles = state.changedFiles.filter(filterMethod); + parent.tree = parent.tree.filter(filterMethod); + + Vue.delete(state.entries, oldEntry.path); + } }, ...projectMutations, ...mergeRequestMutation, diff --git a/app/assets/javascripts/ide/stores/mutations/file.js b/app/assets/javascripts/ide/stores/mutations/file.js index 66f29824898..6ca246c1d63 100644 --- a/app/assets/javascripts/ide/stores/mutations/file.js +++ b/app/assets/javascripts/ide/stores/mutations/file.js @@ -55,7 +55,7 @@ export default { f => f.path === file.path && f.pending && !(f.tempFile && !f.prevPath), ); - if (file.tempFile) { + if (file.tempFile && file.content === '') { Object.assign(state.entries[file.path], { content: raw, }); diff --git a/app/assets/javascripts/lib/utils/common_utils.js b/app/assets/javascripts/lib/utils/common_utils.js index 3e208764b3e..0849d97bc1a 100644 --- a/app/assets/javascripts/lib/utils/common_utils.js +++ b/app/assets/javascripts/lib/utils/common_utils.js @@ -131,16 +131,43 @@ export const parseUrlPathname = url => { return parsedUrl.pathname.charAt(0) === '/' ? parsedUrl.pathname : `/${parsedUrl.pathname}`; }; -// We can trust that each param has one & since values containing & will be encoded -// Remove the first character of search as it is always ? -export const getUrlParamsArray = () => - window.location.search - .slice(1) - .split('&') - .map(param => { - const split = param.split('='); - return [decodeURI(split[0]), split[1]].join('='); - }); +const splitPath = (path = '') => path + .replace(/^\?/, '') + .split('&'); + +export const urlParamsToArray = (path = '') => splitPath(path) + .filter(param => param.length > 0) + .map(param => { + const split = param.split('='); + return [decodeURI(split[0]), split[1]].join('='); + }); + +export const getUrlParamsArray = () => urlParamsToArray(window.location.search); + +export const urlParamsToObject = (path = '') => splitPath(path) + .reduce((dataParam, filterParam) => { + if (filterParam === '') { + return dataParam; + } + + const data = dataParam; + let [key, value] = filterParam.split('='); + const isArray = key.includes('[]'); + key = key.replace('[]', ''); + value = decodeURIComponent(value.replace(/\+/g, ' ')); + + if (isArray) { + if (!data[key]) { + data[key] = []; + } + + data[key].push(value); + } else { + data[key] = value; + } + + return data; + }, {}); export const isMetaKey = e => e.metaKey || e.ctrlKey || e.altKey || e.shiftKey; diff --git a/app/assets/javascripts/main.js b/app/assets/javascripts/main.js index 2718f73a830..c5a5f64abac 100644 --- a/app/assets/javascripts/main.js +++ b/app/assets/javascripts/main.js @@ -29,6 +29,7 @@ import './milestone_select'; import './frequent_items'; import initBreadcrumbs from './breadcrumb'; import initDispatcher from './dispatcher'; +import initUsagePingConsent from './usage_ping_consent'; // expose jQuery as global (TODO: remove these) window.jQuery = jQuery; @@ -78,6 +79,7 @@ document.addEventListener('DOMContentLoaded', () => { initImporterStatus(); initTodoToggle(); initLogoAnimation(); + initUsagePingConsent(); // Set the default path for all cookies to GitLab's root directory Cookies.defaults.path = gon.relative_url_root || '/'; diff --git a/app/assets/javascripts/notes/components/note_actions.vue b/app/assets/javascripts/notes/components/note_actions.vue index cdbbb342331..87fc002fcbc 100644 --- a/app/assets/javascripts/notes/components/note_actions.vue +++ b/app/assets/javascripts/notes/components/note_actions.vue @@ -24,12 +24,13 @@ export default { required: true, }, noteId: { - type: Number, + type: String, required: true, }, noteUrl: { type: String, - required: true, + required: false, + default: '', }, accessLevel: { type: String, @@ -225,11 +226,11 @@ export default { Report as abuse </a> </li> - <li> + <li v-if="noteUrl"> <button :data-clipboard-text="noteUrl" type="button" - css-class="btn-default btn-transparent" + class="btn-default btn-transparent js-btn-copy-note-link" > Copy link </button> diff --git a/app/assets/javascripts/notes/components/note_awards_list.vue b/app/assets/javascripts/notes/components/note_awards_list.vue index e111d3b9ac2..051b17e9aa9 100644 --- a/app/assets/javascripts/notes/components/note_awards_list.vue +++ b/app/assets/javascripts/notes/components/note_awards_list.vue @@ -25,7 +25,7 @@ export default { required: true, }, noteId: { - type: Number, + type: String, required: true, }, canAwardEmoji: { diff --git a/app/assets/javascripts/notes/components/note_form.vue b/app/assets/javascripts/notes/components/note_form.vue index abcd4422d7c..c41ed070383 100644 --- a/app/assets/javascripts/notes/components/note_form.vue +++ b/app/assets/javascripts/notes/components/note_form.vue @@ -20,9 +20,9 @@ export default { default: '', }, noteId: { - type: Number, + type: String, required: false, - default: 0, + default: '', }, markdownVersion: { type: Number, @@ -67,7 +67,10 @@ export default { 'getUserDataByProp', ]), noteHash() { - return `#note_${this.noteId}`; + if (this.noteId) { + return `#note_${this.noteId}`; + } + return '#'; }, markdownPreviewPath() { return this.getNoteableDataByProp('preview_note_path'); diff --git a/app/assets/javascripts/notes/components/note_header.vue b/app/assets/javascripts/notes/components/note_header.vue index a621418cf72..d669d12a39b 100644 --- a/app/assets/javascripts/notes/components/note_header.vue +++ b/app/assets/javascripts/notes/components/note_header.vue @@ -9,7 +9,8 @@ export default { props: { author: { type: Object, - required: true, + required: false, + default: () => ({}), }, createdAt: { type: String, @@ -21,7 +22,7 @@ export default { default: '', }, noteId: { - type: Number, + type: String, required: true, }, includeToggle: { @@ -72,7 +73,10 @@ export default { {{ __('Toggle discussion') }} </button> </div> - <a :href="author.path"> + <a + v-if="Object.keys(author).length" + :href="author.path" + > <span class="note-header-author-name">{{ author.name }}</span> <span v-if="author.status_tooltip_html" @@ -81,6 +85,9 @@ export default { @{{ author.username }} </span> </a> + <span v-else> + {{ __('A deleted user') }} + </span> <span class="note-headline-light"> <span class="note-headline-meta"> <template v-if="actionText"> diff --git a/app/assets/javascripts/registry/components/collapsible_container.vue b/app/assets/javascripts/registry/components/collapsible_container.vue index 4116c4a0489..cea409aa130 100644 --- a/app/assets/javascripts/registry/components/collapsible_container.vue +++ b/app/assets/javascripts/registry/components/collapsible_container.vue @@ -6,6 +6,7 @@ import tooltip from '../../vue_shared/directives/tooltip'; import tableRegistry from './table_registry.vue'; import { errorMessages, errorMessagesTypes } from '../constants'; + import { __ } from '../../locale'; export default { name: 'CollapsibeContainerRegisty', @@ -46,7 +47,10 @@ handleDeleteRepository() { this.deleteRepo(this.repo) - .then(() => this.fetchRepos()) + .then(() => { + Flash(__('This container registry has been scheduled for deletion.'), 'notice'); + this.fetchRepos(); + }) .catch(() => this.showError(errorMessagesTypes.DELETE_REPO)); }, diff --git a/app/assets/javascripts/usage_ping_consent.js b/app/assets/javascripts/usage_ping_consent.js new file mode 100644 index 00000000000..ae3fde190e3 --- /dev/null +++ b/app/assets/javascripts/usage_ping_consent.js @@ -0,0 +1,30 @@ +import $ from 'jquery'; +import axios from './lib/utils/axios_utils'; +import Flash, { hideFlash } from './flash'; +import { convertPermissionToBoolean } from './lib/utils/common_utils'; + +export default () => { + $('body').on('click', '.js-usage-consent-action', (e) => { + e.preventDefault(); + e.stopImmediatePropagation(); // overwrite rails listener + + const { url, checkEnabled, pingEnabled } = e.target.dataset; + const data = { + application_setting: { + version_check_enabled: convertPermissionToBoolean(checkEnabled), + usage_ping_enabled: convertPermissionToBoolean(pingEnabled), + }, + }; + + const hideConsentMessage = () => hideFlash(document.querySelector('.ping-consent-message')); + + axios.put(url, data) + .then(() => { + hideConsentMessage(); + }) + .catch(() => { + hideConsentMessage(); + Flash('Something went wrong. Try again later.'); + }); + }); +}; diff --git a/app/assets/stylesheets/framework/avatar.scss b/app/assets/stylesheets/framework/avatar.scss index 9dd0384a228..a1349c61542 100644 --- a/app/assets/stylesheets/framework/avatar.scss +++ b/app/assets/stylesheets/framework/avatar.scss @@ -104,6 +104,7 @@ a { width: 100%; + height: 100%; display: flex; } diff --git a/app/assets/stylesheets/framework/buttons.scss b/app/assets/stylesheets/framework/buttons.scss index e91e830fcac..f1314821c69 100644 --- a/app/assets/stylesheets/framework/buttons.scss +++ b/app/assets/stylesheets/framework/buttons.scss @@ -166,6 +166,10 @@ @include btn-outline($white-light, $red-500, $red-500, $red-500, $white-light, $red-600, $red-600, $red-700); } + &.btn-warning { + @include btn-outline($white-light, $orange-500, $orange-500, $orange-500, $white-light, $orange-600, $orange-600, $orange-700); + } + &.btn-primary, &.btn-info { @include btn-outline($white-light, $blue-500, $blue-500, $blue-500, $white-light, $blue-600, $blue-600, $blue-700); diff --git a/app/assets/stylesheets/framework/layout.scss b/app/assets/stylesheets/framework/layout.scss index d4bae4cb137..9218df9b40f 100644 --- a/app/assets/stylesheets/framework/layout.scss +++ b/app/assets/stylesheets/framework/layout.scss @@ -69,10 +69,14 @@ body { float: right; } - /* Center alert text and alert action links on smaller screens */ - @include media-breakpoint-down(sm) { - .alert { - text-align: center; + .flex-alert { + @include media-breakpoint-up(lg) { + display: flex; + + .alert-message { + flex: 1; + padding-right: 40px; + } } .alert-link-group { @@ -80,6 +84,13 @@ body { } } + @include media-breakpoint-down(sm) { + .alert-link-group { + float: none; + margin-top: $gl-padding-8; + } + } + /* Stripe the background colors so that adjacent alert-warnings are distinct from one another */ .alert-warning { transition: background-color 0.15s, border-color 0.15s; diff --git a/app/assets/stylesheets/page_bundles/ide.scss b/app/assets/stylesheets/page_bundles/ide.scss index 5ff4e487d04..45df8391f9a 100644 --- a/app/assets/stylesheets/page_bundles/ide.scss +++ b/app/assets/stylesheets/page_bundles/ide.scss @@ -7,6 +7,8 @@ $ide-context-header-padding: 10px; $ide-project-avatar-end: $ide-context-header-padding + 48px; $ide-tree-padding: $gl-padding; $ide-tree-text-start: $ide-activity-bar-width + $ide-tree-padding; +$ide-commit-row-height: 32px; +$ide-commit-header-height: 48px; .project-refs-form, .project-refs-target-form { @@ -567,24 +569,11 @@ $ide-tree-text-start: $ide-activity-bar-width + $ide-tree-padding; } .multi-file-commit-panel-header { - display: flex; - align-items: center; - margin-bottom: 0; + height: $ide-commit-header-height; border-bottom: 1px solid $white-dark; padding: 12px 0; } -.multi-file-commit-panel-header-title { - display: flex; - flex: 1; - align-items: center; - - svg { - margin-right: $gl-btn-padding; - color: $theme-gray-700; - } -} - .multi-file-commit-panel-collapse-btn { border-left: 1px solid $white-dark; margin-left: auto; @@ -594,8 +583,6 @@ $ide-tree-text-start: $ide-activity-bar-width + $ide-tree-padding; flex: 1; overflow: auto; padding: $grid-size 0; - margin-left: -$grid-size; - margin-right: -$grid-size; min-height: 60px; &.form-text.text-muted { @@ -660,6 +647,8 @@ $ide-tree-text-start: $ide-activity-bar-width + $ide-tree-padding; .multi-file-commit-list-path { cursor: pointer; + height: $ide-commit-row-height; + padding-right: 0; &.is-active { background-color: $white-normal; @@ -668,6 +657,12 @@ $ide-tree-text-start: $ide-activity-bar-width + $ide-tree-padding; &:hover, &:focus { outline: 0; + + .multi-file-discard-btn { + > .btn { + display: flex; + } + } } svg { @@ -679,6 +674,7 @@ $ide-tree-text-start: $ide-activity-bar-width + $ide-tree-padding; .multi-file-commit-list-file-path { @include str-truncated(calc(100% - 30px)); + user-select: none; &:active { text-decoration: none; @@ -686,9 +682,11 @@ $ide-tree-text-start: $ide-activity-bar-width + $ide-tree-padding; } .multi-file-discard-btn { - top: 4px; - right: 8px; - bottom: 4px; + > .btn { + display: none; + width: $ide-commit-row-height; + height: $ide-commit-row-height; + } svg { top: 0; @@ -807,10 +805,9 @@ $ide-tree-text-start: $ide-activity-bar-width + $ide-tree-padding; } .ide-staged-action-btn { - width: 22px; - margin-left: -1px; - border-top-left-radius: 0; - border-bottom-left-radius: 0; + width: $ide-commit-row-height; + height: $ide-commit-row-height; + color: inherit; > svg { top: 0; @@ -1442,3 +1439,29 @@ $ide-tree-text-start: $ide-activity-bar-width + $ide-tree-padding; top: 50%; transform: translateY(-50%); } + +.ide-file-templates { + padding: $grid-size $gl-padding; + background-color: $gray-light; + border-bottom: 1px solid $white-dark; + + .dropdown { + min-width: 180px; + } + + .dropdown-content { + max-height: 222px; + } +} + +.ide-commit-editor-header { + height: 65px; + padding: 8px 16px; + background-color: $theme-gray-50; + box-shadow: inset 0 -1px $white-dark; +} + +.ide-commit-list-changed-icon { + width: $ide-commit-row-height; + height: $ide-commit-row-height; +} diff --git a/app/controllers/admin/application_settings_controller.rb b/app/controllers/admin/application_settings_controller.rb index 9723e400574..869213d61f1 100644 --- a/app/controllers/admin/application_settings_controller.rb +++ b/app/controllers/admin/application_settings_controller.rb @@ -9,11 +9,18 @@ class Admin::ApplicationSettingsController < Admin::ApplicationController .new(@application_setting, current_user, application_setting_params) .execute - if successful - redirect_to admin_application_settings_path, - notice: 'Application settings saved successfully' - else - render :show + if recheck_user_consent? + session[:ask_for_usage_stats_consent] = current_user.requires_usage_stats_consent? + end + + respond_to do |format| + if successful + format.json { head :ok } + format.html { redirect_to admin_application_settings_path, notice: 'Application settings saved successfully' } + else + format.json { head :bad_request } + format.html { render :show } + end end end @@ -76,6 +83,13 @@ class Admin::ApplicationSettingsController < Admin::ApplicationController ) end + def recheck_user_consent? + return false unless session[:ask_for_usage_stats_consent] + return false unless params[:application_setting] + + params[:application_setting].key?(:usage_ping_enabled) || params[:application_setting].key?(:version_check_enabled) + end + def visible_application_setting_attributes ApplicationSettingsHelper.visible_attributes + [ :domain_blacklist_file, diff --git a/app/controllers/application_controller.rb b/app/controllers/application_controller.rb index 7cd68d6b92a..7e2b2cf3ad3 100644 --- a/app/controllers/application_controller.rb +++ b/app/controllers/application_controller.rb @@ -22,6 +22,7 @@ class ApplicationController < ActionController::Base before_action :add_gon_variables, unless: [:peek_request?, :json_request?] before_action :configure_permitted_parameters, if: :devise_controller? before_action :require_email, unless: :devise_controller? + before_action :set_usage_stats_consent_flag around_action :set_locale @@ -434,4 +435,29 @@ class ApplicationController < ActionController::Base !(peek_request? || devise_controller?) end + + def set_usage_stats_consent_flag + return unless current_user + return if sessionless_user? + return if session.has_key?(:ask_for_usage_stats_consent) + + session[:ask_for_usage_stats_consent] = current_user.requires_usage_stats_consent? + + if session[:ask_for_usage_stats_consent] + disable_usage_stats + end + end + + def disable_usage_stats + application_setting_params = { + usage_ping_enabled: false, + version_check_enabled: false, + skip_usage_stats_user: true + } + settings = Gitlab::CurrentSettings.current_application_settings + + ApplicationSettings::UpdateService + .new(settings, current_user, application_setting_params) + .execute + end end diff --git a/app/controllers/concerns/issuable_actions.rb b/app/controllers/concerns/issuable_actions.rb index 37e03d70b6f..7b6e5bcb5f1 100644 --- a/app/controllers/concerns/issuable_actions.rb +++ b/app/controllers/concerns/issuable_actions.rb @@ -95,6 +95,7 @@ module IssuableActions .includes(:noteable) .fresh + notes = ResourceEvents::MergeIntoNotesService.new(issuable, current_user).execute(notes) notes = prepare_notes_for_rendering(notes) notes = notes.reject { |n| n.cross_reference_not_visible_for?(current_user) } diff --git a/app/controllers/concerns/notes_actions.rb b/app/controllers/concerns/notes_actions.rb index 5127db3f5fb..b63f2eb85f0 100644 --- a/app/controllers/concerns/notes_actions.rb +++ b/app/controllers/concerns/notes_actions.rb @@ -18,6 +18,7 @@ module NotesActions notes = notes_finder.execute .inc_relations_for_view + notes = ResourceEvents::MergeIntoNotesService.new(noteable, current_user, last_fetched_at: current_fetched_at).execute(notes) notes = prepare_notes_for_rendering(notes) notes = notes.reject { |n| n.cross_reference_not_visible_for?(current_user) } diff --git a/app/controllers/groups/labels_controller.rb b/app/controllers/groups/labels_controller.rb index 3e0076ac935..e95123c0933 100644 --- a/app/controllers/groups/labels_controller.rb +++ b/app/controllers/groups/labels_controller.rb @@ -2,7 +2,6 @@ class Groups::LabelsController < Groups::ApplicationController include ToggleSubscriptionAction before_action :label, only: [:edit, :update, :destroy] - before_action :available_labels, only: [:index] before_action :authorize_admin_labels!, only: [:new, :create, :edit, :update, :destroy] before_action :save_previous_label_path, only: [:edit] @@ -11,10 +10,12 @@ class Groups::LabelsController < Groups::ApplicationController def index respond_to do |format| format.html do - @labels = @available_labels.page(params[:page]) + @labels = @group.labels + .optionally_search(params[:search]) + .page(params[:page]) end format.json do - render json: LabelSerializer.new.represent_appearance(@available_labels) + render json: LabelSerializer.new.represent_appearance(available_labels) end end end diff --git a/app/controllers/profiles_controller.rb b/app/controllers/profiles_controller.rb index 6f50cbb4a36..5671663f81e 100644 --- a/app/controllers/profiles_controller.rb +++ b/app/controllers/profiles_controller.rb @@ -101,6 +101,7 @@ class ProfilesController < Profiles::ApplicationController :organization, :preferred_language, :private_profile, + :include_private_contributions, status: [:emoji, :message] ) end diff --git a/app/controllers/projects/merge_requests/diffs_controller.rb b/app/controllers/projects/merge_requests/diffs_controller.rb index 48e02581d54..34de554212f 100644 --- a/app/controllers/projects/merge_requests/diffs_controller.rb +++ b/app/controllers/projects/merge_requests/diffs_controller.rb @@ -21,6 +21,8 @@ class Projects::MergeRequests::DiffsController < Projects::MergeRequests::Applic def render_diffs @environment = @merge_request.environments_for(current_user).last + @diffs.write_cache + render json: DiffsSerializer.new(current_user: current_user).represent(@diffs, additional_attributes) end diff --git a/app/controllers/projects/registry/repositories_controller.rb b/app/controllers/projects/registry/repositories_controller.rb index 32c0fc6d14a..ef0433795f4 100644 --- a/app/controllers/projects/registry/repositories_controller.rb +++ b/app/controllers/projects/registry/repositories_controller.rb @@ -18,14 +18,10 @@ module Projects end def destroy - if image.destroy - respond_to do |format| - format.json { head :no_content } - end - else - respond_to do |format| - format.json { head :bad_request } - end + DeleteContainerRepositoryWorker.perform_async(current_user.id, image.id) + + respond_to do |format| + format.json { head :no_content } end end @@ -41,10 +37,10 @@ module Projects # Needed to maintain a backwards compatibility. # def ensure_root_container_repository! - ContainerRegistry::Path.new(@project.full_path).tap do |path| + ::ContainerRegistry::Path.new(@project.full_path).tap do |path| break if path.has_repository? - ContainerRepository.build_from_path(path).tap do |repository| + ::ContainerRepository.build_from_path(path).tap do |repository| repository.save! if repository.has_tags? end end diff --git a/app/controllers/projects_controller.rb b/app/controllers/projects_controller.rb index 0eaf9f94e37..98076791ab9 100644 --- a/app/controllers/projects_controller.rb +++ b/app/controllers/projects_controller.rb @@ -191,10 +191,8 @@ class ProjectsController < Projects::ApplicationController end def download_export - if export_project_object_storage? - send_upload(@project.import_export_upload.export_file) - elsif export_project_path - send_file export_project_path, disposition: 'attachment' + if @project.export_file_exists? + send_upload(@project.export_file) else redirect_to( edit_project_path(@project, anchor: 'js-export-project'), @@ -425,12 +423,4 @@ class ProjectsController < Projects::ApplicationController def whitelist_query_limiting Gitlab::QueryLimiting.whitelist('https://gitlab.com/gitlab-org/gitlab-ce/issues/42440') end - - def export_project_path - @export_project_path ||= @project.export_project_path - end - - def export_project_object_storage? - @project.export_project_object_exists? - end end diff --git a/app/finders/user_recent_events_finder.rb b/app/finders/user_recent_events_finder.rb index 876f086a3ef..b874f6959c9 100644 --- a/app/finders/user_recent_events_finder.rb +++ b/app/finders/user_recent_events_finder.rb @@ -48,20 +48,6 @@ class UserRecentEventsFinder end def projects - # Compile a list of projects `current_user` interacted with - # and `target_user` is allowed to see. - - authorized = target_user - .project_interactions - .joins(:project_authorizations) - .where(project_authorizations: { user: current_user }) - .select(:id) - - visible = target_user - .project_interactions - .where(visibility_level: Gitlab::VisibilityLevel.levels_for_user(current_user)) - .select(:id) - - Gitlab::SQL::Union.new([authorized, visible]).to_sql + target_user.project_interactions.to_sql end end diff --git a/app/helpers/application_settings_helper.rb b/app/helpers/application_settings_helper.rb index 684c84c3006..90fbf49be4a 100644 --- a/app/helpers/application_settings_helper.rb +++ b/app/helpers/application_settings_helper.rb @@ -220,6 +220,7 @@ module ApplicationSettingsHelper :recaptcha_enabled, :recaptcha_private_key, :recaptcha_site_key, + :receive_max_input_size, :repository_checks_enabled, :repository_storages, :require_two_factor_authentication, diff --git a/app/helpers/events_helper.rb b/app/helpers/events_helper.rb index 269acf5b2e2..34d54e2d681 100644 --- a/app/helpers/events_helper.rb +++ b/app/helpers/events_helper.rb @@ -19,7 +19,7 @@ module EventsHelper name = self_added ? 'You' : author.name link_to name, user_path(author.username), title: name else - event.author_name + escape_once(event.author_name) end end diff --git a/app/helpers/notes_helper.rb b/app/helpers/notes_helper.rb index 5404ead44f3..6285b43f917 100644 --- a/app/helpers/notes_helper.rb +++ b/app/helpers/notes_helper.rb @@ -108,7 +108,7 @@ module NotesHelper end def noteable_note_url(note) - Gitlab::UrlBuilder.build(note) + Gitlab::UrlBuilder.build(note) if note.id end def form_resources diff --git a/app/helpers/version_check_helper.rb b/app/helpers/version_check_helper.rb index c20753ece72..a7fef93e7f1 100644 --- a/app/helpers/version_check_helper.rb +++ b/app/helpers/version_check_helper.rb @@ -1,8 +1,10 @@ module VersionCheckHelper def version_status_badge - if Rails.env.production? && Gitlab::CurrentSettings.version_check_enabled - image_url = VersionCheck.new.url - image_tag image_url, class: 'js-version-status-badge' - end + return unless Rails.env.production? + return unless Gitlab::CurrentSettings.version_check_enabled + return if User.single_user&.requires_usage_stats_consent? + + image_url = VersionCheck.new.url + image_tag image_url, class: 'js-version-status-badge' end end diff --git a/app/models/application_setting.rb b/app/models/application_setting.rb index 03bd7fa016e..d8536c5512d 100644 --- a/app/models/application_setting.rb +++ b/app/models/application_setting.rb @@ -302,7 +302,8 @@ class ApplicationSetting < ActiveRecord::Base instance_statistics_visibility_private: false, user_default_external: false, user_default_internal_regex: nil, - user_show_add_ssh_key_message: true + user_show_add_ssh_key_message: true, + usage_stats_set_by_user_id: nil } end diff --git a/app/models/concerns/issuable.rb b/app/models/concerns/issuable.rb index 7f14d78e976..5f65fceb7af 100644 --- a/app/models/concerns/issuable.rb +++ b/app/models/concerns/issuable.rb @@ -109,10 +109,6 @@ module Issuable false end - def etag_caching_enabled? - false - end - def has_multiple_assignees? assignees.count > 1 end diff --git a/app/models/concerns/noteable.rb b/app/models/concerns/noteable.rb index ce778eae271..098eed137ba 100644 --- a/app/models/concerns/noteable.rb +++ b/app/models/concerns/noteable.rb @@ -82,4 +82,23 @@ module Noteable def lockable? [MergeRequest, Issue].include?(self.class) end + + def etag_caching_enabled? + false + end + + def expire_note_etag_cache + return unless discussions_rendered_on_frontend? + return unless etag_caching_enabled? + + Gitlab::EtagCaching::Store.new.touch(note_etag_key) + end + + def note_etag_key + Gitlab::Routing.url_helpers.project_noteable_notes_path( + project, + target_type: self.class.name.underscore, + target_id: id + ) + end end diff --git a/app/models/concerns/storage/legacy_namespace.rb b/app/models/concerns/storage/legacy_namespace.rb index 3b745657a9e..9785011720a 100644 --- a/app/models/concerns/storage/legacy_namespace.rb +++ b/app/models/concerns/storage/legacy_namespace.rb @@ -25,8 +25,6 @@ module Storage Gitlab::PagesTransfer.new.rename_namespace(full_path_was, full_path) end - remove_exports! - # If repositories moved successfully we need to # send update instructions to users. # However we cannot allow rollback since we moved namespace dir @@ -101,8 +99,6 @@ module Storage end end end - - remove_exports! end def remove_legacy_exports! diff --git a/app/models/event.rb b/app/models/event.rb index ba28866e8e6..041dac6941b 100644 --- a/app/models/event.rb +++ b/app/models/event.rb @@ -151,15 +151,17 @@ class Event < ActiveRecord::Base if push? || commit_note? Ability.allowed?(user, :download_code, project) elsif membership_changed? - true + Ability.allowed?(user, :read_project, project) elsif created_project? - true + Ability.allowed?(user, :read_project, project) elsif issue? || issue_note? Ability.allowed?(user, :read_issue, note? ? note_target : target) elsif merge_request? || merge_request_note? Ability.allowed?(user, :read_merge_request, note? ? note_target : target) + elsif milestone? + Ability.allowed?(user, :read_project, project) else - milestone? + false # No other event types are visible end end diff --git a/app/models/label.rb b/app/models/label.rb index 96c1515b41a..8db7c3abd10 100644 --- a/app/models/label.rb +++ b/app/models/label.rb @@ -5,6 +5,7 @@ class Label < ActiveRecord::Base include Referable include Subscribable include Gitlab::SQL::Pattern + include OptionallySearch # Represents a "No Label" state used for filtering Issues and Merge # Requests that have no label assigned. diff --git a/app/models/label_note.rb b/app/models/label_note.rb new file mode 100644 index 00000000000..680952cf421 --- /dev/null +++ b/app/models/label_note.rb @@ -0,0 +1,97 @@ +# frozen_string_literal: true + +class LabelNote < Note + attr_accessor :resource_parent + attr_reader :events + + def self.from_events(events, resource: nil, resource_parent: nil) + resource ||= events.first.issuable + + attrs = { + system: true, + author: events.first.user, + created_at: events.first.created_at, + discussion_id: events.first.discussion_id, + noteable: resource, + system_note_metadata: SystemNoteMetadata.new(action: 'label'), + events: events, + resource_parent: resource_parent + } + + if resource_parent.is_a?(Project) + attrs[:project_id] = resource_parent.id + end + + LabelNote.new(attrs) + end + + def events=(events) + @events = events + + update_outdated_markdown + end + + def cached_html_up_to_date?(markdown_field) + true + end + + def note + @note ||= note_text + end + + def note_html + @note_html ||= "<p dir=\"auto\">#{note_text(html: true)}</p>" + end + + def project + resource_parent if resource_parent.is_a?(Project) + end + + def group + resource_parent if resource_parent.is_a?(Group) + end + + private + + def update_outdated_markdown + events.each do |event| + if event.outdated_markdown? + event.refresh_invalid_reference + end + end + end + + def note_text(html: false) + added = labels_str('added', label_refs_by_action('add', html)) + removed = labels_str('removed', label_refs_by_action('remove', html)) + + [added, removed].compact.join(' and ') + end + + # returns string containing added/removed labels including + # count of deleted labels: + # + # added ~1 ~2 + 1 deleted label + # added 3 deleted labels + # added ~1 ~2 labels + def labels_str(prefix, label_refs) + existing_refs = label_refs.select { |ref| ref.present? }.sort + refs_str = existing_refs.empty? ? nil : existing_refs.join(' ') + + deleted = label_refs.count - existing_refs.count + deleted_str = deleted == 0 ? nil : "#{deleted} deleted" + + return nil unless refs_str || deleted_str + + label_list_str = [refs_str, deleted_str].compact.join(' + ') + suffix = 'label'.pluralize(deleted > 0 ? deleted : existing_refs.count) + + "#{prefix} #{label_list_str} #{suffix}" + end + + def label_refs_by_action(action, html) + field = html ? :reference_html : :reference + + events.select { |e| e.action == action }.map(&field) + end +end diff --git a/app/models/namespace.rb b/app/models/namespace.rb index 0deb44d7916..76920c3c039 100644 --- a/app/models/namespace.rb +++ b/app/models/namespace.rb @@ -253,18 +253,6 @@ class Namespace < ActiveRecord::Base end end - # Exports belonging to projects with legacy storage are placed in a common - # subdirectory of the namespace, so a simple `rm -rf` is sufficient to remove - # them. - # - # Exports of projects using hashed storage are placed in a location defined - # only by the project ID, so each must be removed individually. - def remove_exports! - remove_legacy_exports! - - all_projects.with_storage_feature(:repository).find_each(&:remove_exports) - end - def refresh_project_authorizations owner.refresh_authorized_projects end diff --git a/app/models/note.rb b/app/models/note.rb index 2e343b8f9f8..8f090cc31e6 100644 --- a/app/models/note.rb +++ b/app/models/note.rb @@ -389,18 +389,7 @@ class Note < ActiveRecord::Base end def expire_etag_cache - return unless noteable&.discussions_rendered_on_frontend? - return unless noteable&.etag_caching_enabled? - - Gitlab::EtagCaching::Store.new.touch(etag_key) - end - - def etag_key - Gitlab::Routing.url_helpers.project_noteable_notes_path( - project, - target_type: noteable_type.underscore, - target_id: noteable_id - ) + noteable&.expire_note_etag_cache end def touch(*args) diff --git a/app/models/project.rb b/app/models/project.rb index 97d9fa355ef..45cf527d7c6 100644 --- a/app/models/project.rb +++ b/app/models/project.rb @@ -232,6 +232,8 @@ class Project < ActiveRecord::Base has_many :clusters, through: :cluster_project, class_name: 'Clusters::Cluster' has_many :cluster_ingresses, through: :clusters, source: :application_ingress, class_name: 'Clusters::Applications::Ingress' + has_many :prometheus_metrics + # Container repositories need to remove data from the container registry, # which is not managed by the DB. Hence we're still using dependent: :destroy # here. @@ -1733,16 +1735,12 @@ class Project < ActiveRecord::Base import_export_shared.archive_path end - def export_project_path - Dir.glob("#{export_path}/*export.tar.gz").max_by { |f| File.ctime(f) } - end - def export_status if export_in_progress? :started elsif after_export_in_progress? :after_export_action - elsif export_project_path || export_project_object_exists? + elsif export_file_exists? :finished else :none @@ -1757,21 +1755,19 @@ class Project < ActiveRecord::Base import_export_shared.after_export_in_progress? end - def remove_exports(path = export_path) - if path.present? - FileUtils.rm_rf(path) - elsif export_project_object_exists? - import_export_upload.remove_export_file! - import_export_upload.save - end + def remove_exports + return unless export_file_exists? + + import_export_upload.remove_export_file! + import_export_upload.save end - def remove_exported_project_file - remove_exports(export_project_path) + def export_file_exists? + export_file&.file end - def export_project_object_exists? - Gitlab::ImportExport.object_storage? && import_export_upload&.export_file&.file + def export_file + import_export_upload&.export_file end def full_path_slug diff --git a/app/models/prometheus_metric.rb b/app/models/prometheus_metric.rb new file mode 100644 index 00000000000..ce2db9cb44c --- /dev/null +++ b/app/models/prometheus_metric.rb @@ -0,0 +1,89 @@ +# frozen_string_literal: true + +class PrometheusMetric < ActiveRecord::Base + belongs_to :project, validate: true, inverse_of: :prometheus_metrics + + enum group: { + # built-in groups + nginx_ingress: -1, + ha_proxy: -2, + aws_elb: -3, + nginx: -4, + kubernetes: -5, + + # custom/user groups + business: 0, + response: 1, + system: 2 + } + + validates :title, presence: true + validates :query, presence: true + validates :group, presence: true + validates :y_label, presence: true + validates :unit, presence: true + + validates :project, presence: true, unless: :common? + validates :project, absence: true, if: :common? + + scope :common, -> { where(common: true) } + + GROUP_TITLES = { + # built-in groups + nginx_ingress: _('Response metrics (NGINX Ingress)'), + ha_proxy: _('Response metrics (HA Proxy)'), + aws_elb: _('Response metrics (AWS ELB)'), + nginx: _('Response metrics (NGINX)'), + kubernetes: _('System metrics (Kubernetes)'), + + # custom/user groups + business: _('Business metrics (Custom)'), + response: _('Response metrics (Custom)'), + system: _('System metrics (Custom)') + }.freeze + + REQUIRED_METRICS = { + nginx_ingress: %w(nginx_upstream_responses_total nginx_upstream_response_msecs_avg), + ha_proxy: %w(haproxy_frontend_http_requests_total haproxy_frontend_http_responses_total), + aws_elb: %w(aws_elb_request_count_sum aws_elb_latency_average aws_elb_httpcode_backend_5_xx_sum), + nginx: %w(nginx_server_requests nginx_server_requestMsec), + kubernetes: %w(container_memory_usage_bytes container_cpu_usage_seconds_total) + }.freeze + + def group_title + GROUP_TITLES[group.to_sym] + end + + def required_metrics + REQUIRED_METRICS[group.to_sym].to_a.map(&:to_s) + end + + def to_query_metric + Gitlab::Prometheus::Metric.new(id: id, title: title, required_metrics: required_metrics, weight: 0, y_label: y_label, queries: queries) + end + + def queries + [ + { + query_range: query, + unit: unit, + label: legend, + series: query_series + }.compact + ] + end + + def query_series + case legend + when 'Status Code' + [{ + label: 'status_code', + when: [ + { value: '2xx', color: 'green' }, + { value: '4xx', color: 'orange' }, + { value: '5xx', color: 'red' } + ] + }] + end + end +end diff --git a/app/models/resource_label_event.rb b/app/models/resource_label_event.rb index 42c255fcd1e..3fd96b9dc18 100644 --- a/app/models/resource_label_event.rb +++ b/app/models/resource_label_event.rb @@ -3,33 +3,122 @@ # This model is not used yet, it will be used for: # https://gitlab.com/gitlab-org/gitlab-ce/issues/48483 class ResourceLabelEvent < ActiveRecord::Base + include Importable + include Gitlab::Utils::StrongMemoize + include CacheMarkdownField + + cache_markdown_field :reference + belongs_to :user belongs_to :issue belongs_to :merge_request belongs_to :label - validates :user, presence: true, on: :create - validates :label, presence: true, on: :create + scope :created_after, ->(time) { where('created_at > ?', time) } + + validates :user, presence: { unless: :importing? }, on: :create + validates :label, presence: { unless: :importing? }, on: :create validate :exactly_one_issuable + after_save :expire_etag_cache + after_destroy :expire_etag_cache + enum action: { add: 1, remove: 2 } - def self.issuable_columns - %i(issue_id merge_request_id).freeze + def self.issuable_attrs + %i(issue merge_request).freeze end def issuable issue || merge_request end + # create same discussion id for all actions with the same user and time + def discussion_id(resource = nil) + strong_memoize(:discussion_id) do + Digest::SHA1.hexdigest([self.class.name, created_at, user_id].join("-")) + end + end + + def project + issuable.project + end + + def group + issuable.group if issuable.respond_to?(:group) + end + + def outdated_markdown? + return true if label_id.nil? && reference.present? + + reference.nil? || latest_cached_markdown_version != cached_markdown_version + end + + def banzai_render_context(field) + super.merge(pipeline: 'label', only_path: true) + end + + def refresh_invalid_reference + # label_id could be nullified on label delete + self.reference = '' if label_id.nil? + + # reference is not set for events which were not rendered yet + self.reference ||= label_reference + + if changed? + save + elsif invalidated_markdown_cache? + refresh_markdown_cache! + end + end + private + def label_reference + if local_label? + label.to_reference(format: :id) + elsif label.is_a?(GroupLabel) + label.to_reference(label.group, target_project: resource_parent, format: :id) + else + label.to_reference(resource_parent, format: :id) + end + end + def exactly_one_issuable - if self.class.issuable_columns.count { |attr| self[attr] } != 1 - errors.add(:base, "Exactly one of #{self.class.issuable_columns.join(', ')} is required") + issuable_count = self.class.issuable_attrs.count { |attr| self["#{attr}_id"] } + + return true if issuable_count == 1 + + # if none of issuable IDs is set, check explicitly if nested issuable + # object is set, this is used during project import + if issuable_count == 0 && importing? + issuable_count = self.class.issuable_attrs.count { |attr| self.public_send(attr) } # rubocop:disable GitlabSecurity/PublicSend + + return true if issuable_count == 1 end + + errors.add(:base, "Exactly one of #{self.class.issuable_attrs.join(', ')} is required") + end + + def expire_etag_cache + issuable.expire_note_etag_cache + end + + def local_label? + params = { include_ancestor_groups: true } + if resource_parent.is_a?(Project) + params[:project_id] = resource_parent.id + else + params[:group_id] = resource_parent.id + end + + LabelsFinder.new(nil, params).execute(skip_authorization: true).where(id: label.id).any? + end + + def resource_parent + issuable.project || issuable.group end end diff --git a/app/models/user.rb b/app/models/user.rb index 0fcc952b5cd..568ec101016 100644 --- a/app/models/user.rb +++ b/app/models/user.rb @@ -490,6 +490,16 @@ class User < ActiveRecord::Base u.name = 'Ghost User' end end + + # Return true if there is only single non-internal user in the deployment, + # ghost user is ignored. + def single_user? + User.non_internal.limit(2).count == 1 + end + + def single_user + User.non_internal.first if single_user? + end end def full_path @@ -1287,6 +1297,10 @@ class User < ActiveRecord::Base !terms_accepted? end + def requires_usage_stats_consent? + !consented_usage_stats? && 7.days.ago > self.created_at && !has_current_license? && User.single_user? + end + # @deprecated alias_method :owned_or_masters_groups, :owned_or_maintainers_groups @@ -1301,6 +1315,14 @@ class User < ActiveRecord::Base private + def has_current_license? + false + end + + def consented_usage_stats? + Gitlab::CurrentSettings.usage_stats_set_by_user_id == self.id + end + def owned_projects_union Gitlab::SQL::Union.new([ Project.where(namespace: namespace), diff --git a/app/presenters/commit_status_presenter.rb b/app/presenters/commit_status_presenter.rb index a08f34e2335..65e77ea3f92 100644 --- a/app/presenters/commit_status_presenter.rb +++ b/app/presenters/commit_status_presenter.rb @@ -11,10 +11,16 @@ class CommitStatusPresenter < Gitlab::View::Presenter::Delegated runner_unsupported: 'Your runner is outdated, please upgrade your runner' }.freeze + private_constant :CALLOUT_FAILURE_MESSAGES + presents :build + def self.callout_failure_messages + CALLOUT_FAILURE_MESSAGES + end + def callout_failure_message - CALLOUT_FAILURE_MESSAGES.fetch(failure_reason.to_sym) + self.class.callout_failure_messages.fetch(failure_reason.to_sym) end def recoverable? diff --git a/app/serializers/note_entity.rb b/app/serializers/note_entity.rb index daa5c24d0f5..c6d27817411 100644 --- a/app/serializers/note_entity.rb +++ b/app/serializers/note_entity.rb @@ -4,6 +4,12 @@ class NoteEntity < API::Entities::Note include RequestAwareEntity include NotesHelper + expose :id do |note| + # resource events are represented as notes too, but don't + # have ID, discussion ID is used for them instead + note.id ? note.id.to_s : note.discussion_id + end + expose :type expose :author, using: NoteUserEntity @@ -46,8 +52,8 @@ class NoteEntity < API::Entities::Note expose :emoji_awardable?, as: :emoji_awardable expose :award_emoji, if: -> (note, _) { note.emoji_awardable? }, using: AwardEmojiEntity - expose :report_abuse_path do |note| - new_abuse_report_path(user_id: note.author.id, ref_url: Gitlab::UrlBuilder.build(note)) + expose :report_abuse_path, if: -> (note, _) { note.author_id } do |note| + new_abuse_report_path(user_id: note.author_id, ref_url: Gitlab::UrlBuilder.build(note)) end expose :noteable_note_url do |note| diff --git a/app/serializers/project_note_entity.rb b/app/serializers/project_note_entity.rb index d7c4d0aacc6..f6cdea1d8b5 100644 --- a/app/serializers/project_note_entity.rb +++ b/app/serializers/project_note_entity.rb @@ -1,7 +1,7 @@ # frozen_string_literal: true class ProjectNoteEntity < NoteEntity - expose :human_access do |note| + expose :human_access, if: -> (note, _) { note.project.present? } do |note| note.project.team.human_max_access(note.author_id) end @@ -9,7 +9,7 @@ class ProjectNoteEntity < NoteEntity toggle_award_emoji_project_note_path(note.project, note.id) end - expose :path do |note| + expose :path, if: -> (note, _) { note.id } do |note| project_note_path(note.project, note) end diff --git a/app/services/application_settings/update_service.rb b/app/services/application_settings/update_service.rb index 19cf34e2ac4..2e4643ed668 100644 --- a/app/services/application_settings/update_service.rb +++ b/app/services/application_settings/update_service.rb @@ -11,11 +11,19 @@ module ApplicationSettings params[:performance_bar_allowed_group_id] = performance_bar_allowed_group_id end + if usage_stats_updated? && !params.delete(:skip_usage_stats_user) + params[:usage_stats_set_by_user_id] = current_user.id + end + @application_setting.update(@params) end private + def usage_stats_updated? + params.key?(:usage_ping_enabled) || params.key?(:version_check_enabled) + end + def update_terms(terms) return unless terms.present? diff --git a/app/services/issuable/common_system_notes_service.rb b/app/services/issuable/common_system_notes_service.rb index 028b350ca07..ab53c38aa3a 100644 --- a/app/services/issuable/common_system_notes_service.rb +++ b/app/services/issuable/common_system_notes_service.rb @@ -55,7 +55,9 @@ module Issuable added_labels = issuable.labels - old_labels removed_labels = old_labels - issuable.labels - SystemNoteService.change_label(issuable, issuable.project, current_user, added_labels, removed_labels) + ResourceEvents::ChangeLabelsService + .new(issuable, current_user) + .execute(added_labels: added_labels, removed_labels: removed_labels) end def create_title_change_note(old_title) diff --git a/app/services/issues/move_service.rb b/app/services/issues/move_service.rb index 841bce9949e..c52aa577dd8 100644 --- a/app/services/issues/move_service.rb +++ b/app/services/issues/move_service.rb @@ -36,6 +36,7 @@ module Issues def update_new_issue rewrite_notes + copy_resource_label_events rewrite_issue_award_emoji add_note_moved_from end @@ -96,6 +97,18 @@ module Issues end end + def copy_resource_label_events + @old_issue.resource_label_events.find_in_batches do |batch| + events = batch.map do |event| + event.attributes + .except('id', 'reference', 'reference_html') + .merge('issue_id' => @new_issue.id, 'created_at' => event.created_at) + end + + Gitlab::Database.bulk_insert(ResourceLabelEvent.table_name, events) + end + end + def rewrite_issue_award_emoji rewrite_award_emoji(@old_issue, @new_issue) end diff --git a/app/services/labels/promote_service.rb b/app/services/labels/promote_service.rb index 623a5f0950e..fcdcea2d0ea 100644 --- a/app/services/labels/promote_service.rb +++ b/app/services/labels/promote_service.rb @@ -13,6 +13,7 @@ module Labels label_ids_for_merge(new_label).find_in_batches(batch_size: BATCH_SIZE) do |batched_ids| update_issuables(new_label, batched_ids) + update_resource_label_events(new_label, batched_ids) update_issue_board_lists(new_label, batched_ids) update_priorities(new_label, batched_ids) subscribe_users(new_label, batched_ids) @@ -52,6 +53,12 @@ module Labels .update_all(label_id: new_label) end + def update_resource_label_events(new_label, label_ids) + ResourceLabelEvent + .where(label: label_ids) + .update_all(label_id: new_label) + end + def update_issue_board_lists(new_label, label_ids) List .where(label: label_ids) diff --git a/app/services/merge_requests/reload_diffs_service.rb b/app/services/merge_requests/reload_diffs_service.rb index 8d85dc9eb5f..1390ae0e199 100644 --- a/app/services/merge_requests/reload_diffs_service.rb +++ b/app/services/merge_requests/reload_diffs_service.rb @@ -30,7 +30,7 @@ module MergeRequests def clear_cache(new_diff) # Executing the iteration we cache highlighted diffs for each diff file of # MergeRequestDiff. - new_diff.diffs_collection.diff_files.to_a + new_diff.diffs_collection.write_cache # 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 @@ -38,7 +38,7 @@ module MergeRequests MergeRequestDiff.where(merge_request: merge_request).each do |merge_request_diff| next if merge_request_diff == new_diff - merge_request_diff.diffs_collection.clear_cache! + merge_request_diff.diffs_collection.clear_cache end end end diff --git a/app/services/projects/container_repository/destroy_service.rb b/app/services/projects/container_repository/destroy_service.rb new file mode 100644 index 00000000000..a8e7eab6068 --- /dev/null +++ b/app/services/projects/container_repository/destroy_service.rb @@ -0,0 +1,13 @@ +# frozen_string_literal: true + +module Projects + module ContainerRepository + class DestroyService < BaseService + def execute(container_repository) + return false unless can?(current_user, :update_container_image, project) + + container_repository.destroy + end + end + end +end diff --git a/app/services/resource_events/change_labels_service.rb b/app/services/resource_events/change_labels_service.rb index 8edb0ddb3ed..039d6e2ebad 100644 --- a/app/services/resource_events/change_labels_service.rb +++ b/app/services/resource_events/change_labels_service.rb @@ -1,7 +1,5 @@ # frozen_string_literal: true -# This service is not used yet, it will be used for: -# https://gitlab.com/gitlab-org/gitlab-ce/issues/48483 module ResourceEvents class ChangeLabelsService attr_reader :resource, :user @@ -25,6 +23,7 @@ module ResourceEvents end Gitlab::Database.bulk_insert(ResourceLabelEvent.table_name, labels) + resource.expire_note_etag_cache end private diff --git a/app/services/resource_events/merge_into_notes_service.rb b/app/services/resource_events/merge_into_notes_service.rb new file mode 100644 index 00000000000..1b02a1602e2 --- /dev/null +++ b/app/services/resource_events/merge_into_notes_service.rb @@ -0,0 +1,55 @@ +# frozen_string_literal: true + +# We store events about issuable label changes in a separate table (not as +# other system notes), but we still want to display notes about label changes +# as classic system notes in UI. This service generates "synthetic" notes for +# label event changes and merges them with classic notes and sorts them by +# creation time. + +module ResourceEvents + class MergeIntoNotesService + include Gitlab::Utils::StrongMemoize + + attr_reader :resource, :current_user, :params + + def initialize(resource, current_user, params = {}) + @resource = resource + @current_user = current_user + @params = params + end + + def execute(notes = []) + (notes + label_notes).sort_by { |n| n.created_at } + end + + private + + def label_notes + label_events_by_discussion_id.map do |discussion_id, events| + LabelNote.from_events(events, resource: resource, resource_parent: resource_parent) + end + end + + def label_events_by_discussion_id + return [] unless resource.respond_to?(:resource_label_events) + + events = resource.resource_label_events.includes(:label, :user) + events = since_fetch_at(events) + + events.group_by { |event| event.discussion_id } + end + + def since_fetch_at(events) + return events unless params[:last_fetched_at].present? + + last_fetched_at = Time.at(params.fetch(:last_fetched_at).to_i) + events.created_after(last_fetched_at - NotesFinder::FETCH_OVERLAP) + end + + def resource_parent + strong_memoize(:resource_parent) do + resource.project || resource.group + end + end + end +end diff --git a/app/services/submit_usage_ping_service.rb b/app/services/submit_usage_ping_service.rb index 93c2e222963..62222d3fd2a 100644 --- a/app/services/submit_usage_ping_service.rb +++ b/app/services/submit_usage_ping_service.rb @@ -15,6 +15,7 @@ class SubmitUsagePingService def execute return false unless Gitlab::CurrentSettings.usage_ping_enabled? + return false if User.single_user&.requires_usage_stats_consent? response = Gitlab::HTTP.post( URL, diff --git a/app/services/system_note_service.rb b/app/services/system_note_service.rb index dda89830179..3ea81445798 100644 --- a/app/services/system_note_service.rb +++ b/app/services/system_note_service.rb @@ -98,47 +98,6 @@ module SystemNoteService create_note(NoteSummary.new(issue, project, author, body, action: 'assignee')) end - # Called when one or more labels on a Noteable are added and/or removed - # - # noteable - Noteable object - # project - Project owning noteable - # author - User performing the change - # added_labels - Array of Labels added - # removed_labels - Array of Labels removed - # - # Example Note text: - # - # "added ~1 and removed ~2 ~3 labels" - # - # "added ~4 label" - # - # "removed ~5 label" - # - # Returns the created Note object - def change_label(noteable, project, author, added_labels, removed_labels) - labels_count = added_labels.count + removed_labels.count - - references = ->(label) { label.to_reference(format: :id) } - added_labels = added_labels.map(&references).join(' ') - removed_labels = removed_labels.map(&references).join(' ') - - text_parts = [] - - if added_labels.present? - text_parts << "added #{added_labels}" - text_parts << 'and' if removed_labels.present? - end - - if removed_labels.present? - text_parts << "removed #{removed_labels}" - end - - text_parts << 'label'.pluralize(labels_count) - body = text_parts.join(' ') - - create_note(NoteSummary.new(noteable, project, author, body, action: 'label')) - end - # Called when the milestone of a Noteable is changed # # noteable - Noteable object diff --git a/app/services/wikis/create_attachment_service.rb b/app/services/wikis/create_attachment_service.rb index 30fe0e371a6..df31ad7c8ea 100644 --- a/app/services/wikis/create_attachment_service.rb +++ b/app/services/wikis/create_attachment_service.rb @@ -11,7 +11,7 @@ module Wikis def initialize(*args) super - @file_name = truncate_file_name(params[:file_name]) + @file_name = clean_file_name(params[:file_name]) @file_path = File.join(ATTACHMENT_PATH, SecureRandom.hex, @file_name) if @file_name @commit_message ||= "Upload attachment #{@file_name}" @branch_name ||= wiki.default_branch @@ -23,8 +23,16 @@ module Wikis private - def truncate_file_name(file_name) + def clean_file_name(file_name) return unless file_name.present? + + file_name = truncate_file_name(file_name) + # CommonMark does not allow Urls with whitespaces, so we have to replace them + # Using the same regex Carrierwave use to replace invalid characters + file_name.gsub(CarrierWave::SanitizedFile.sanitize_regexp, '_') + end + + def truncate_file_name(file_name) return file_name if file_name.length <= MAX_FILENAME_LENGTH extension = File.extname(file_name) diff --git a/app/uploaders/avatar_uploader.rb b/app/uploaders/avatar_uploader.rb index b29ef57b071..8526bc16390 100644 --- a/app/uploaders/avatar_uploader.rb +++ b/app/uploaders/avatar_uploader.rb @@ -18,6 +18,10 @@ class AvatarUploader < GitlabUploader false end + def absolute_path + self.class.absolute_path(model.avatar) + end + private def dynamic_segment diff --git a/app/uploaders/namespace_file_uploader.rb b/app/uploaders/namespace_file_uploader.rb index 52969762b7d..b0154f85a5c 100644 --- a/app/uploaders/namespace_file_uploader.rb +++ b/app/uploaders/namespace_file_uploader.rb @@ -6,8 +6,15 @@ class NamespaceFileUploader < FileUploader options.storage_path end - def self.base_dir(model, _store = nil) - File.join(options.base_dir, 'namespace', model_path_segment(model)) + def self.base_dir(model, store = nil) + base_dirs(model)[store || Store::LOCAL] + end + + def self.base_dirs(model) + { + Store::LOCAL => File.join(options.base_dir, 'namespace', model_path_segment(model)), + Store::REMOTE => File.join('namespace', model_path_segment(model)) + } end def self.model_path_segment(model) @@ -18,11 +25,4 @@ class NamespaceFileUploader < FileUploader def store_dir store_dirs[object_store] end - - def store_dirs - { - Store::LOCAL => File.join(base_dir, dynamic_segment), - Store::REMOTE => File.join('namespace', self.class.model_path_segment(model), dynamic_segment) - } - end end diff --git a/app/views/admin/application_settings/_account_and_limit.html.haml b/app/views/admin/application_settings/_account_and_limit.html.haml index 9121e44d31b..10bc3452d8b 100644 --- a/app/views/admin/application_settings/_account_and_limit.html.haml +++ b/app/views/admin/application_settings/_account_and_limit.html.haml @@ -14,7 +14,10 @@ = f.label :max_attachment_size, 'Maximum attachment size (MB)', class: 'label-bold' = f.number_field :max_attachment_size, class: 'form-control' .form-group - = f.label :session_expire_delay, 'Session duration (minutes)', class: 'label-bold' + = f.label :receive_max_input_size, 'Maximum push size (MB)', class: 'label-light' + = f.number_field :receive_max_input_size, class: 'form-control' + .form-group + = f.label :session_expire_delay, 'Session duration (minutes)', class: 'label-light' = f.number_field :session_expire_delay, class: 'form-control' %span.form-text.text-muted#session_expire_delay_help_block GitLab restart is required to apply changes .form-group diff --git a/app/views/events/_event.html.haml b/app/views/events/_event.html.haml index 53a33adc14d..5623f0f590a 100644 --- a/app/views/events/_event.html.haml +++ b/app/views/events/_event.html.haml @@ -11,3 +11,5 @@ = render "events/event/note", event: event - else = render "events/event/common", event: event +- elsif @user.include_private_contributions? + = render "events/event/private", event: event diff --git a/app/views/events/_event_scope.html.haml b/app/views/events/_event_scope.html.haml index 8f7da7d8c4f..98941722434 100644 --- a/app/views/events/_event_scope.html.haml +++ b/app/views/events/_event_scope.html.haml @@ -1,7 +1,7 @@ %span.event-scope = event_preposition(event) - if event.project - = link_to_project event.project + = link_to_project(event.project) - else = event.project_name diff --git a/app/views/events/event/_common.html.haml b/app/views/events/event/_common.html.haml index 01e72862114..829a3da1558 100644 --- a/app/views/events/event/_common.html.haml +++ b/app/views/events/event/_common.html.haml @@ -1,7 +1,7 @@ = icon_for_profile_event(event) .event-title - %span.author_name= link_to_author event + %span.author_name= link_to_author(event) %span{ class: event.action_name } - if event.target = event.action_name diff --git a/app/views/events/event/_created_project.html.haml b/app/views/events/event/_created_project.html.haml index d8e59be57bb..6ad7e157131 100644 --- a/app/views/events/event/_created_project.html.haml +++ b/app/views/events/event/_created_project.html.haml @@ -1,11 +1,11 @@ = icon_for_profile_event(event) .event-title - %span.author_name= link_to_author event + %span.author_name= link_to_author(event) %span{ class: event.action_name } = event_action_name(event) - if event.project - = link_to_project event.project + = link_to_project(event.project) - else = event.project_name diff --git a/app/views/events/event/_note.html.haml b/app/views/events/event/_note.html.haml index de6383e4097..cdacd998a69 100644 --- a/app/views/events/event/_note.html.haml +++ b/app/views/events/event/_note.html.haml @@ -1,7 +1,7 @@ = icon_for_profile_event(event) .event-title - %span.author_name= link_to_author event + %span.author_name= link_to_author(event) = event.action_name = event_note_title_html(event) diff --git a/app/views/events/event/_private.html.haml b/app/views/events/event/_private.html.haml new file mode 100644 index 00000000000..ccd2aacb4ea --- /dev/null +++ b/app/views/events/event/_private.html.haml @@ -0,0 +1,10 @@ +.event-inline.event-item + .event-item-timestamp + = time_ago_with_tooltip(event.created_at) + + .system-note-image= sprite_icon('eye-slash', size: 16, 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 } diff --git a/app/views/events/event/_push.html.haml b/app/views/events/event/_push.html.haml index 85f2d00bde3..5f0ee79cd9b 100644 --- a/app/views/events/event/_push.html.haml +++ b/app/views/events/event/_push.html.haml @@ -3,7 +3,7 @@ = icon_for_profile_event(event) .event-title - %span.author_name= link_to_author event + %span.author_name= link_to_author(event) %span.pushed #{event.action_name} #{event.ref_type} %strong - commits_link = project_commits_path(project, event.ref_name) diff --git a/app/views/layouts/_page.html.haml b/app/views/layouts/_page.html.haml index f67a8878c80..a41d30da450 100644 --- a/app/views/layouts/_page.html.haml +++ b/app/views/layouts/_page.html.haml @@ -8,6 +8,7 @@ = render "layouts/broadcast" = render 'layouts/header/read_only_banner' = yield :flash_message + = render "shared/ping_consent" - unless @hide_breadcrumbs = render "layouts/nav/breadcrumbs" = render "layouts/flash" diff --git a/app/views/profiles/show.html.haml b/app/views/profiles/show.html.haml index 9f79feb4ddd..0a1ee648d97 100644 --- a/app/views/profiles/show.html.haml +++ b/app/views/profiles/show.html.haml @@ -1,5 +1,6 @@ -- breadcrumb_title "Edit Profile" +- breadcrumb_title s_("Profiles|Edit Profile") - @content_class = "limit-container-width" unless fluid_layout +- gravatar_link = link_to Gitlab.config.gravatar.host, 'https://' + Gitlab.config.gravatar.host = bootstrap_form_for @user, url: profile_path, method: :put, html: { multipart: true, class: 'edit-user prepend-top-default js-quick-submit' }, authenticity_token: true do |f| = form_errors(@user) @@ -7,34 +8,36 @@ .row .col-lg-4.profile-settings-sidebar %h4.prepend-top-0 - Public Avatar + = s_("Profiles|Public Avatar") %p - if @user.avatar? - You can change your avatar here - if gravatar_enabled? - or remove the current avatar to revert to #{link_to Gitlab.config.gravatar.host, 'https://' + Gitlab.config.gravatar.host} + = s_("Profiles|You can change your avatar here or remove the current avatar to revert to %{gravatar_link}").html_safe % { gravatar_link: gravatar_link } + - else + = s_("Profiles|You can change your avatar here") - else - You can upload an avatar here - if gravatar_enabled? - or change it at #{link_to Gitlab.config.gravatar.host, 'https://' + Gitlab.config.gravatar.host} + = s_("Profiles|You can upload your avatar here or change it at %{gravatar_link}").html_safe % { gravatar_link: gravatar_link } + - else + = s_("Profiles|You can upload your avatar here") .col-lg-8 .clearfix.avatar-image.append-bottom-default = link_to avatar_icon_for_user(@user, 400), target: '_blank', rel: 'noopener noreferrer' do = image_tag avatar_icon_for_user(@user, 160), alt: '', class: 'avatar s160' - %h5.prepend-top-0= _("Upload new avatar") + %h5.prepend-top-0= s_("Profiles|Upload new avatar") .prepend-top-5.append-bottom-10 - %button.btn.js-choose-user-avatar-button{ type: 'button' }= _("Choose file...") - %span.avatar-file-name.prepend-left-default.js-avatar-filename= _("No file chosen") + %button.btn.js-choose-user-avatar-button{ type: 'button' }= s_("Profiles|Choose file...") + %span.avatar-file-name.prepend-left-default.js-avatar-filename= s_("Profiles|No file chosen") = f.file_field_without_bootstrap :avatar, class: 'js-user-avatar-input hidden', accept: 'image/*' - .form-text.text-muted= _("The maximum file size allowed is 200KB.") + .form-text.text-muted= s_("Profiles|The maximum file size allowed is 200KB.") - if @user.avatar? %hr - = link_to _('Remove avatar'), profile_avatar_path, data: { confirm: _('Avatar will be removed. Are you sure?') }, method: :delete, class: 'btn btn-danger btn-inverted' + = link_to s_("Profiles|Remove avatar"), profile_avatar_path, data: { confirm: s_("Profiles|Avatar will be removed. Are you sure?") }, method: :delete, class: 'btn btn-danger btn-inverted' %hr .row .col-lg-4.profile-settings-sidebar - %h4.prepend-top-0= s_("User|Current status") + %h4.prepend-top-0= s_("Profiles|Current status") %p= s_("Profiles|This emoji and message will appear on your profile and throughout the interface.") .col-lg-8 = f.fields_for :status, @user.status do |status_form| @@ -66,62 +69,66 @@ .row .col-lg-4.profile-settings-sidebar %h4.prepend-top-0 - Main settings + = s_("Profiles|Main settings") %p - This information will appear on your profile. + = s_("Profiles|This information will appear on your profile.") - if current_user.ldap_user? - Some options are unavailable for LDAP accounts + = s_("Profiles|Some options are unavailable for LDAP accounts") .col-lg-8 .row - if @user.read_only_attribute?(:name) = f.text_field :name, required: true, readonly: true, wrapper: { class: 'col-md-9' }, - help: "Your name was automatically set based on your #{ attribute_provider_label(:name) } account, so people you know can recognize you." + help: s_("Profiles|Your name was automatically set based on your %{provider_label} account, so people you know can recognize you.") % { provider_label: attribute_provider_label(:name) } - else = f.text_field :name, label: 'Full name', required: true, wrapper: { class: 'col-md-9' }, help: "Enter your name, so people you know can recognize you." = f.text_field :id, readonly: true, label: 'User ID', wrapper: { class: 'col-md-3' } - if @user.read_only_attribute?(:email) - = f.text_field :email, required: true, readonly: true, help: "Your email address was automatically set based on your #{ attribute_provider_label(:email) } account." + = f.text_field :email, required: true, readonly: true, help: s_("Profiles|Your email address was automatically set based on your %{provider_label} account.") % { provider_label: attribute_provider_label(:email) } - else = f.text_field :email, required: true, value: (@user.email unless @user.temp_oauth_email?), help: user_email_help_text(@user) = f.select :public_email, options_for_select(@user.all_emails, selected: @user.public_email), - { help: 'This email will be displayed on your public profile.', include_blank: 'Do not show on profile' }, + { help: s_("Profiles|This email will be displayed on your public profile."), include_blank: s_("Profiles|Do not show on profile") }, control_class: 'select2' = f.select :preferred_language, Gitlab::I18n::AVAILABLE_LANGUAGES.map { |value, label| [label, value] }, - { help: 'This feature is experimental and translations are not complete yet.' }, + { help: s_("Profiles|This feature is experimental and translations are not complete yet.") }, control_class: 'select2' = f.text_field :skype = f.text_field :linkedin = f.text_field :twitter - = f.text_field :website_url, label: 'Website' + = f.text_field :website_url, label: s_("Profiles|Website") - if @user.read_only_attribute?(:location) - = f.text_field :location, readonly: true, help: "Your location was automatically set based on your #{ attribute_provider_label(:location) } account." + = f.text_field :location, readonly: true, help: s_("Profiles|Your location was automatically set based on your %{provider_label} account.") % { provider_label: attribute_provider_label(:location) } - else = f.text_field :location = f.text_field :organization - = f.text_area :bio, rows: 4, maxlength: 250, help: 'Tell us about yourself in fewer than 250 characters.' + = f.text_area :bio, rows: 4, maxlength: 250, help: s_("Profiles|Tell us about yourself in fewer than 250 characters.") %hr - %h5 Private profile + %h5= ("Private profile") - private_profile_label = capture do - Don't display activity-related personal information on your profile + = s_("Profiles|Don't display activity-related personal information on your profiles") = link_to icon('question-circle'), help_page_path('user/profile/index.md', anchor: 'private-profile') = f.check_box :private_profile, label: private_profile_label + %h5= s_("Profiles|Private contributions") + = f.check_box :include_private_contributions, label: 'Include private contributions on my profile' + .help-block + = s_("Profiles|Choose to show contributions of private projects on your public profile without any project, repository or organization information.") .prepend-top-default.append-bottom-default - = f.submit 'Update profile settings', class: 'btn btn-success' - = link_to 'Cancel', user_path(current_user), class: 'btn btn-cancel' + = f.submit s_("Profiles|Update profile settings"), class: 'btn btn-success' + = link_to _("Cancel"), user_path(current_user), class: 'btn btn-cancel' .modal.modal-profile-crop .modal-dialog .modal-content .modal-header %h4.modal-title - Position and size your new avatar - %button.close{ type: "button", "data-dismiss": "modal", "aria-label" => _('Close') } + = s_("Profiles|Position and size your new avatar") + %button.close{ type: "button", "data-dismiss": "modal", "aria-label" => _("Close") } %span{ "aria-hidden": true } × .modal-body .profile-crop-image-container - %img.modal-profile-crop-image{ alt: 'Avatar cropper' } + %img.modal-profile-crop-image{ alt: s_("Profiles|Avatar cropper") } .crop-controls .btn-group %button.btn.btn-primary{ data: { method: 'zoom', option: '0.1' } } @@ -130,4 +137,4 @@ %span.fa.fa-search-minus .modal-footer %button.btn.btn-primary.js-upload-user-avatar{ type: 'button' } - Set new profile picture + = s_("Profiles|Set new profile picture") diff --git a/app/views/shared/_ping_consent.html.haml b/app/views/shared/_ping_consent.html.haml new file mode 100644 index 00000000000..f8eb2b2833b --- /dev/null +++ b/app/views/shared/_ping_consent.html.haml @@ -0,0 +1,12 @@ +- if session[:ask_for_usage_stats_consent] + .ping-consent-message.alert.alert-warning.flex-alert + - settings_link_start = '<a href="%{url}" target="_blank" rel="noopener noreferrer" class="alert-link">'.html_safe % { url: admin_application_settings_path(anchor: 'js-usage-settings') } + - info_link_start = '<a href="%{url}" target="_blank" rel="noopener noreferrer" class="alert-link">'.html_safe % { url: help_page_path('user/admin_area/settings/usage_statistics.md') } + .alert-message + = s_('To help improve GitLab, we would like to periodically collect usage information. This can be changed at any time in %{settings_link_start}Settings%{link_end}. %{info_link_start}More Information%{link_end}').html_safe % { settings_link_start: settings_link_start, info_link_start: info_link_start, link_end: '</a>'.html_safe } + .alert-link-group + - send_usage_data_path = admin_application_settings_path(application_setting: { version_check_enabled: 1, usage_ping_enabled: 1 }) + - not_now_path = admin_application_settings_path(application_setting: { version_check_enabled: 0, usage_ping_enabled: 0 }) + = link_to _("Send usage data"), send_usage_data_path, 'data-url' => admin_application_settings_path, method: :put, 'data-check-enabled': true, 'data-ping-enabled': true, class: 'alert-link js-usage-consent-action' + | + = link_to _('Not now'), not_now_path, 'data-url' => admin_application_settings_path, method: :put, 'data-check-enabled': false, 'data-ping-enabled': false, class: 'hide-ping-consent-message alert-link js-usage-consent-action' diff --git a/app/views/users/calendar_activities.html.haml b/app/views/users/calendar_activities.html.haml index 2d4656e8608..938cb579e9f 100644 --- a/app/views/users/calendar_activities.html.haml +++ b/app/views/users/calendar_activities.html.haml @@ -1,6 +1,5 @@ %h4.prepend-top-20 - Contributions for - %strong= @calendar_date.to_s(:medium) + = _("Contributions for <strong>%{calendar_date}</strong>").html_safe % { calendar_date: @calendar_date.to_s(:medium) } - if @events.any? %ul.bordered-list @@ -9,25 +8,28 @@ %span.light %i.fa.fa-clock-o = event.created_at.strftime('%-I:%M%P') - - if event.push? - #{event.action_name} #{event.ref_type} + - if event.visible_to_user?(current_user) + - if event.push? + #{event.action_name} #{event.ref_type} + %strong + - commits_path = project_commits_path(event.project, event.ref_name) + = link_to_if event.project.repository.branch_exists?(event.ref_name), event.ref_name, commits_path + - else + = event_action_name(event) + %strong + - if event.note? + = link_to event.note_target.to_reference, event_note_target_url(event), class: 'has-tooltip', title: event.target_title + - elsif event.target + = link_to event.target.to_reference, [event.project.namespace.becomes(Namespace), event.project, event.target], class: 'has-tooltip', title: event.target_title + + at %strong - - commits_path = project_commits_path(event.project, event.ref_name) - = link_to_if event.project.repository.branch_exists?(event.ref_name), event.ref_name, commits_path + - if event.project + = link_to_project(event.project) + - else + = event.project_name - else - = event_action_name(event) - %strong - - if event.note? - = link_to event.note_target.to_reference, event_note_target_url(event), class: 'has-tooltip', title: event.target_title - - elsif event.target - = link_to event.target.to_reference, [event.project.namespace.becomes(Namespace), event.project, event.target], class: 'has-tooltip', title: event.target_title - - at - %strong - - if event.project - = link_to_project event.project - - else - = event.project_name + made a private contribution - else %p - No contributions found for #{@calendar_date.to_s(:medium)} + = _('No contributions were found') diff --git a/app/workers/all_queues.yml b/app/workers/all_queues.yml index ae9dc8d4857..1eeb972cee9 100644 --- a/app/workers/all_queues.yml +++ b/app/workers/all_queues.yml @@ -87,6 +87,7 @@ - authorized_projects - background_migration - create_gpg_signature +- delete_container_repository - delete_merged_branches - delete_user - email_receiver diff --git a/app/workers/delete_container_repository_worker.rb b/app/workers/delete_container_repository_worker.rb new file mode 100644 index 00000000000..b703530d3a0 --- /dev/null +++ b/app/workers/delete_container_repository_worker.rb @@ -0,0 +1,34 @@ +# frozen_string_literal: true + +class DeleteContainerRepositoryWorker + include ApplicationWorker + include ExclusiveLeaseGuard + + LEASE_TIMEOUT = 1.hour + + attr_reader :container_repository + + def perform(current_user_id, container_repository_id) + current_user = User.find_by(id: current_user_id) + @container_repository = ContainerRepository.find_by(id: container_repository_id) + project = container_repository&.project + + return unless current_user && container_repository && project + + # If a user accidentally attempts to delete the same container registry in quick succession, + # this can lead to orphaned tags. + try_obtain_lease do + Projects::ContainerRepository::DestroyService.new(project, current_user).execute(container_repository) + end + end + + # For ExclusiveLeaseGuard concern + def lease_key + @lease_key ||= "container_repository:delete:#{container_repository.id}" + end + + # For ExclusiveLeaseGuard concern + def lease_timeout + LEASE_TIMEOUT + end +end diff --git a/app/workers/new_merge_request_worker.rb b/app/workers/new_merge_request_worker.rb index 5d8b8904502..62f9d9b6f57 100644 --- a/app/workers/new_merge_request_worker.rb +++ b/app/workers/new_merge_request_worker.rb @@ -9,6 +9,8 @@ class NewMergeRequestWorker EventCreateService.new.open_mr(issuable, user) NotificationService.new.new_merge_request(issuable, user) + + issuable.diffs.write_cache issuable.create_cross_references!(user) end diff --git a/changelogs/unreleased/48778-remove-old-storage-logic-from-import-export.yml b/changelogs/unreleased/48778-remove-old-storage-logic-from-import-export.yml new file mode 100644 index 00000000000..4ddbc118e86 --- /dev/null +++ b/changelogs/unreleased/48778-remove-old-storage-logic-from-import-export.yml @@ -0,0 +1,5 @@ +--- +title: Update Import/Export to only use new storage uploaders logic +merge_request: 21409 +author: +type: added diff --git a/changelogs/unreleased/51180-update-ffi-to-1-9-25.yml b/changelogs/unreleased/51180-update-ffi-to-1-9-25.yml new file mode 100644 index 00000000000..67354d6a610 --- /dev/null +++ b/changelogs/unreleased/51180-update-ffi-to-1-9-25.yml @@ -0,0 +1,5 @@ +--- +title: Update ffi to 1.9.25 +merge_request: 21561 +author: Takuya Noguchi +type: other diff --git a/changelogs/unreleased/feat-update-contribution-calendar.yml b/changelogs/unreleased/feat-update-contribution-calendar.yml new file mode 100644 index 00000000000..4ada8b1fcd5 --- /dev/null +++ b/changelogs/unreleased/feat-update-contribution-calendar.yml @@ -0,0 +1,5 @@ +--- +title: Include private contributions to contributions calendar +merge_request: 17296 +author: George Tsiolis +type: added diff --git a/changelogs/unreleased/fix-namespace-uploader.yml b/changelogs/unreleased/fix-namespace-uploader.yml new file mode 100644 index 00000000000..081adc9a6f1 --- /dev/null +++ b/changelogs/unreleased/fix-namespace-uploader.yml @@ -0,0 +1,5 @@ +--- +title: Fix NamespaceUploader.base_dir for remote uploads +merge_request: +author: +type: fixed diff --git a/changelogs/unreleased/fj-51194-fix-wiki-attachments-with-whitespaces.yml b/changelogs/unreleased/fj-51194-fix-wiki-attachments-with-whitespaces.yml new file mode 100644 index 00000000000..a6464807e01 --- /dev/null +++ b/changelogs/unreleased/fj-51194-fix-wiki-attachments-with-whitespaces.yml @@ -0,0 +1,5 @@ +--- +title: Replace white spaces in wiki attachments file names +merge_request: 21569 +author: +type: fixed diff --git a/changelogs/unreleased/ide-commit-panel-improved.yml b/changelogs/unreleased/ide-commit-panel-improved.yml new file mode 100644 index 00000000000..245214185e5 --- /dev/null +++ b/changelogs/unreleased/ide-commit-panel-improved.yml @@ -0,0 +1,5 @@ +--- +title: Improved commit panel in Web IDE +merge_request: 21471 +author: +type: changed diff --git a/changelogs/unreleased/ide-file-templates.yml b/changelogs/unreleased/ide-file-templates.yml new file mode 100644 index 00000000000..68983670b25 --- /dev/null +++ b/changelogs/unreleased/ide-file-templates.yml @@ -0,0 +1,5 @@ +--- +title: Added file templates to the Web IDE +merge_request: +author: +type: added diff --git a/changelogs/unreleased/import-all-common-metrics-into-database.yml b/changelogs/unreleased/import-all-common-metrics-into-database.yml new file mode 100644 index 00000000000..524112fe115 --- /dev/null +++ b/changelogs/unreleased/import-all-common-metrics-into-database.yml @@ -0,0 +1,5 @@ +--- +title: Import all common metrics into database +merge_request: 21459 +author: +type: changed diff --git a/changelogs/unreleased/label-event.yml b/changelogs/unreleased/label-event.yml new file mode 100644 index 00000000000..e543abe5649 --- /dev/null +++ b/changelogs/unreleased/label-event.yml @@ -0,0 +1,6 @@ +--- +title: Use separate model for tracking resource label changes and render label system + notes based on data from this model. +merge_request: +author: +type: added diff --git a/changelogs/unreleased/osw-send-max-patch-bytes-to-gitaly.yml b/changelogs/unreleased/osw-send-max-patch-bytes-to-gitaly.yml new file mode 100644 index 00000000000..3c50448e3ff --- /dev/null +++ b/changelogs/unreleased/osw-send-max-patch-bytes-to-gitaly.yml @@ -0,0 +1,5 @@ +--- +title: Send max_patch_bytes to Gitaly via Gitaly::CommitDiffRequest +merge_request: 21575 +author: +type: other diff --git a/changelogs/unreleased/osw-write-cache-upon-mr-creation-and-cache-refactoring.yml b/changelogs/unreleased/osw-write-cache-upon-mr-creation-and-cache-refactoring.yml new file mode 100644 index 00000000000..4fba33decfa --- /dev/null +++ b/changelogs/unreleased/osw-write-cache-upon-mr-creation-and-cache-refactoring.yml @@ -0,0 +1,5 @@ +--- +title: Write diff highlighting cache upon MR creation (refactors caching) +merge_request: 21489 +author: +type: performance diff --git a/changelogs/unreleased/rd-26044-new-option-to-prevent-too-big-git-pushes.yml b/changelogs/unreleased/rd-26044-new-option-to-prevent-too-big-git-pushes.yml new file mode 100644 index 00000000000..f464b6dda5b --- /dev/null +++ b/changelogs/unreleased/rd-26044-new-option-to-prevent-too-big-git-pushes.yml @@ -0,0 +1,5 @@ +--- +title: Allow admins to configure the maximum Git push size +merge_request: 20758 +author: +type: added diff --git a/changelogs/unreleased/sh-delete-container-registry-async.yml b/changelogs/unreleased/sh-delete-container-registry-async.yml new file mode 100644 index 00000000000..dfe0e812112 --- /dev/null +++ b/changelogs/unreleased/sh-delete-container-registry-async.yml @@ -0,0 +1,5 @@ +--- +title: Delete a container registry asynchronously +merge_request: 21553 +author: +type: fixed diff --git a/changelogs/unreleased/sh-remove-orphaned-label-links.yml b/changelogs/unreleased/sh-remove-orphaned-label-links.yml new file mode 100644 index 00000000000..b035b57ff1b --- /dev/null +++ b/changelogs/unreleased/sh-remove-orphaned-label-links.yml @@ -0,0 +1,5 @@ +--- +title: Remove orphaned label links +merge_request: 21552 +author: +type: fixed diff --git a/changelogs/unreleased/usage_consent.yml b/changelogs/unreleased/usage_consent.yml new file mode 100644 index 00000000000..56e00879b9a --- /dev/null +++ b/changelogs/unreleased/usage_consent.yml @@ -0,0 +1,5 @@ +--- +title: Ask user explicitly about usage stats agreement on single user deployments. +merge_request: 21423 +author: +type: added diff --git a/changelogs/unreleased/zj-cleanup-port-gitaly.yml b/changelogs/unreleased/zj-cleanup-port-gitaly.yml new file mode 100644 index 00000000000..25e13b0fd50 --- /dev/null +++ b/changelogs/unreleased/zj-cleanup-port-gitaly.yml @@ -0,0 +1,5 @@ +--- +title: Administrative cleanup rake tasks now leverage Gitaly +merge_request: 21588 +author: +type: changed diff --git a/config/prometheus/additional_metrics.yml b/config/prometheus/common_metrics.yml index c994bad7865..52023a2e3cb 100644 --- a/config/prometheus/additional_metrics.yml +++ b/config/prometheus/common_metrics.yml @@ -7,7 +7,8 @@ - nginx_upstream_responses_total weight: 1 queries: - - query_range: 'sum(rate(nginx_upstream_responses_total{upstream=~"%{kube_namespace}-%{ci_environment_slug}-.*"}[2m])) by (status_code)' + - id: response_metrics_nginx_ingress_throughput_status_code + query_range: 'sum(rate(nginx_upstream_responses_total{upstream=~"%{kube_namespace}-%{ci_environment_slug}-.*"}[2m])) by (status_code)' unit: req / sec label: Status Code series: @@ -25,7 +26,8 @@ - nginx_upstream_response_msecs_avg weight: 1 queries: - - query_range: 'avg(nginx_upstream_response_msecs_avg{upstream=~"%{kube_namespace}-%{ci_environment_slug}-.*"})' + - id: response_metrics_nginx_ingress_latency_pod_average + query_range: 'avg(nginx_upstream_response_msecs_avg{upstream=~"%{kube_namespace}-%{ci_environment_slug}-.*"})' label: Pod average unit: ms - title: "HTTP Error Rate" @@ -34,7 +36,8 @@ - nginx_upstream_responses_total weight: 1 queries: - - query_range: 'sum(rate(nginx_upstream_responses_total{status_code="5xx", upstream=~"%{kube_namespace}-%{ci_environment_slug}-.*"}[2m])) / sum(rate(nginx_upstream_responses_total{upstream=~"%{kube_namespace}-%{ci_environment_slug}-.*"}[2m])) * 100' + - id: response_metrics_nginx_ingress_http_error_rate + query_range: 'sum(rate(nginx_upstream_responses_total{status_code="5xx", upstream=~"%{kube_namespace}-%{ci_environment_slug}-.*"}[2m])) / sum(rate(nginx_upstream_responses_total{upstream=~"%{kube_namespace}-%{ci_environment_slug}-.*"}[2m])) * 100' label: 5xx Errors unit: "%" - group: Response metrics (HA Proxy) @@ -46,10 +49,12 @@ - haproxy_frontend_http_requests_total weight: 1 queries: - - query_range: 'sum(rate(haproxy_frontend_http_requests_total{%{environment_filter}}[2m])) by (code)' + - id: response_metrics_ha_proxy_throughput_status_code + query_range: 'sum(rate(haproxy_frontend_http_requests_total{%{environment_filter}}[2m])) by (code)' unit: req / sec + label: Status Code series: - - label: code + - label: status_code when: - value: 2xx color: green @@ -63,7 +68,8 @@ - haproxy_frontend_http_responses_total weight: 1 queries: - - query_range: 'sum(rate(haproxy_frontend_http_responses_total{code="5xx",%{environment_filter}}[2m])) / sum(rate(haproxy_frontend_http_responses_total{%{environment_filter}}[2m]))' + - id: response_metrics_ha_proxy_http_error_rate + query_range: 'sum(rate(haproxy_frontend_http_responses_total{code="5xx",%{environment_filter}}[2m])) / sum(rate(haproxy_frontend_http_responses_total{%{environment_filter}}[2m]))' label: HTTP Errors unit: "%" - group: Response metrics (AWS ELB) @@ -75,7 +81,8 @@ - aws_elb_request_count_sum weight: 1 queries: - - query_range: 'sum(aws_elb_request_count_sum{%{environment_filter}}) / 60' + - id: response_metrics_aws_elb_throughput_requests + query_range: 'sum(aws_elb_request_count_sum{%{environment_filter}}) / 60' label: Total unit: req / sec - title: "Latency" @@ -84,7 +91,8 @@ - aws_elb_latency_average weight: 1 queries: - - query_range: 'avg(aws_elb_latency_average{%{environment_filter}}) * 1000' + - id: response_metrics_aws_elb_latency_average + query_range: 'avg(aws_elb_latency_average{%{environment_filter}}) * 1000' label: Average unit: ms - title: "HTTP Error Rate" @@ -94,7 +102,8 @@ - aws_elb_httpcode_backend_5_xx_sum weight: 1 queries: - - query_range: 'sum(aws_elb_httpcode_backend_5_xx_sum{%{environment_filter}}) / sum(aws_elb_request_count_sum{%{environment_filter}})' + - id: response_metrics_aws_elb_http_error_rate + query_range: 'sum(aws_elb_httpcode_backend_5_xx_sum{%{environment_filter}}) / sum(aws_elb_request_count_sum{%{environment_filter}})' label: HTTP Errors unit: "%" - group: Response metrics (NGINX) @@ -106,7 +115,8 @@ - nginx_server_requests weight: 1 queries: - - query_range: 'sum(rate(nginx_server_requests{server_zone!="*", server_zone!="_", %{environment_filter}}[2m])) by (code)' + - id: response_metrics_nginx_throughput_status_code + query_range: 'sum(rate(nginx_server_requests{server_zone!="*", server_zone!="_", %{environment_filter}}[2m])) by (code)' unit: req / sec label: Status Code series: @@ -124,7 +134,8 @@ - nginx_server_requestMsec weight: 1 queries: - - query_range: 'avg(nginx_server_requestMsec{%{environment_filter}})' + - id: response_metrics_nginx_latency + query_range: 'avg(nginx_server_requestMsec{%{environment_filter}})' label: Upstream unit: ms - title: "HTTP Error Rate" @@ -133,7 +144,8 @@ - nginx_server_requests weight: 1 queries: - - query_range: 'sum(rate(nginx_server_requests{code="5xx", %{environment_filter}}[2m]))' + - id: response_metrics_nginx_http_error_rate + query_range: 'sum(rate(nginx_server_requests{code="5xx", %{environment_filter}}[2m]))' label: HTTP Errors unit: "errors / sec" - group: System metrics (Kubernetes) @@ -145,7 +157,8 @@ - container_memory_usage_bytes weight: 4 queries: - - query_range: 'avg(sum(container_memory_usage_bytes{container_name!="POD",pod_name=~"^%{ci_environment_slug}-(.*)",namespace="%{kube_namespace}"}) by (job)) without (job) /1024/1024/1024' + - id: system_metrics_kubernetes_container_memory_total + query_range: 'avg(sum(container_memory_usage_bytes{container_name!="POD",pod_name=~"^%{ci_environment_slug}-(.*)",namespace="%{kube_namespace}"}) by (job)) without (job) /1024/1024/1024' label: Total unit: GB - title: "Core Usage (Total)" @@ -154,7 +167,8 @@ - container_cpu_usage_seconds_total weight: 3 queries: - - query_range: 'avg(sum(rate(container_cpu_usage_seconds_total{container_name!="POD",pod_name=~"^%{ci_environment_slug}-(.*)",namespace="%{kube_namespace}"}[15m])) by (job)) without (job)' + - id: system_metrics_kubernetes_container_cores_total + query_range: 'avg(sum(rate(container_cpu_usage_seconds_total{container_name!="POD",pod_name=~"^%{ci_environment_slug}-(.*)",namespace="%{kube_namespace}"}[15m])) by (job)) without (job)' label: Total unit: "cores" - title: "Memory Usage (Pod average)" @@ -163,15 +177,39 @@ - container_memory_usage_bytes weight: 2 queries: - - query_range: 'avg(sum(container_memory_usage_bytes{container_name!="POD",pod_name=~"^%{ci_environment_slug}-(.*)",namespace="%{kube_namespace}"}) by (job)) without (job) / count(avg(container_memory_usage_bytes{container_name!="POD",pod_name=~"^%{ci_environment_slug}-(.*)",namespace="%{kube_namespace}"}) without (job)) /1024/1024' + - id: system_metrics_kubernetes_container_memory_average + query_range: 'avg(sum(container_memory_usage_bytes{container_name!="POD",pod_name=~"^%{ci_environment_slug}-(.*)",namespace="%{kube_namespace}"}) by (job)) without (job) / count(avg(container_memory_usage_bytes{container_name!="POD",pod_name=~"^%{ci_environment_slug}-(.*)",namespace="%{kube_namespace}"}) without (job)) /1024/1024' + label: Pod average + unit: MB + - title: "Canary: Memory Usage (Pod Average)" + y_label: "Memory Used per Pod" + required_metrics: + - container_memory_usage_bytes + weight: 2 + queries: + - id: system_metrics_kubernetes_container_memory_average_canary + query_range: 'avg(sum(container_memory_usage_bytes{container_name!="POD",pod_name=~"^%{ci_environment_slug}-canary-(.*)",namespace="%{kube_namespace}"}) by (job)) without (job) / count(avg(container_memory_usage_bytes{container_name!="POD",pod_name=~"^%{ci_environment_slug}-canary-(.*)",namespace="%{kube_namespace}"}) without (job)) /1024/1024' label: Pod average unit: MB - - title: "Core Usage (Pod average)" + track: canary + - title: "Core Usage (Pod Average)" y_label: "Cores per Pod" required_metrics: - container_cpu_usage_seconds_total weight: 1 queries: - - query_range: 'avg(sum(rate(container_cpu_usage_seconds_total{container_name!="POD",pod_name=~"^%{ci_environment_slug}-(.*)",namespace="%{kube_namespace}"}[15m])) by (job)) without (job) / count(sum(rate(container_cpu_usage_seconds_total{container_name!="POD",pod_name=~"^%{ci_environment_slug}-(.*)",namespace="%{kube_namespace}"}[15m])) by (pod_name))' + - id: system_metrics_kubernetes_container_core_usage + query_range: 'avg(sum(rate(container_cpu_usage_seconds_total{container_name!="POD",pod_name=~"^%{ci_environment_slug}-(.*)",namespace="%{kube_namespace}"}[15m])) by (job)) without (job) / count(sum(rate(container_cpu_usage_seconds_total{container_name!="POD",pod_name=~"^%{ci_environment_slug}-(.*)",namespace="%{kube_namespace}"}[15m])) by (pod_name))' label: Pod average - unit: "cores"
\ No newline at end of file + unit: "cores" + - title: "Canary: Core Usage (Pod Average)" + y_label: "Cores per Pod" + required_metrics: + - container_cpu_usage_seconds_total + weight: 1 + queries: + - id: system_metrics_kubernetes_container_core_usage_canary + query_range: 'avg(sum(rate(container_cpu_usage_seconds_total{container_name!="POD",pod_name=~"^%{ci_environment_slug}-canary-(.*)",namespace="%{kube_namespace}"}[15m])) by (job)) without (job) / count(sum(rate(container_cpu_usage_seconds_total{container_name!="POD",pod_name=~"^%{ci_environment_slug}-canary-(.*)",namespace="%{kube_namespace}"}[15m])) by (pod_name))' + label: Pod average + unit: "cores" + track: canary diff --git a/config/sidekiq_queues.yml b/config/sidekiq_queues.yml index dc49403aca1..0e723cdeb9c 100644 --- a/config/sidekiq_queues.yml +++ b/config/sidekiq_queues.yml @@ -46,6 +46,7 @@ - [project_service, 1] - [delete_user, 1] - [todos_destroyer, 1] + - [delete_container_repository, 1] - [delete_merged_branches, 1] - [authorized_projects, 1] - [expire_build_instance_artifacts, 1] diff --git a/db/fixtures/development/99_common_metrics.rb b/db/fixtures/development/99_common_metrics.rb new file mode 100644 index 00000000000..1f39c0ce5a0 --- /dev/null +++ b/db/fixtures/development/99_common_metrics.rb @@ -0,0 +1,5 @@ +# frozen_string_literal: true + +require Rails.root.join('db/importers/common_metrics_importer.rb') + +::Importers::CommonMetricsImporter.new.execute diff --git a/db/fixtures/production/999_common_metrics.rb b/db/fixtures/production/999_common_metrics.rb new file mode 100644 index 00000000000..1f39c0ce5a0 --- /dev/null +++ b/db/fixtures/production/999_common_metrics.rb @@ -0,0 +1,5 @@ +# frozen_string_literal: true + +require Rails.root.join('db/importers/common_metrics_importer.rb') + +::Importers::CommonMetricsImporter.new.execute diff --git a/db/importers/common_metrics_importer.rb b/db/importers/common_metrics_importer.rb new file mode 100644 index 00000000000..3a150e8fc5f --- /dev/null +++ b/db/importers/common_metrics_importer.rb @@ -0,0 +1,101 @@ +# frozen_string_literal: true + +module Importers + class PrometheusMetric < ActiveRecord::Base + enum group: { + # built-in groups + nginx_ingress: -1, + ha_proxy: -2, + aws_elb: -3, + nginx: -4, + kubernetes: -5, + + # custom groups + business: 0, + response: 1, + system: 2 + } + + scope :common, -> { where(common: true) } + + GROUP_TITLES = { + business: _('Business metrics (Custom)'), + response: _('Response metrics (Custom)'), + system: _('System metrics (Custom)'), + nginx_ingress: _('Response metrics (NGINX Ingress)'), + ha_proxy: _('Response metrics (HA Proxy)'), + aws_elb: _('Response metrics (AWS ELB)'), + nginx: _('Response metrics (NGINX)'), + kubernetes: _('System metrics (Kubernetes)') + }.freeze + end + + class CommonMetricsImporter + MissingQueryId = Class.new(StandardError) + + attr_reader :content + + def initialize(file = 'config/prometheus/common_metrics.yml') + @content = YAML.load_file(file) + end + + def execute + process_content do |id, attributes| + find_or_build_metric!(id) + .update!(**attributes) + end + end + + private + + def process_content(&blk) + content.map do |group| + process_group(group, &blk) + end + end + + def process_group(group, &blk) + attributes = { + group: find_group_title_key(group['group']) + } + + group['metrics'].map do |metric| + process_metric(metric, attributes, &blk) + end + end + + def process_metric(metric, attributes, &blk) + attributes = attributes.merge( + title: metric['title'], + y_label: metric['y_label']) + + metric['queries'].map do |query| + process_metric_query(query, attributes, &blk) + end + end + + def process_metric_query(query, attributes, &blk) + attributes = attributes.merge( + legend: query['label'], + query: query['query_range'], + unit: query['unit']) + + yield(query['id'], attributes) + end + + def find_or_build_metric!(id) + raise MissingQueryId unless id + + PrometheusMetric.common.find_by(identifier: id) || + PrometheusMetric.new(common: true, identifier: id) + end + + def find_group_title_key(title) + PrometheusMetric.groups[find_group_title(title)] + end + + def find_group_title(title) + PrometheusMetric::GROUP_TITLES.invert[title] + end + end +end diff --git a/db/migrate/20180101160629_create_prometheus_metrics.rb b/db/migrate/20180101160629_create_prometheus_metrics.rb new file mode 100644 index 00000000000..c3be0939b17 --- /dev/null +++ b/db/migrate/20180101160629_create_prometheus_metrics.rb @@ -0,0 +1,18 @@ +# frozen_string_literal: true + +class CreatePrometheusMetrics < ActiveRecord::Migration + DOWNTIME = false + + def change + create_table :prometheus_metrics do |t| + t.references :project, index: true, foreign_key: { on_delete: :cascade }, null: false + t.string :title, null: false + t.string :query, null: false + t.string :y_label + t.string :unit + t.string :legend + t.integer :group, null: false, index: true + t.timestamps_with_timezone null: false + end + end +end diff --git a/db/migrate/20180101160630_change_project_id_for_prometheus_metrics.rb b/db/migrate/20180101160630_change_project_id_for_prometheus_metrics.rb new file mode 100644 index 00000000000..66820f13f54 --- /dev/null +++ b/db/migrate/20180101160630_change_project_id_for_prometheus_metrics.rb @@ -0,0 +1,11 @@ +# frozen_string_literal: true + +class ChangeProjectIdForPrometheusMetrics < ActiveRecord::Migration + include Gitlab::Database::MigrationHelpers + + DOWNTIME = false + + def change + change_column_null :prometheus_metrics, :project_id, true + end +end diff --git a/db/migrate/20180228172924_add_include_private_contributions_to_users.rb b/db/migrate/20180228172924_add_include_private_contributions_to_users.rb new file mode 100644 index 00000000000..ea3ebdd83d1 --- /dev/null +++ b/db/migrate/20180228172924_add_include_private_contributions_to_users.rb @@ -0,0 +1,7 @@ +class AddIncludePrivateContributionsToUsers < ActiveRecord::Migration + DOWNTIME = false + + def change + add_column :users, :include_private_contributions, :boolean + end +end diff --git a/db/migrate/20180720023512_add_receive_max_input_size_to_application_settings.rb b/db/migrate/20180720023512_add_receive_max_input_size_to_application_settings.rb new file mode 100644 index 00000000000..4ed851a0780 --- /dev/null +++ b/db/migrate/20180720023512_add_receive_max_input_size_to_application_settings.rb @@ -0,0 +1,11 @@ +# frozen_string_literal: true +# See http://doc.gitlab.com/ce/development/migration_style_guide.html +# for more information on how to write migrations for GitLab. + +class AddReceiveMaxInputSizeToApplicationSettings < ActiveRecord::Migration + DOWNTIME = false + + def change + add_column :application_settings, :receive_max_input_size, :integer + end +end diff --git a/db/migrate/20180831164904_fix_prometheus_metric_query_limits.rb b/db/migrate/20180831164904_fix_prometheus_metric_query_limits.rb new file mode 100644 index 00000000000..28c92e7c7ac --- /dev/null +++ b/db/migrate/20180831164904_fix_prometheus_metric_query_limits.rb @@ -0,0 +1,19 @@ +# frozen_string_literal: true + +# See http://doc.gitlab.com/ce/development/migration_style_guide.html +# for more information on how to write migrations for GitLab. +require Rails.root.join('db/migrate/prometheus_metrics_limits_to_mysql') + +class FixPrometheusMetricQueryLimits < ActiveRecord::Migration + include Gitlab::Database::MigrationHelpers + + DOWNTIME = false + + def up + PrometheusMetricsLimitsToMysql.new.up + end + + def down + # no-op + end +end diff --git a/db/migrate/20180831164905_add_common_to_prometheus_metrics.rb b/db/migrate/20180831164905_add_common_to_prometheus_metrics.rb new file mode 100644 index 00000000000..e21c156fff6 --- /dev/null +++ b/db/migrate/20180831164905_add_common_to_prometheus_metrics.rb @@ -0,0 +1,17 @@ +# frozen_string_literal: true + +class AddCommonToPrometheusMetrics < ActiveRecord::Migration + include Gitlab::Database::MigrationHelpers + + DOWNTIME = false + + disable_ddl_transaction! + + def up + add_column_with_default(:prometheus_metrics, :common, :boolean, default: false) + end + + def down + remove_column(:prometheus_metrics, :common) + end +end diff --git a/db/migrate/20180831164907_add_index_on_common_for_prometheus_metrics.rb b/db/migrate/20180831164907_add_index_on_common_for_prometheus_metrics.rb new file mode 100644 index 00000000000..fdbaaf67b87 --- /dev/null +++ b/db/migrate/20180831164907_add_index_on_common_for_prometheus_metrics.rb @@ -0,0 +1,17 @@ +# frozen_string_literal: true + +class AddIndexOnCommonForPrometheusMetrics < ActiveRecord::Migration + include Gitlab::Database::MigrationHelpers + + DOWNTIME = false + + disable_ddl_transaction! + + def up + add_concurrent_index :prometheus_metrics, :common + end + + def down + remove_concurrent_index :prometheus_metrics, :common + end +end diff --git a/db/migrate/20180831164908_add_identifier_to_prometheus_metric.rb b/db/migrate/20180831164908_add_identifier_to_prometheus_metric.rb new file mode 100644 index 00000000000..67de990757e --- /dev/null +++ b/db/migrate/20180831164908_add_identifier_to_prometheus_metric.rb @@ -0,0 +1,11 @@ +# frozen_string_literal: true + +class AddIdentifierToPrometheusMetric < ActiveRecord::Migration + include Gitlab::Database::MigrationHelpers + + DOWNTIME = false + + def change + add_column :prometheus_metrics, :identifier, :string + end +end diff --git a/db/migrate/20180831164909_add_index_for_identifier_to_prometheus_metric.rb b/db/migrate/20180831164909_add_index_for_identifier_to_prometheus_metric.rb new file mode 100644 index 00000000000..b30c24ccafe --- /dev/null +++ b/db/migrate/20180831164909_add_index_for_identifier_to_prometheus_metric.rb @@ -0,0 +1,17 @@ +# frozen_string_literal: true + +class AddIndexForIdentifierToPrometheusMetric < ActiveRecord::Migration + include Gitlab::Database::MigrationHelpers + + DOWNTIME = false + + disable_ddl_transaction! + + def up + add_concurrent_index :prometheus_metrics, :identifier, unique: true + end + + def down + remove_concurrent_index :prometheus_metrics, :identifier, unique: true + end +end diff --git a/db/migrate/20180831164910_import_common_metrics.rb b/db/migrate/20180831164910_import_common_metrics.rb new file mode 100644 index 00000000000..72658c09b8e --- /dev/null +++ b/db/migrate/20180831164910_import_common_metrics.rb @@ -0,0 +1,17 @@ +# frozen_string_literal: true + +class ImportCommonMetrics < ActiveRecord::Migration + include Gitlab::Database::MigrationHelpers + + require Rails.root.join('db/importers/common_metrics_importer.rb') + + DOWNTIME = false + + def up + Importers::CommonMetricsImporter.new.execute + end + + def down + # no-op + end +end diff --git a/db/migrate/20180901200537_add_resource_label_event_reference_fields.rb b/db/migrate/20180901200537_add_resource_label_event_reference_fields.rb new file mode 100644 index 00000000000..264970ceed8 --- /dev/null +++ b/db/migrate/20180901200537_add_resource_label_event_reference_fields.rb @@ -0,0 +1,11 @@ +# frozen_string_literal: true + +class AddResourceLabelEventReferenceFields < ActiveRecord::Migration + DOWNTIME = false + + def change + add_column :resource_label_events, :cached_markdown_version, :integer + add_column :resource_label_events, :reference, :text + add_column :resource_label_events, :reference_html, :text + end +end diff --git a/db/migrate/20180906101639_add_user_ping_consent_to_application_settings.rb b/db/migrate/20180906101639_add_user_ping_consent_to_application_settings.rb new file mode 100644 index 00000000000..5d0e67d2648 --- /dev/null +++ b/db/migrate/20180906101639_add_user_ping_consent_to_application_settings.rb @@ -0,0 +1,19 @@ +# frozen_string_literal: true + +class AddUserPingConsentToApplicationSettings < ActiveRecord::Migration + include Gitlab::Database::MigrationHelpers + + DOWNTIME = false + + disable_ddl_transaction! + + def up + add_column :application_settings, :usage_stats_set_by_user_id, :integer + add_concurrent_foreign_key :application_settings, :users, column: :usage_stats_set_by_user_id, on_delete: :nullify + end + + def down + remove_foreign_key :application_settings, column: :usage_stats_set_by_user_id + remove_column :application_settings, :usage_stats_set_by_user_id + end +end diff --git a/db/migrate/prometheus_metrics_limits_to_mysql.rb b/db/migrate/prometheus_metrics_limits_to_mysql.rb new file mode 100644 index 00000000000..79f4ab9b64b --- /dev/null +++ b/db/migrate/prometheus_metrics_limits_to_mysql.rb @@ -0,0 +1,12 @@ +class PrometheusMetricsLimitsToMysql < ActiveRecord::Migration + DOWNTIME = false + + def up + return unless Gitlab::Database.mysql? + + change_column :prometheus_metrics, :query, :text, limit: 4096, default: nil + end + + def down + end +end diff --git a/db/post_migrate/20180906051323_remove_orphaned_label_links.rb b/db/post_migrate/20180906051323_remove_orphaned_label_links.rb new file mode 100644 index 00000000000..b56b74f483e --- /dev/null +++ b/db/post_migrate/20180906051323_remove_orphaned_label_links.rb @@ -0,0 +1,43 @@ +# frozen_string_literal: true + +class RemoveOrphanedLabelLinks < ActiveRecord::Migration + include Gitlab::Database::MigrationHelpers + + DOWNTIME = false + + disable_ddl_transaction! + + class LabelLinks < ActiveRecord::Base + self.table_name = 'label_links' + include EachBatch + + def self.orphaned + where('NOT EXISTS ( SELECT 1 FROM labels WHERE labels.id = label_links.label_id )') + end + end + + def up + # Some of these queries can take up to 10 seconds to run on GitLab.com, + # which is pretty close to our 15 second statement timeout. To ensure a + # smooth deployment procedure we disable the statement timeouts for this + # migration, just in case. + disable_statement_timeout do + # On GitLab.com there are over 2,000,000 orphaned label links. On + # staging, removing 100,000 rows generated a max replication lag of 6.7 + # MB. In total, removing all these rows will only generate about 136 MB + # of data, so it should be safe to do this. + LabelLinks.orphaned.each_batch(of: 100_000) do |batch| + batch.delete_all + end + end + + add_concurrent_foreign_key(:label_links, :labels, column: :label_id, on_delete: :cascade) + end + + def down + # There is no way to restore orphaned label links. + if foreign_key_exists?(:label_links, column: :label_id) + remove_foreign_key(:label_links, column: :label_id) + end + end +end diff --git a/db/schema.rb b/db/schema.rb index 242192ebf5b..13814dd569e 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: 20180901171833) do +ActiveRecord::Schema.define(version: 20180906101639) do # These are extensions that must be enabled in order to support this database enable_extension "plpgsql" @@ -172,6 +172,8 @@ ActiveRecord::Schema.define(version: 20180901171833) do t.boolean "instance_statistics_visibility_private", default: false, null: false t.boolean "web_ide_clientside_preview_enabled", default: false, null: false t.boolean "user_show_add_ssh_key_message", default: true, null: false + t.integer "usage_stats_set_by_user_id" + t.integer "receive_max_input_size" end create_table "audit_events", force: :cascade do |t| @@ -1700,6 +1702,25 @@ ActiveRecord::Schema.define(version: 20180901171833) do add_index "projects", ["star_count"], name: "index_projects_on_star_count", using: :btree add_index "projects", ["visibility_level"], name: "index_projects_on_visibility_level", using: :btree + create_table "prometheus_metrics", force: :cascade do |t| + t.integer "project_id" + t.string "title", null: false + t.string "query", null: false + t.string "y_label" + t.string "unit" + t.string "legend" + t.integer "group", null: false + t.datetime_with_timezone "created_at", null: false + t.datetime_with_timezone "updated_at", null: false + t.boolean "common", default: false, null: false + t.string "identifier" + end + + add_index "prometheus_metrics", ["common"], name: "index_prometheus_metrics_on_common", using: :btree + add_index "prometheus_metrics", ["group"], name: "index_prometheus_metrics_on_group", using: :btree + add_index "prometheus_metrics", ["identifier"], name: "index_prometheus_metrics_on_identifier", unique: true, using: :btree + add_index "prometheus_metrics", ["project_id"], name: "index_prometheus_metrics_on_project_id", using: :btree + create_table "protected_branch_merge_access_levels", force: :cascade do |t| t.integer "protected_branch_id", null: false t.integer "access_level", default: 40, null: false @@ -1822,6 +1843,9 @@ ActiveRecord::Schema.define(version: 20180901171833) do t.integer "label_id" t.integer "user_id" t.datetime_with_timezone "created_at", null: false + t.integer "cached_markdown_version" + t.text "reference" + t.text "reference_html" end add_index "resource_label_events", ["issue_id"], name: "index_resource_label_events_on_issue_id", using: :btree @@ -2182,6 +2206,7 @@ ActiveRecord::Schema.define(version: 20180901171833) do t.integer "accepted_term_id" t.string "feed_token" t.boolean "private_profile" + t.boolean "include_private_contributions" end add_index "users", ["admin"], name: "index_users_on_admin", using: :btree @@ -2253,6 +2278,7 @@ ActiveRecord::Schema.define(version: 20180901171833) do add_index "web_hooks", ["project_id"], name: "index_web_hooks_on_project_id", using: :btree add_index "web_hooks", ["type"], name: "index_web_hooks_on_type", using: :btree + add_foreign_key "application_settings", "users", column: "usage_stats_set_by_user_id", name: "fk_964370041d", on_delete: :nullify add_foreign_key "badges", "namespaces", column: "group_id", on_delete: :cascade add_foreign_key "badges", "projects", on_delete: :cascade add_foreign_key "boards", "namespaces", column: "group_id", on_delete: :cascade @@ -2333,6 +2359,7 @@ ActiveRecord::Schema.define(version: 20180901171833) do add_foreign_key "issues", "users", column: "author_id", name: "fk_05f1e72feb", on_delete: :nullify add_foreign_key "issues", "users", column: "closed_by_id", name: "fk_c63cbf6c25", on_delete: :nullify add_foreign_key "issues", "users", column: "updated_by_id", name: "fk_ffed080f01", on_delete: :nullify + add_foreign_key "label_links", "labels", name: "fk_d97dd08678", on_delete: :cascade add_foreign_key "label_priorities", "labels", on_delete: :cascade add_foreign_key "label_priorities", "projects", on_delete: :cascade add_foreign_key "labels", "namespaces", column: "group_id", on_delete: :cascade @@ -2380,6 +2407,7 @@ ActiveRecord::Schema.define(version: 20180901171833) 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 "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 add_foreign_key "protected_branches", "projects", name: "fk_7a9c6d93e7", on_delete: :cascade diff --git a/doc/administration/raketasks/project_import_export.md b/doc/administration/raketasks/project_import_export.md index 7bd765a35e0..f43bba0a7a7 100644 --- a/doc/administration/raketasks/project_import_export.md +++ b/doc/administration/raketasks/project_import_export.md @@ -9,6 +9,7 @@ > application settings (`/admin/application_settings`) under 'Import sources'. > - The exports are stored in a temporary [shared directory][tmp] and are deleted > every 24 hours by a specific worker. +> - ImportExport can use object storage automatically starting from GitLab 11.3 The GitLab Import/Export version can be checked by using: @@ -30,12 +31,6 @@ sudo gitlab-rake gitlab:import_export:data bundle exec rake gitlab:import_export:data RAILS_ENV=production ``` -In order to enable Object Storage on the Export, you can use the [feature flag][feature-flags]: - -``` -import_export_object_storage -``` - [ce-3050]: https://gitlab.com/gitlab-org/gitlab-ce/issues/3050 [feature-flags]: https://docs.gitlab.com/ee/api/features.html [tmp]: ../../development/shared_files.md diff --git a/doc/api/README.md b/doc/api/README.md index e2a6e87a2c3..1738d4fae5c 100644 --- a/doc/api/README.md +++ b/doc/api/README.md @@ -40,6 +40,7 @@ following locations: - [Namespaces](namespaces.md) - [Notes](notes.md) (comments) - [Discussions](discussions.md) (threaded comments) +- [Resource Label Events](resource_label_events.md) - [Notification settings](notification_settings.md) - [Open source license templates](templates/licenses.md) - [Pages Domains](pages_domains.md) diff --git a/doc/api/resource_label_events.md b/doc/api/resource_label_events.md new file mode 100644 index 00000000000..33e4821ccf4 --- /dev/null +++ b/doc/api/resource_label_events.md @@ -0,0 +1,175 @@ +# Resource label events API + +Resource label events keep track about who, when, and which label was added or removed to an issuable. + +## Issues + +### List project issue label events + +Gets a list of all label events for a single issue. + +``` +GET /projects/:id/issues/:issue_iid/resource_label_events +``` + +| Attribute | Type | Required | Description | +| ------------------- | ---------------- | ---------- | ------------ | +| `id` | integer/string | yes | The ID or [URL-encoded path of the project](README.md#namespaced-path-encoding) | +| `issue_iid` | integer | yes | The IID of an issue | + +```json +[ + { + "id": 142, + "user": { + "id": 1, + "name": "Administrator", + "username": "root", + "state": "active", + "avatar_url": "https://www.gravatar.com/avatar/e64c7d89f26bd1972efa854d13d7dd61?s=80&d=identicon", + "web_url": "http://gitlab.example.com/root" + }, + "created_at": "2018-08-20T13:38:20.077Z", + "resource_type": "Issue", + "resource_id": 253, + "label": { + "id": 73, + "name": "a1", + "color": "#34495E", + "description": "" + }, + "action": "add" + }, + { + "id": 143, + "user": { + "id": 1, + "name": "Administrator", + "username": "root", + "state": "active", + "avatar_url": "https://www.gravatar.com/avatar/e64c7d89f26bd1972efa854d13d7dd61?s=80&d=identicon", + "web_url": "http://gitlab.example.com/root" + }, + "created_at": "2018-08-20T13:38:20.077Z", + "resource_type": "Issue", + "resource_id": 253, + "label": { + "id": 74, + "name": "p1", + "color": "#0033CC", + "description": "" + }, + "action": "remove" + } +] +``` + +```bash +curl --request GET --header "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" https://gitlab.example.com/api/v4/projects/5/issues/11/resource_label_events +``` + +### Get single issue label event + +Returns a single label event for a specific project issue + +``` +GET /projects/:id/issues/:issue_iid/resource_label_events/:resource_label_event_id +``` + +Parameters: + +| Attribute | Type | Required | Description | +| --------------- | -------------- | -------- | ----------- | +| `id` | integer/string | yes | The ID or [URL-encoded path of the project](README.md#namespaced-path-encoding) | +| `issue_iid` | integer | yes | The IID of an issue | +| `resource_label_event_id` | integer | yes | The ID of a label event | + +```bash +curl --request GET --header "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" https://gitlab.example.com/api/v4/projects/5/issues/11/resource_label_events/1 +``` + +## Merge requests + +### List project merge request label events + +Gets a list of all label events for a single merge request. + +``` +GET /projects/:id/merge_requests/:merge_request_iid/resource_label_events +``` + +| Attribute | Type | Required | Description | +| ------------------- | ---------------- | ---------- | ------------ | +| `id` | integer/string | yes | The ID or [URL-encoded path of the project](README.md#namespaced-path-encoding) | +| `merge_request_iid` | integer | yes | The IID of a merge request | + +```json +[ + { + "id": 119, + "user": { + "id": 1, + "name": "Administrator", + "username": "root", + "state": "active", + "avatar_url": "https://www.gravatar.com/avatar/e64c7d89f26bd1972efa854d13d7dd61?s=80&d=identicon", + "web_url": "http://gitlab.example.com/root" + }, + "created_at": "2018-08-20T06:17:28.394Z", + "resource_type": "MergeRequest", + "resource_id": 28, + "label": { + "id": 74, + "name": "p1", + "color": "#0033CC", + "description": "" + }, + "action": "add" + }, + { + "id": 120, + "user": { + "id": 1, + "name": "Administrator", + "username": "root", + "state": "active", + "avatar_url": "https://www.gravatar.com/avatar/e64c7d89f26bd1972efa854d13d7dd61?s=80&d=identicon", + "web_url": "http://gitlab.example.com/root" + }, + "created_at": "2018-08-20T06:17:28.394Z", + "resource_type": "MergeRequest", + "resource_id": 28, + "label": { + "id": 41, + "name": "project", + "color": "#D1D100", + "description": "" + }, + "action": "add" + } +] +``` + +```bash +curl --request GET --header "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" https://gitlab.example.com/api/v4/projects/5/merge_requests/11/resource_label_events +``` + +### Get single merge request label event + +Returns a single label event for a specific project merge request + +``` +GET /projects/:id/merge_requests/:merge_request_iid/resource_label_events/:resource_label_event_id +``` + +Parameters: + +| Attribute | Type | Required | Description | +| ------------------- | -------------- | -------- | ----------- | +| `id` | integer/string | yes | The ID or [URL-encoded path of the project](README.md#namespaced-path-encoding) | +| `merge_request_iid` | integer | yes | The IID of a merge request | +| `resource_label_event_id` | integer | yes | The ID of a label event | + +```bash +curl --request GET --header "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" https://gitlab.example.com/api/v4/projects/5/merge_requests/11/resource_label_events/120 +``` diff --git a/doc/ci/yaml/README.md b/doc/ci/yaml/README.md index d89705e8ead..72b90ac6334 100644 --- a/doc/ci/yaml/README.md +++ b/doc/ci/yaml/README.md @@ -1456,7 +1456,9 @@ There are three possible values: `none`, `normal`, and `recursive`: ``` - `recursive` means that all submodules (including submodules of submodules) - will be included. It is equivalent to: + will be included. This feature needs Git v1.8.1 and later. When using a + GitLab Runner with an executor not based on Docker, make sure the Git version + meets that requirement. It is equivalent to: ``` git submodule sync --recursive diff --git a/doc/development/README.md b/doc/development/README.md index 20f8fa1d368..e786d6594c7 100644 --- a/doc/development/README.md +++ b/doc/development/README.md @@ -47,6 +47,7 @@ description: 'Learn how to contribute to GitLab.' - [How to dump production data to staging](db_dump.md) - [Working with the GitHub importer](github_importer.md) - [Working with Merge Request diffs](diffs.md) +- [Prometheus metrics](prometheus_metrics.md) ## Performance guides diff --git a/doc/development/prometheus_metrics.md b/doc/development/prometheus_metrics.md new file mode 100644 index 00000000000..b6b6d9665ea --- /dev/null +++ b/doc/development/prometheus_metrics.md @@ -0,0 +1,48 @@ +# Working with Prometheus Metrics + +## Adding to the library + +We strive to support the 2-4 most important metrics for each common system service that supports Prometheus. If you are looking for support for a particular exporter which has not yet been added to the library, additions can be made [to the `common_metrics.yml`](https://gitlab.com/gitlab-org/gitlab-ce/blob/master/config/prometheus/common_metrics.yml) file. + +### Query identifier + +The requirement for adding a new metric is to make each query to have an unique identifier which is used to update the metric later when changed: + +```yaml +- group: Response metrics (NGINX Ingress) + metrics: + - title: "Throughput" + y_label: "Requests / Sec" + queries: + - id: response_metrics_nginx_ingress_throughput_status_code + query_range: 'sum(rate(nginx_upstream_responses_total{upstream=~"%{kube_namespace}-%{ci_environment_slug}-.*"}[2m])) by (status_code)' + unit: req / sec + label: Status Code +``` + +### Update existing metrics + +After you add or change existing _common_ metric you have to create a new database migration that will query and update all existing metrics. + +NOTE: **Note:** +If a query metric (which is identified by `id:`) is removed it will not be removed from database by default. +You might want to add additional database migration that makes a decision what to do with removed one. +For example: you might be interested in migrating all dependent data to a different metric. + +```ruby +class ImportCommonMetrics < ActiveRecord::Migration + include Gitlab::Database::MigrationHelpers + + require Rails.root.join('db/importers/common_metrics_importer.rb') + + DOWNTIME = false + + def up + Importers::CommonMetricsImporter.new.execute + end + + def down + # no-op + end +end +``` diff --git a/doc/topics/autodevops/index.md b/doc/topics/autodevops/index.md index c0268ce136c..e778f1d83df 100644 --- a/doc/topics/autodevops/index.md +++ b/doc/topics/autodevops/index.md @@ -238,6 +238,12 @@ There is also a feature flag to enable Auto DevOps to a percentage of projects which can be enabled from the console with `Feature.get(:force_autodevops_on_by_default).enable_percentage_of_actors(10)`. +NOTE: **Enabled by default:** +Starting with GitLab 11.3, the Auto DevOps pipeline will be enabled by default for all +projects. If it's not explicitly enabled for the project, Auto DevOps will be automatically +disabled on the first pipeline failure. Your project will continue to use an alternative +[CI/CD configuration file](../../ci/yaml/README.md) if one is found. + ### Deployment strategy > [Introduced](https://gitlab.com/gitlab-org/gitlab-ce/issues/38542) in GitLab 11.0. diff --git a/doc/user/profile/index.md b/doc/user/profile/index.md index b1b822f25bd..6b225147232 100644 --- a/doc/user/profile/index.md +++ b/doc/user/profile/index.md @@ -91,6 +91,18 @@ To enable private profile: NOTE: **Note:** You and GitLab admins can see your the abovementioned information on your profile even if it is private. +## Private contributions + +> [Introduced](https://gitlab.com/gitlab-org/gitlab-ce/issues/14078) in GitLab 11.3. + +Enabling private contributions will include contributions to private projects, in the user contribution calendar graph and user recent activity. + +To enable private contributions: + +1. Navigate to your personal [profile settings](#profile-settings). +2. Check the "Private contributions" option. +3. Hit **Update profile settings**. + ## Current status > Introduced in GitLab 11.2. diff --git a/doc/user/project/integrations/prometheus_library/metrics.md b/doc/user/project/integrations/prometheus_library/metrics.md index 96a22316265..ec16902fcc8 100644 --- a/doc/user/project/integrations/prometheus_library/metrics.md +++ b/doc/user/project/integrations/prometheus_library/metrics.md @@ -17,9 +17,3 @@ GitLab retrieves performance data from the configured Prometheus server, and att In order to isolate and only display relevant metrics for a given environment, GitLab needs a method to detect which labels are associated. To do that, GitLab uses the defined queries and fills in the environment specific variables. Typically this involves looking for the [$CI_ENVIRONMENT_SLUG](../../../../ci/variables/README.md#predefined-variables-environment-variables), but may also include other information such as the project's Kubernetes namespace. Each search query is defined in the [exporter specific documentation](#prometheus-metrics-library). - -## Adding to the library - -We strive to support the 2-4 most important metrics for each common system service that supports Prometheus. If you are looking for support for a particular exporter which has not yet been added to the library, additions can be made [to the `additional_metrics.yml`](https://gitlab.com/gitlab-org/gitlab-ce/blob/master/config/prometheus/additional_metrics.yml) file. - -> Note: The library is only for monitoring public, common, system services which all customers can benefit from. Support for monitoring [customer proprietary metrics](https://gitlab.com/gitlab-org/gitlab-ee/issues/2273) will be added in a subsequent release. diff --git a/lib/api/api.rb b/lib/api/api.rb index 843f75d3096..e89d9337853 100644 --- a/lib/api/api.rb +++ b/lib/api/api.rb @@ -118,6 +118,7 @@ module API mount ::API::Namespaces mount ::API::Notes mount ::API::Discussions + mount ::API::ResourceLabelEvents mount ::API::NotificationSettings mount ::API::PagesDomains mount ::API::Pipelines diff --git a/lib/api/entities.rb b/lib/api/entities.rb index 90abee94f6a..f0eafbaeb94 100644 --- a/lib/api/entities.rb +++ b/lib/api/entities.rb @@ -1437,5 +1437,19 @@ module API badge.type == 'ProjectBadge' ? 'project' : 'group' end end + + class ResourceLabelEvent < Grape::Entity + expose :id + expose :user, using: Entities::UserBasic + expose :created_at + expose :resource_type do |event, options| + event.issuable.class.name + end + expose :resource_id do |event, options| + event.issuable.id + end + expose :label, using: Entities::LabelBasic + expose :action + end end end diff --git a/lib/api/internal.rb b/lib/api/internal.rb index 0990e2a1fba..e3e8cb71c09 100644 --- a/lib/api/internal.rb +++ b/lib/api/internal.rb @@ -74,6 +74,7 @@ module API gl_repository: gl_repository, gl_id: Gitlab::GlId.gl_id(user), gl_username: user&.username, + git_config_options: [], # This repository_path is a bogus value but gitlab-shell still requires # its presence. https://gitlab.com/gitlab-org/gitlab-shell/issues/135 @@ -81,6 +82,13 @@ module API gitaly: gitaly_payload(params[:action]) } + + # Custom option for git-receive-pack command + receive_max_input_size = Gitlab::CurrentSettings.receive_max_input_size.to_i + if receive_max_input_size > 0 + payload[:git_config_options] << "receive.maxInputSize=#{receive_max_input_size.megabytes}" + end + response_with_status(**payload) when ::Gitlab::GitAccessResult::CustomAction response_with_status(code: 300, message: check_result.message, payload: check_result.payload) diff --git a/lib/api/project_export.rb b/lib/api/project_export.rb index 15c57a2fc02..8562ae6d737 100644 --- a/lib/api/project_export.rb +++ b/lib/api/project_export.rb @@ -21,12 +21,8 @@ module API detail 'This feature was introduced in GitLab 10.6.' end get ':id/export/download' do - path = user_project.export_project_path - - if path - present_disk_file!(path, File.basename(path), 'application/gzip') - elsif user_project.export_project_object_exists? - present_carrierwave_file!(user_project.import_export_upload.export_file) + if user_project.export_file_exists? + present_carrierwave_file!(user_project.export_file) else render_api_error!('404 Not found or has expired', 404) end diff --git a/lib/api/resource_label_events.rb b/lib/api/resource_label_events.rb new file mode 100644 index 00000000000..5ac3adeb990 --- /dev/null +++ b/lib/api/resource_label_events.rb @@ -0,0 +1,52 @@ +# frozen_string_literal: true + +module API + class ResourceLabelEvents < Grape::API + include PaginationParams + helpers ::API::Helpers::NotesHelpers + + before { authenticate! } + + EVENTABLE_TYPES = [Issue, MergeRequest].freeze + + EVENTABLE_TYPES.each do |eventable_type| + parent_type = eventable_type.parent_class.to_s.underscore + eventables_str = eventable_type.to_s.underscore.pluralize + + params do + requires :id, type: String, desc: "The ID of a #{parent_type}" + end + resource parent_type.pluralize.to_sym, requirements: API::PROJECT_ENDPOINT_REQUIREMENTS do + desc "Get a list of #{eventable_type.to_s.downcase} resource label events" do + success Entities::ResourceLabelEvent + detail 'This feature was introduced in 11.3' + end + params do + requires :eventable_id, types: [Integer, String], desc: 'The ID of the eventable' + use :pagination + end + get ":id/#{eventables_str}/:eventable_id/resource_label_events" do + eventable = find_noteable(parent_type, eventables_str, params[:eventable_id]) + events = eventable.resource_label_events.includes(:label, :user) + + present paginate(events), with: Entities::ResourceLabelEvent + end + + desc "Get a single #{eventable_type.to_s.downcase} resource label event" do + success Entities::ResourceLabelEvent + detail 'This feature was introduced in 11.3' + end + params do + requires :event_id, type: String, desc: 'The ID of a resource label event' + requires :eventable_id, types: [Integer, String], desc: 'The ID of the eventable' + end + get ":id/#{eventables_str}/:eventable_id/resource_label_events/:event_id" do + eventable = find_noteable(parent_type, eventables_str, params[:eventable_id]) + event = eventable.resource_label_events.find(params[:event_id]) + + present event, with: Entities::ResourceLabelEvent + end + end + end + end +end diff --git a/lib/banzai/filter/spaced_link_filter.rb b/lib/banzai/filter/spaced_link_filter.rb index 574a8a6c7a5..a4dd6abfe03 100644 --- a/lib/banzai/filter/spaced_link_filter.rb +++ b/lib/banzai/filter/spaced_link_filter.rb @@ -8,8 +8,9 @@ module Banzai # # Based on Banzai::Filter::AutolinkFilter # - # CommonMark does not allow spaces in the url portion of a link. - # For example, `[example](page slug)` is not valid. However, + # CommonMark does not allow spaces in the url portion of a link/url. + # For example, `[example](page slug)` is not valid. + # Neither is `![example](test image.jpg)`. However, # in our wikis, we support (via RedCarpet) this type of link, allowing # wiki pages to be easily linked by their title. This filter adds that functionality. # The intent is for this to only be used in Wikis - in general, we want @@ -20,10 +21,17 @@ module Banzai # Pattern to match a standard markdown link # - # Rubular: http://rubular.com/r/z9EAHxYmKI - LINK_PATTERN = /\[([^\]]+)\]\(([^)"]+)(?: \"([^\"]+)\")?\)/ - - # Text matching LINK_PATTERN inside these elements will not be linked + # Rubular: http://rubular.com/r/2EXEQ49rg5 + LINK_OR_IMAGE_PATTERN = %r{ + (?<preview_operator>!)? + \[(?<text>.+?)\] + \( + (?<new_link>.+?) + (?<title>\ ".+?")? + \) + }x + + # Text matching LINK_OR_IMAGE_PATTERN inside these elements will not be linked IGNORE_PARENTS = %w(a code kbd pre script style).to_set # The XPath query to use for finding text nodes to parse. @@ -38,7 +46,7 @@ module Banzai doc.xpath(TEXT_QUERY).each do |node| content = node.to_html - next unless content.match(LINK_PATTERN) + next unless content.match(LINK_OR_IMAGE_PATTERN) html = spaced_link_filter(content) @@ -53,25 +61,37 @@ module Banzai private def spaced_link_match(link) - match = LINK_PATTERN.match(link) - return link unless match && match[1] && match[2] + match = LINK_OR_IMAGE_PATTERN.match(link) + return link unless match # escape the spaces in the url so that it's a valid markdown link, # then run it through the markdown processor again, let it do its magic - text = match[1] - new_link = match[2].gsub(' ', '%20') - title = match[3] ? " \"#{match[3]}\"" : '' - html = Banzai::Filter::MarkdownFilter.call("[#{text}](#{new_link}#{title})", context) + html = Banzai::Filter::MarkdownFilter.call(transform_markdown(match), context) # link is wrapped in a <p>, so strip that off html.sub('<p>', '').chomp('</p>') end def spaced_link_filter(text) - Gitlab::StringRegexMarker.new(CGI.unescapeHTML(text), text.html_safe).mark(LINK_PATTERN) do |link, left:, right:| + Gitlab::StringRegexMarker.new(CGI.unescapeHTML(text), text.html_safe).mark(LINK_OR_IMAGE_PATTERN) do |link, left:, right:| spaced_link_match(link) end end + + def transform_markdown(match) + preview_operator, text, new_link, title = process_match(match) + + "#{preview_operator}[#{text}](#{new_link}#{title})" + end + + def process_match(match) + [ + match[:preview_operator], + match[:text], + match[:new_link].gsub(' ', '%20'), + match[:title] + ] + end end end end diff --git a/lib/banzai/pipeline/label_pipeline.rb b/lib/banzai/pipeline/label_pipeline.rb new file mode 100644 index 00000000000..725cccc4b2b --- /dev/null +++ b/lib/banzai/pipeline/label_pipeline.rb @@ -0,0 +1,14 @@ +# frozen_string_literal: true + +module Banzai + module Pipeline + class LabelPipeline < BasePipeline + def self.filters + @filters ||= FilterArray[ + Filter::SanitizationFilter, + Filter::LabelReferenceFilter + ] + end + end + end +end diff --git a/lib/banzai/pipeline/wiki_pipeline.rb b/lib/banzai/pipeline/wiki_pipeline.rb index 737ff0cc818..d2fe5a6492f 100644 --- a/lib/banzai/pipeline/wiki_pipeline.rb +++ b/lib/banzai/pipeline/wiki_pipeline.rb @@ -5,7 +5,7 @@ module Banzai @filters ||= begin super.insert_after(Filter::TableOfContentsFilter, Filter::GollumTagsFilter) .insert_before(Filter::TaskListFilter, Filter::WikiLinkFilter) - .insert_before(Filter::WikiLinkFilter, Filter::SpacedLinkFilter) + .insert_before(Filter::VideoLinkFilter, Filter::SpacedLinkFilter) end end end diff --git a/lib/gitlab/ci/status/build/failed.rb b/lib/gitlab/ci/status/build/failed.rb index 508b4814631..2fa9a0d4541 100644 --- a/lib/gitlab/ci/status/build/failed.rb +++ b/lib/gitlab/ci/status/build/failed.rb @@ -13,6 +13,8 @@ module Gitlab runner_unsupported: 'unsupported runner' }.freeze + private_constant :REASONS + def status_tooltip base_message end @@ -25,6 +27,10 @@ module Gitlab build.failed? end + def self.reasons + REASONS + end + private def base_message @@ -36,7 +42,7 @@ module Gitlab end def failure_reason_message - REASONS.fetch(subject.failure_reason.to_sym) + self.class.reasons.fetch(subject.failure_reason.to_sym) end end end diff --git a/lib/gitlab/contributions_calendar.rb b/lib/gitlab/contributions_calendar.rb index 4c28489f45a..58ca077e636 100644 --- a/lib/gitlab/contributions_calendar.rb +++ b/lib/gitlab/contributions_calendar.rb @@ -7,7 +7,11 @@ module Gitlab def initialize(contributor, current_user = nil) @contributor = contributor @current_user = current_user - @projects = ContributedProjectsFinder.new(contributor).execute(current_user) + @projects = if @contributor.include_private_contributions? + ContributedProjectsFinder.new(@contributor).execute(@contributor) + else + ContributedProjectsFinder.new(contributor).execute(current_user) + end end def activity_dates @@ -36,13 +40,9 @@ module Gitlab def events_by_date(date) return Event.none unless can_read_cross_project? - events = Event.contributions.where(author_id: contributor.id) + Event.contributions.where(author_id: contributor.id) .where(created_at: date.beginning_of_day..date.end_of_day) .where(project_id: projects) - - # Use visible_to_user? instead of the complicated logic in activity_dates - # because we're only viewing the events for a single day. - events.select { |event| event.visible_to_user?(current_user) } end def starting_year diff --git a/lib/gitlab/diff/file_collection/base.rb b/lib/gitlab/diff/file_collection/base.rb index c79d8d3cb21..2acb0e43b69 100644 --- a/lib/gitlab/diff/file_collection/base.rb +++ b/lib/gitlab/diff/file_collection/base.rb @@ -2,7 +2,7 @@ module Gitlab module Diff module FileCollection class Base - attr_reader :project, :diff_options, :diff_refs, :fallback_diff_refs + attr_reader :project, :diff_options, :diff_refs, :fallback_diff_refs, :diffable delegate :count, :size, :real_size, to: :diff_files @@ -33,6 +33,14 @@ module Gitlab diff_files.find { |diff_file| diff_file.new_path == new_path } end + def clear_cache + # No-op + end + + def write_cache + # No-op + end + private def decorate_diff!(diff) diff --git a/lib/gitlab/diff/file_collection/merge_request_diff.rb b/lib/gitlab/diff/file_collection/merge_request_diff.rb index be25e1bab21..0dd073a3a8e 100644 --- a/lib/gitlab/diff/file_collection/merge_request_diff.rb +++ b/lib/gitlab/diff/file_collection/merge_request_diff.rb @@ -2,6 +2,8 @@ module Gitlab module Diff module FileCollection class MergeRequestDiff < Base + extend ::Gitlab::Utils::Override + def initialize(merge_request_diff, diff_options:) @merge_request_diff = merge_request_diff @@ -13,70 +15,35 @@ module Gitlab end def diff_files - # Make sure to _not_ send any method call to Gitlab::Diff::File - # _before_ all of them were collected (`super`). Premature method calls will - # trigger N+1 RPCs to Gitaly through BatchLoader records (Blob.lazy). - # diff_files = super - diff_files.each { |diff_file| cache_highlight!(diff_file) if cacheable?(diff_file) } - store_highlight_cache + diff_files.each { |diff_file| cache.decorate(diff_file) } diff_files end - def real_size - @merge_request_diff.real_size + override :write_cache + def write_cache + cache.write_if_empty end - def clear_cache! - Rails.cache.delete(cache_key) + override :clear_cache + def clear_cache + cache.clear end def cache_key - [@merge_request_diff, 'highlighted-diff-files', Gitlab::Diff::Line::SERIALIZE_KEYS, diff_options] - end - - private - - def highlight_diff_file_from_cache!(diff_file, cache_diff_lines) - diff_file.highlighted_diff_lines = cache_diff_lines.map do |line| - Gitlab::Diff::Line.init_from_hash(line) - end + cache.key end - # - # If we find the highlighted diff files lines on the cache we replace existing diff_files lines (no highlighted) - # for the highlighted ones, so we just skip their execution. - # If the highlighted diff files lines are not cached we calculate and cache them. - # - # The content of the cache is a Hash where the key identifies the file and the values are Arrays of - # hashes that represent serialized diff lines. - # - def cache_highlight!(diff_file) - item_key = diff_file.file_identifier - - if highlight_cache[item_key] - highlight_diff_file_from_cache!(diff_file, highlight_cache[item_key]) - else - highlight_cache[item_key] = diff_file.highlighted_diff_lines.map(&:to_hash) - end - end - - def highlight_cache - return @highlight_cache if defined?(@highlight_cache) - - @highlight_cache = Rails.cache.read(cache_key) || {} - @highlight_cache_was_empty = @highlight_cache.empty? - @highlight_cache + def real_size + @merge_request_diff.real_size end - def store_highlight_cache - Rails.cache.write(cache_key, highlight_cache, expires_in: 1.week) if @highlight_cache_was_empty - end + private - def cacheable?(diff_file) - @merge_request_diff.present? && diff_file.text? && diff_file.diffable? + def cache + @cache ||= Gitlab::Diff::HighlightCache.new(self) end end end diff --git a/lib/gitlab/diff/highlight_cache.rb b/lib/gitlab/diff/highlight_cache.rb new file mode 100644 index 00000000000..e4390771db2 --- /dev/null +++ b/lib/gitlab/diff/highlight_cache.rb @@ -0,0 +1,68 @@ +# frozen_string_literal: true +# +module Gitlab + module Diff + class HighlightCache + delegate :diffable, to: :@diff_collection + delegate :diff_options, to: :@diff_collection + + def initialize(diff_collection, backend: Rails.cache) + @backend = backend + @diff_collection = diff_collection + end + + # - Reads from cache + # - Assigns DiffFile#highlighted_diff_lines for cached files + def decorate(diff_file) + if content = read_file(diff_file) + diff_file.highlighted_diff_lines = content.map do |line| + Gitlab::Diff::Line.init_from_hash(line) + end + end + end + + # It populates a Hash in order to submit a single write to the memory + # cache. This avoids excessive IO generated by N+1's (1 writing for + # each highlighted line or file). + def write_if_empty + return if cached_content.present? + + @diff_collection.diff_files.each do |diff_file| + next unless cacheable?(diff_file) + + diff_file_id = diff_file.file_identifier + + cached_content[diff_file_id] = diff_file.highlighted_diff_lines.map(&:to_hash) + end + + cache.write(key, cached_content, expires_in: 1.week) + end + + def clear + cache.delete(key) + end + + def key + [diffable, 'highlighted-diff-files', Gitlab::Diff::Line::SERIALIZE_KEYS, diff_options] + end + + private + + def read_file(diff_file) + cached_content[diff_file.file_identifier] + end + + def cache + @backend + end + + def cached_content + @cached_content ||= cache.read(key) || {} + end + + def cacheable?(diff_file) + diffable.present? && diff_file.text? && diff_file.diffable? + end + end + end +end diff --git a/lib/gitlab/git/diff_collection.rb b/lib/gitlab/git/diff_collection.rb index 219c69893ad..20dce8d0e06 100644 --- a/lib/gitlab/git/diff_collection.rb +++ b/lib/gitlab/git/diff_collection.rb @@ -11,7 +11,7 @@ module Gitlab delegate :max_files, :max_lines, :max_bytes, :safe_max_files, :safe_max_lines, :safe_max_bytes, to: :limits - def self.collection_limits(options = {}) + def self.limits(options = {}) limits = {} limits[:max_files] = options.fetch(:max_files, DEFAULT_LIMITS[:max_files]) limits[:max_lines] = options.fetch(:max_lines, DEFAULT_LIMITS[:max_lines]) @@ -19,13 +19,14 @@ module Gitlab limits[:safe_max_files] = [limits[:max_files], DEFAULT_LIMITS[:max_files]].min limits[:safe_max_lines] = [limits[:max_lines], DEFAULT_LIMITS[:max_lines]].min limits[:safe_max_bytes] = limits[:safe_max_files] * 5.kilobytes # Average 5 KB per file + limits[:max_patch_bytes] = Gitlab::Git::Diff::SIZE_LIMIT OpenStruct.new(limits) end def initialize(iterator, options = {}) @iterator = iterator - @limits = self.class.collection_limits(options) + @limits = self.class.limits(options) @enforce_limits = !!options.fetch(:limits, true) @expanded = !!options.fetch(:expanded, true) diff --git a/lib/gitlab/gitaly_client/commit_service.rb b/lib/gitlab/gitaly_client/commit_service.rb index 6a97cd8ed17..aa5b4f94090 100644 --- a/lib/gitlab/gitaly_client/commit_service.rb +++ b/lib/gitlab/gitaly_client/commit_service.rb @@ -369,7 +369,7 @@ module Gitlab request_params[:ignore_whitespace_change] = options.fetch(:ignore_whitespace_change, false) request_params[:enforce_limits] = options.fetch(:limits, true) request_params[:collapse_diffs] = !options.fetch(:expanded, true) - request_params.merge!(Gitlab::Git::DiffCollection.collection_limits(options).to_h) + request_params.merge!(Gitlab::Git::DiffCollection.limits(options).to_h) request = Gitaly::CommitDiffRequest.new(request_params) response = GitalyClient.call(@repository.storage, :diff_service, :commit_diff, request, timeout: GitalyClient.medium_timeout) diff --git a/lib/gitlab/gitaly_client/remote_service.rb b/lib/gitlab/gitaly_client/remote_service.rb index 6415c64b4e2..4661448621b 100644 --- a/lib/gitlab/gitaly_client/remote_service.rb +++ b/lib/gitlab/gitaly_client/remote_service.rb @@ -1,6 +1,8 @@ module Gitlab module GitalyClient class RemoteService + include Gitlab::EncodingHelper + MAX_MSG_SIZE = 128.kilobytes.freeze def self.exists?(remote_url) @@ -61,7 +63,7 @@ module Gitlab response = GitalyClient.call(@storage, :remote_service, :find_remote_root_ref, request) - response.ref.presence + encode_utf8(response.ref) end def update_remote_mirror(ref_name, only_branches_matching) diff --git a/lib/gitlab/gitaly_client/storage_service.rb b/lib/gitlab/gitaly_client/storage_service.rb index eb0e910665b..3a26dd58ff4 100644 --- a/lib/gitlab/gitaly_client/storage_service.rb +++ b/lib/gitlab/gitaly_client/storage_service.rb @@ -5,6 +5,14 @@ module Gitlab @storage = storage end + # Returns all directories in the git storage directory, lexically ordered + def list_directories(depth: 1) + request = Gitaly::ListDirectoriesRequest.new(storage_name: @storage, depth: depth) + + GitalyClient.call(@storage, :storage_service, :list_directories, request) + .flat_map(&:paths) + end + # Delete all repositories in the storage. This is a slow and VERY DESTRUCTIVE operation. def delete_all_repositories request = Gitaly::DeleteAllRepositoriesRequest.new(storage_name: @storage) diff --git a/lib/gitlab/import_export.rb b/lib/gitlab/import_export.rb index be3710c5b7f..53fe2f8e436 100644 --- a/lib/gitlab/import_export.rb +++ b/lib/gitlab/import_export.rb @@ -40,10 +40,6 @@ module Gitlab "#{basename[0..FILENAME_LIMIT]}_export.tar.gz" end - def object_storage? - Feature.enabled?(:import_export_object_storage) - end - def version VERSION end diff --git a/lib/gitlab/import_export/after_export_strategies/base_after_export_strategy.rb b/lib/gitlab/import_export/after_export_strategies/base_after_export_strategy.rb index 83134bb0769..7cbf653dd97 100644 --- a/lib/gitlab/import_export/after_export_strategies/base_after_export_strategy.rb +++ b/lib/gitlab/import_export/after_export_strategies/base_after_export_strategy.rb @@ -53,7 +53,7 @@ module Gitlab end def self.lock_file_path(project) - return unless project.export_path || object_storage? + return unless project.export_path || export_file_exists? lock_path = project.import_export_shared.archive_path @@ -83,8 +83,8 @@ module Gitlab errors.full_messages.each { |msg| project.import_export_shared.add_error_message(msg) } end - def object_storage? - project.export_project_object_exists? + def export_file_exists? + project.export_file_exists? end end end diff --git a/lib/gitlab/import_export/after_export_strategies/web_upload_strategy.rb b/lib/gitlab/import_export/after_export_strategies/web_upload_strategy.rb index dce8f89c0ab..4f29bdcea2c 100644 --- a/lib/gitlab/import_export/after_export_strategies/web_upload_strategy.rb +++ b/lib/gitlab/import_export/after_export_strategies/web_upload_strategy.rb @@ -23,7 +23,7 @@ module Gitlab def strategy_execute handle_response_error(send_file) - project.remove_exported_project_file + project.remove_exports end def handle_response_error(response) @@ -40,15 +40,11 @@ module Gitlab def send_file Gitlab::HTTP.public_send(http_method.downcase, url, send_file_options) # rubocop:disable GitlabSecurity/PublicSend ensure - export_file.close if export_file && !object_storage? + export_file.close if export_file end def export_file - if object_storage? - project.import_export_upload.export_file.file.open - else - File.open(project.export_project_path) - end + project.export_file.open end def send_file_options @@ -63,11 +59,7 @@ module Gitlab end def export_size - if object_storage? - project.import_export_upload.export_file.file.size - else - File.size(project.export_project_path) - end + project.export_file.file.size end end end diff --git a/lib/gitlab/import_export/avatar_restorer.rb b/lib/gitlab/import_export/avatar_restorer.rb index cfa595629f4..ded05f73cf8 100644 --- a/lib/gitlab/import_export/avatar_restorer.rb +++ b/lib/gitlab/import_export/avatar_restorer.rb @@ -19,7 +19,7 @@ module Gitlab private def avatar_export_file - @avatar_export_file ||= Dir["#{avatar_export_path}/*"].first + @avatar_export_file ||= Dir["#{avatar_export_path}/**/*"].first end def avatar_export_path diff --git a/lib/gitlab/import_export/avatar_saver.rb b/lib/gitlab/import_export/avatar_saver.rb index 31ef0490cb3..6ffebf83dd2 100644 --- a/lib/gitlab/import_export/avatar_saver.rb +++ b/lib/gitlab/import_export/avatar_saver.rb @@ -1,8 +1,6 @@ module Gitlab module ImportExport class AvatarSaver - include Gitlab::ImportExport::CommandLineUtil - def initialize(project:, shared:) @project = project @shared = shared @@ -14,19 +12,12 @@ module Gitlab Gitlab::ImportExport::UploadsManager.new( project: @project, shared: @shared, - relative_export_path: 'avatar', - from: avatar_path + relative_export_path: 'avatar' ).save rescue => e @shared.error(e) false end - - private - - def avatar_path - @project.avatar.path - end end end end diff --git a/lib/gitlab/import_export/import_export.yml b/lib/gitlab/import_export/import_export.yml index f69f98a78a3..a19b3c88627 100644 --- a/lib/gitlab/import_export/import_export.yml +++ b/lib/gitlab/import_export/import_export.yml @@ -19,6 +19,9 @@ project_tree: - milestone: - events: - :push_event_payload + - resource_label_events: + - label: + :priorities - :issue_assignees - snippets: - :award_emoji @@ -45,6 +48,9 @@ project_tree: - milestone: - events: - :push_event_payload + - resource_label_events: + - label: + :priorities - pipelines: - notes: - :author @@ -64,6 +70,7 @@ project_tree: - :create_access_levels - :project_feature - :custom_attributes + - :prometheus_metrics - :project_badges - :ci_cd_settings @@ -108,6 +115,9 @@ excluded_attributes: - :remote_mirror_available_overridden - :description_html - :repository_languages + prometheus_metrics: + - :common + - :identifier snippets: - :expired_at merge_request_diff: @@ -133,6 +143,10 @@ excluded_attributes: - :event_id project_badges: - :group_id + resource_label_events: + - :reference + - :reference_html + - :epic_id methods: labels: diff --git a/lib/gitlab/import_export/importer.rb b/lib/gitlab/import_export/importer.rb index 4e179f63d8c..72d5b9b830c 100644 --- a/lib/gitlab/import_export/importer.rb +++ b/lib/gitlab/import_export/importer.rb @@ -92,8 +92,6 @@ module Gitlab end def remove_import_file - return unless Gitlab::ImportExport.object_storage? - upload = @project.import_export_upload return unless upload&.import_file&.file diff --git a/lib/gitlab/import_export/project_tree_restorer.rb b/lib/gitlab/import_export/project_tree_restorer.rb index f4106e03a57..00ea4b833e2 100644 --- a/lib/gitlab/import_export/project_tree_restorer.rb +++ b/lib/gitlab/import_export/project_tree_restorer.rb @@ -199,7 +199,7 @@ module Gitlab end def excluded_keys_for_relation(relation) - @reader.attributes_finder.find_excluded_keys(relation) + reader.attributes_finder.find_excluded_keys(relation) end end end diff --git a/lib/gitlab/import_export/saver.rb b/lib/gitlab/import_export/saver.rb index 3cd153a4fd2..59a74083395 100644 --- a/lib/gitlab/import_export/saver.rb +++ b/lib/gitlab/import_export/saver.rb @@ -18,7 +18,7 @@ module Gitlab Rails.logger.info("Saved project export #{archive_file}") - save_on_object_storage if use_object_storage? + save_upload else @shared.error(Gitlab::ImportExport::Error.new(error_message)) false @@ -27,10 +27,8 @@ module Gitlab @shared.error(e) false ensure - if use_object_storage? - remove_archive - remove_export_path - end + remove_archive + remove_export_path end private @@ -51,7 +49,7 @@ module Gitlab @archive_file ||= File.join(@shared.archive_path, Gitlab::ImportExport.export_filename(project: @project)) end - def save_on_object_storage + def save_upload upload = ImportExportUpload.find_or_initialize_by(project: @project) File.open(archive_file) { |file| upload.export_file = file } @@ -59,12 +57,8 @@ module Gitlab upload.save! end - def use_object_storage? - Gitlab::ImportExport.object_storage? - end - def error_message - "Unable to save #{archive_file} into #{@shared.export_path}. Object storage enabled: #{use_object_storage?}" + "Unable to save #{archive_file} into #{@shared.export_path}." end end end diff --git a/lib/gitlab/import_export/uploads_manager.rb b/lib/gitlab/import_export/uploads_manager.rb index e0d4235e65b..8511319cb1c 100644 --- a/lib/gitlab/import_export/uploads_manager.rb +++ b/lib/gitlab/import_export/uploads_manager.rb @@ -5,18 +5,13 @@ module Gitlab UPLOADS_BATCH_SIZE = 100 - def initialize(project:, shared:, relative_export_path: 'uploads', from: nil) + def initialize(project:, shared:, relative_export_path: 'uploads') @project = project @shared = shared @relative_export_path = relative_export_path - @from = from || default_uploads_path end def save - if File.file?(@from) && @relative_export_path == 'avatar' - copy_files(@from, File.join(uploads_export_path, @project.avatar.filename)) - end - copy_project_uploads true @@ -55,17 +50,11 @@ module Gitlab copy_files(uploader.absolute_path, File.join(uploads_export_path, uploader.upload.path)) else - next unless Gitlab::ImportExport.object_storage? - download_and_copy(uploader) end end end - def default_uploads_path - FileUploader.absolute_base_dir(@project) - end - def uploads_export_path @uploads_export_path ||= File.join(@shared.export_path, @relative_export_path) end diff --git a/lib/gitlab/import_export/uploads_restorer.rb b/lib/gitlab/import_export/uploads_restorer.rb index 25f85936227..b4313ff4cb4 100644 --- a/lib/gitlab/import_export/uploads_restorer.rb +++ b/lib/gitlab/import_export/uploads_restorer.rb @@ -2,30 +2,14 @@ module Gitlab module ImportExport class UploadsRestorer < UploadsSaver def restore - if Gitlab::ImportExport.object_storage? - Gitlab::ImportExport::UploadsManager.new( - project: @project, - shared: @shared - ).restore - elsif File.directory?(uploads_export_path) - copy_files(uploads_export_path, uploads_path) - - true - else - true # Proceed without uploads - end + Gitlab::ImportExport::UploadsManager.new( + project: @project, + shared: @shared + ).restore rescue => e @shared.error(e) false end - - def uploads_path - FileUploader.absolute_base_dir(@project) - end - - def uploads_export_path - @uploads_export_path ||= File.join(@shared.export_path, 'uploads') - end end end end diff --git a/lib/gitlab/import_export/uploads_saver.rb b/lib/gitlab/import_export/uploads_saver.rb index b3f17af5661..0275f686c5e 100644 --- a/lib/gitlab/import_export/uploads_saver.rb +++ b/lib/gitlab/import_export/uploads_saver.rb @@ -1,8 +1,6 @@ module Gitlab module ImportExport class UploadsSaver - include Gitlab::ImportExport::CommandLineUtil - def initialize(project:, shared:) @project = project @shared = shared diff --git a/lib/gitlab/prometheus/additional_metrics_parser.rb b/lib/gitlab/prometheus/additional_metrics_parser.rb index bb1172f82a1..a240d090074 100644 --- a/lib/gitlab/prometheus/additional_metrics_parser.rb +++ b/lib/gitlab/prometheus/additional_metrics_parser.rb @@ -5,7 +5,7 @@ module Gitlab MUTEX = Mutex.new extend self - def load_groups_from_yaml(file_name = 'additional_metrics.yml') + def load_groups_from_yaml(file_name) yaml_metrics_raw(file_name).map(&method(:group_from_entry)) end diff --git a/lib/gitlab/prometheus/metric_group.rb b/lib/gitlab/prometheus/metric_group.rb index e91c6fb2e27..d696a8fc00c 100644 --- a/lib/gitlab/prometheus/metric_group.rb +++ b/lib/gitlab/prometheus/metric_group.rb @@ -4,10 +4,13 @@ module Gitlab include ActiveModel::Model attr_accessor :name, :priority, :metrics + validates :name, :priority, :metrics, presence: true def self.common_metrics - AdditionalMetricsParser.load_groups_from_yaml + ::PrometheusMetric.common.group_by(&:group_title).map do |name, metrics| + MetricGroup.new(name: name, priority: 0, metrics: metrics.map(&:to_query_metric)) + end end # EE only diff --git a/lib/gitlab/template_helper.rb b/lib/gitlab/template_helper.rb index f24a01e6cf5..fc498dde723 100644 --- a/lib/gitlab/template_helper.rb +++ b/lib/gitlab/template_helper.rb @@ -1,24 +1,9 @@ module Gitlab module TemplateHelper - include Gitlab::Utils::StrongMemoize - def prepare_template_environment(file) return unless file - if Gitlab::ImportExport.object_storage? - params[:import_export_upload] = ImportExportUpload.new(import_file: file) - else - FileUtils.mkdir_p(File.dirname(import_upload_path)) - FileUtils.copy_entry(file.path, import_upload_path) - - params[:import_source] = import_upload_path - end - end - - def import_upload_path - strong_memoize(:import_upload_path) do - Gitlab::ImportExport.import_upload_path(filename: tmp_filename) - end + params[:import_export_upload] = ImportExportUpload.new(import_file: file) end def tmp_filename diff --git a/lib/gitlab/workhorse.rb b/lib/gitlab/workhorse.rb index a9629a92a50..30a8c3578d8 100644 --- a/lib/gitlab/workhorse.rb +++ b/lib/gitlab/workhorse.rb @@ -22,18 +22,27 @@ module Gitlab project = repository.project - { + attrs = { GL_ID: Gitlab::GlId.gl_id(user), GL_REPOSITORY: Gitlab::GlRepository.gl_repository(project, is_wiki), GL_USERNAME: user&.username, ShowAllRefs: show_all_refs, Repository: repository.gitaly_repository.to_h, RepoPath: 'ignored but not allowed to be empty in gitlab-workhorse', + GitConfigOptions: [], GitalyServer: { address: Gitlab::GitalyClient.address(project.repository_storage), token: Gitlab::GitalyClient.token(project.repository_storage) } } + + # Custom option for git-receive-pack command + receive_max_input_size = Gitlab::CurrentSettings.receive_max_input_size.to_i + if receive_max_input_size > 0 + attrs[:GitConfigOptions] << "receive.maxInputSize=#{receive_max_input_size.megabytes}" + end + + attrs end def send_git_blob(repository, blob) diff --git a/lib/tasks/gitlab/cleanup.rake b/lib/tasks/gitlab/cleanup.rake index c8a8863443e..e8ae5dfa540 100644 --- a/lib/tasks/gitlab/cleanup.rake +++ b/lib/tasks/gitlab/cleanup.rake @@ -1,40 +1,29 @@ -# Gitaly migration: https://gitlab.com/gitlab-org/gitaly/issues/954 -# +# frozen_string_literal: true +require 'set' + namespace :gitlab do namespace :cleanup do - HASHED_REPOSITORY_NAME = '@hashed'.freeze - desc "GitLab | Cleanup | Clean namespaces" task dirs: :gitlab_environment do - warn_user_is_not_gitlab + namespaces = Set.new(Namespace.pluck(:path)) + namespaces << Storage::HashedProject::ROOT_PATH_PREFIX - namespaces = Namespace.pluck(:path) - namespaces << HASHED_REPOSITORY_NAME # add so that it will be ignored - Gitlab.config.repositories.storages.each do |name, repository_storage| - git_base_path = Gitlab::GitalyClient::StorageSettings.allow_disk_access { repository_storage.legacy_disk_path } - all_dirs = Dir.glob(git_base_path + '/*') + Gitaly::Server.all.each do |server| + all_dirs = Gitlab::GitalyClient::StorageService + .new(server.storage) + .list_directories(depth: 0) + .reject { |dir| dir.ends_with?('.git') || namespaces.include?(File.basename(dir)) } - puts git_base_path.color(:yellow) puts "Looking for directories to remove... " - - all_dirs.reject! do |dir| - # skip if git repo - dir =~ /.git$/ - end - - all_dirs.reject! do |dir| - dir_name = File.basename dir - - # skip if namespace present - namespaces.include?(dir_name) - end - all_dirs.each do |dir_path| if remove? - if FileUtils.rm_rf dir_path - puts "Removed...#{dir_path}".color(:red) - else - puts "Cannot remove #{dir_path}".color(:red) + begin + Gitlab::GitalyClient::NamespaceService.new(server.storage) + .remove(dir_path) + + puts "Removed...#{dir_path}" + rescue StandardError => e + puts "Cannot remove #{dir_path}: #{e.message}".color(:red) end else puts "Can be removed: #{dir_path}".color(:red) @@ -49,29 +38,29 @@ namespace :gitlab do desc "GitLab | Cleanup | Clean repositories" task repos: :gitlab_environment do - warn_user_is_not_gitlab - move_suffix = "+orphaned+#{Time.now.to_i}" - Gitlab.config.repositories.storages.each do |name, repository_storage| - repo_root = Gitlab::GitalyClient::StorageSettings.allow_disk_access { repository_storage.legacy_disk_path } - - # Look for global repos (legacy, depth 1) and normal repos (depth 2) - IO.popen(%W(find #{repo_root} -mindepth 1 -maxdepth 2 -name *.git)) do |find| - find.each_line do |path| - path.chomp! - repo_with_namespace = path - .sub(repo_root, '') - .sub(%r{^/*}, '') - .chomp('.git') - .chomp('.wiki') - - # TODO ignoring hashed repositories for now. But revisit to fully support - # possible orphaned hashed repos - next if repo_with_namespace.start_with?("#{HASHED_REPOSITORY_NAME}/") || Project.find_by_full_path(repo_with_namespace) - - new_path = path + move_suffix - puts path.inspect + ' -> ' + new_path.inspect - File.rename(path, new_path) + + Gitaly::Server.all.each do |server| + Gitlab::GitalyClient::StorageService + .new(server.storage) + .list_directories + .each do |path| + repo_with_namespace = path.chomp('.git').chomp('.wiki') + + # TODO ignoring hashed repositories for now. But revisit to fully support + # possible orphaned hashed repos + next if repo_with_namespace.start_with?(Storage::HashedProject::ROOT_PATH_PREFIX) + next if Project.find_by_full_path(repo_with_namespace) + + new_path = path + move_suffix + puts path.inspect + ' -> ' + new_path.inspect + + begin + Gitlab::GitalyClient::NamespaceService + .new(server.storage) + .rename(path, new_path) + rescue StandardError => e + puts "Error occured while moving the repository: #{e.message}".color(:red) end end end diff --git a/lib/tasks/gitlab/update_templates.rake b/lib/tasks/gitlab/update_templates.rake index a25f7ce59c7..ef6a32d6730 100644 --- a/lib/tasks/gitlab/update_templates.rake +++ b/lib/tasks/gitlab/update_templates.rake @@ -6,6 +6,8 @@ namespace :gitlab do desc "GitLab | Update project templates" task :update_project_templates do + include Gitlab::ImportExport::CommandLineUtil + if Rails.env.production? puts "This rake task is not meant fo production instances".red exit(1) @@ -52,7 +54,7 @@ namespace :gitlab do end Projects::ImportExport::ExportService.new(project, admin).execute - FileUtils.cp(project.export_project_path, template.archive_path) + download_or_copy_upload(project.export_file, template.archive_path) Projects::DestroyService.new(admin, project).execute puts "Exported #{template.name}".green end diff --git a/lib/tasks/migrate/add_limits_mysql.rake b/lib/tasks/migrate/add_limits_mysql.rake index 9b05876034c..c77fa49d586 100644 --- a/lib/tasks/migrate/add_limits_mysql.rake +++ b/lib/tasks/migrate/add_limits_mysql.rake @@ -3,6 +3,7 @@ require Rails.root.join('db/migrate/markdown_cache_limits_to_mysql') require Rails.root.join('db/migrate/merge_request_diff_file_limits_to_mysql') require Rails.root.join('db/migrate/limits_ci_build_trace_chunks_raw_data_for_mysql') require Rails.root.join('db/migrate/gpg_keys_limits_to_mysql') +require Rails.root.join('db/migrate/prometheus_metrics_limits_to_mysql') desc "GitLab | Add limits to strings in mysql database" task add_limits_mysql: :environment do @@ -12,4 +13,5 @@ task add_limits_mysql: :environment do MergeRequestDiffFileLimitsToMysql.new.up LimitsCiBuildTraceChunksRawDataForMysql.new.up IncreaseMysqlTextLimitForGpgKeys.new.up + PrometheusMetricsLimitsToMysql.new.up end diff --git a/locale/gitlab.pot b/locale/gitlab.pot index 0e89c19066e..e9a92a386cb 100644 --- a/locale/gitlab.pot +++ b/locale/gitlab.pot @@ -259,6 +259,9 @@ msgstr "" msgid "A default branch cannot be chosen for an empty project." msgstr "" +msgid "A deleted user" +msgstr "" + msgid "A new branch will be created in your fork and a new merge request will be started." msgstr "" @@ -1035,6 +1038,9 @@ msgstr "" msgid "Browse files" msgstr "" +msgid "Business metrics (Custom)" +msgstr "" + msgid "ByAuthor|by" msgstr "" @@ -1167,6 +1173,12 @@ msgstr "" msgid "Choose a branch/tag (e.g. %{master}) or enter a commit (e.g. %{sha}) to see what's changed or to create a merge request." msgstr "" +msgid "Choose a template..." +msgstr "" + +msgid "Choose a type..." +msgstr "" + msgid "Choose any color." msgstr "" @@ -1882,6 +1894,9 @@ msgstr "" msgid "Contribution guide" msgstr "" +msgid "Contributions for <strong>%{calendar_date}</strong>" +msgstr "" + msgid "Contributors" msgstr "" @@ -2292,9 +2307,21 @@ msgstr "" msgid "Disable group Runners" msgstr "" +msgid "Discard" +msgstr "" + +msgid "Discard all changes" +msgstr "" + +msgid "Discard all unstaged changes?" +msgstr "" + msgid "Discard changes" msgstr "" +msgid "Discard changes to %{path}?" +msgstr "" + msgid "Discard draft" msgstr "" @@ -2691,6 +2718,9 @@ msgstr "" msgid "Fields on this page are now uneditable, you can configure" msgstr "" +msgid "File templates" +msgstr "" + msgid "Files" msgstr "" @@ -2703,6 +2733,9 @@ msgstr "" msgid "Filter by commit message" msgstr "" +msgid "Filter..." +msgstr "" + msgid "Find by path" msgstr "" @@ -3762,9 +3795,6 @@ msgstr "" msgid "More" msgstr "" -msgid "More actions" -msgstr "" - msgid "More information" msgstr "" @@ -3905,6 +3935,9 @@ msgstr "" msgid "No container images stored for this project. Add one by following the instructions above." msgstr "" +msgid "No contributions were found" +msgstr "" + msgid "No due date" msgstr "" @@ -3977,6 +4010,9 @@ msgstr "" msgid "Not enough data" msgstr "" +msgid "Not now" +msgstr "" + msgid "Note that the master branch is automatically protected. %{link_to_protected_branches}" msgstr "" @@ -4441,6 +4477,9 @@ 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 "" @@ -4450,15 +4489,30 @@ msgstr "" msgid "Profiles|Add status emoji" msgstr "" +msgid "Profiles|Avatar cropper" +msgstr "" + +msgid "Profiles|Avatar will be removed. Are you sure?" +msgstr "" + msgid "Profiles|Change username" msgstr "" +msgid "Profiles|Choose file..." +msgstr "" + +msgid "Profiles|Choose to show contributions of private projects on your public profile without any project, repository or organization information." +msgstr "" + msgid "Profiles|Clear status" msgstr "" msgid "Profiles|Current path: %{path}" msgstr "" +msgid "Profiles|Current status" +msgstr "" + msgid "Profiles|Delete Account" msgstr "" @@ -4471,39 +4525,108 @@ msgstr "" msgid "Profiles|Deleting an account has the following effects:" msgstr "" +msgid "Profiles|Do not show on profile" +msgstr "" + +msgid "Profiles|Don't display activity-related personal information on your profiles" +msgstr "" + +msgid "Profiles|Edit Profile" +msgstr "" + msgid "Profiles|Invalid password" msgstr "" msgid "Profiles|Invalid username" msgstr "" +msgid "Profiles|Main settings" +msgstr "" + +msgid "Profiles|No file chosen" +msgstr "" + msgid "Profiles|Path" msgstr "" +msgid "Profiles|Position and size your new avatar" +msgstr "" + +msgid "Profiles|Private contributions" +msgstr "" + +msgid "Profiles|Public Avatar" +msgstr "" + +msgid "Profiles|Remove avatar" +msgstr "" + +msgid "Profiles|Set new profile picture" +msgstr "" + +msgid "Profiles|Some options are unavailable for LDAP accounts" +msgstr "" + +msgid "Profiles|Tell us about yourself in fewer than 250 characters." +msgstr "" + +msgid "Profiles|The maximum file size allowed is 200KB." +msgstr "" + msgid "Profiles|This doesn't look like a public SSH key, are you sure you want to add it?" msgstr "" +msgid "Profiles|This email will be displayed on your public profile." +msgstr "" + msgid "Profiles|This emoji and message will appear on your profile and throughout the interface." msgstr "" +msgid "Profiles|This feature is experimental and translations are not complete yet." +msgstr "" + +msgid "Profiles|This information will appear on your profile." +msgstr "" + msgid "Profiles|Type your %{confirmationValue} to confirm:" msgstr "" msgid "Profiles|Typically starts with \"ssh-rsa …\"" msgstr "" +msgid "Profiles|Update profile settings" +msgstr "" + msgid "Profiles|Update username" msgstr "" +msgid "Profiles|Upload new avatar" +msgstr "" + msgid "Profiles|Username change failed - %{message}" msgstr "" msgid "Profiles|Username successfully changed" msgstr "" +msgid "Profiles|Website" +msgstr "" + msgid "Profiles|What's your status?" msgstr "" +msgid "Profiles|You can change your avatar here" +msgstr "" + +msgid "Profiles|You can change your avatar here or remove the current avatar to revert to %{gravatar_link}" +msgstr "" + +msgid "Profiles|You can upload your avatar here" +msgstr "" + +msgid "Profiles|You can upload your avatar here or change it at %{gravatar_link}" +msgstr "" + msgid "Profiles|You don't have access to delete this user." msgstr "" @@ -4513,6 +4636,15 @@ msgstr "" msgid "Profiles|Your account is currently an owner in these groups:" msgstr "" +msgid "Profiles|Your email address was automatically set based on your %{provider_label} account." +msgstr "" + +msgid "Profiles|Your location was automatically set based on your %{provider_label} account." +msgstr "" + +msgid "Profiles|Your name was automatically set based on your %{provider_label} account, so people you know can recognize you." +msgstr "" + msgid "Profiles|Your status" msgstr "" @@ -4917,6 +5049,21 @@ msgstr "" msgid "Resolve discussion" msgstr "" +msgid "Response metrics (AWS ELB)" +msgstr "" + +msgid "Response metrics (Custom)" +msgstr "" + +msgid "Response metrics (HA Proxy)" +msgstr "" + +msgid "Response metrics (NGINX Ingress)" +msgstr "" + +msgid "Response metrics (NGINX)" +msgstr "" + msgid "Resume" msgstr "" @@ -5135,6 +5282,9 @@ msgstr "" msgid "Send email" msgstr "" +msgid "Send usage data" +msgstr "" + msgid "Sep" msgstr "" @@ -5500,6 +5650,12 @@ msgstr "" msgid "System Info" msgstr "" +msgid "System metrics (Custom)" +msgstr "" + +msgid "System metrics (Kubernetes)" +msgstr "" + msgid "Tag (%{tag_count})" msgid_plural "Tags (%{tag_count})" msgstr[0] "" @@ -5733,6 +5889,12 @@ msgstr "" msgid "There are no projects shared with this group yet" msgstr "" +msgid "There are no staged changes" +msgstr "" + +msgid "There are no unstaged changes" +msgstr "" + msgid "There are problems accessing Git storage: " msgstr "" @@ -5772,6 +5934,9 @@ msgstr "" msgid "This branch has changed since you started editing. Would you like to create a new branch?" msgstr "" +msgid "This container registry has been scheduled for deletion." +msgstr "" + msgid "This diff is collapsed." msgstr "" @@ -6091,6 +6256,9 @@ msgstr "" msgid "To help improve GitLab and its user experience, GitLab will periodically collect usage information." msgstr "" +msgid "To help improve GitLab, we would like to periodically collect usage information. This can be changed at any time in %{settings_link_start}Settings%{link_end}. %{info_link_start}More Information%{link_end}" +msgstr "" + msgid "To import GitHub repositories, you can use a %{personal_access_token_link}. When you create your Personal Access Token, you will need to select the <code>repo</code> scope, so we can display a list of your public and private repositories which are available to import." msgstr "" @@ -6175,6 +6343,9 @@ msgstr "" msgid "Unable to load the diff. %{button_try_again}" msgstr "" +msgid "Undo" +msgstr "" + msgid "Unlock" msgstr "" @@ -6187,6 +6358,9 @@ msgstr "" msgid "Unresolve discussion" msgstr "" +msgid "Unstage" +msgstr "" + msgid "Unstage all changes" msgstr "" @@ -6241,9 +6415,6 @@ msgstr "" msgid "Upload file" msgstr "" -msgid "Upload new avatar" -msgstr "" - msgid "UploadLink|click to upload" msgstr "" @@ -6289,9 +6460,6 @@ msgstr "" msgid "Users" msgstr "" -msgid "User|Current status" -msgstr "" - msgid "Variables" msgstr "" @@ -6604,6 +6772,12 @@ msgstr "" msgid "You need permission." msgstr "" +msgid "You will loose all changes you've made to this file. This action cannot be undone." +msgstr "" + +msgid "You will loose all the unstaged changes you've made in this project. This action cannot be undone." +msgstr "" + msgid "You will not get any notifications via email" msgstr "" diff --git a/package.json b/package.json index 7e6ddf0fca7..d5b747b4131 100644 --- a/package.json +++ b/package.json @@ -18,7 +18,7 @@ "webpack-prod": "NODE_ENV=production webpack --config config/webpack.config.js" }, "dependencies": { - "@gitlab-org/gitlab-svgs": "^1.28.0", + "@gitlab-org/gitlab-svgs": "^1.29.0", "@gitlab-org/gitlab-ui": "1.0.5", "autosize": "^4.0.0", "axios": "^0.17.1", diff --git a/public/robots.txt b/public/robots.txt index 1f9d42f4adc..ea931e1a223 100644 --- a/public/robots.txt +++ b/public/robots.txt @@ -21,6 +21,8 @@ Disallow: /groups/new Disallow: /groups/*/edit Disallow: /users Disallow: /help +# Only specifically allow the Sign In page to avoid very ugly search results +Allow: /users/sign_in # Global snippets User-Agent: * diff --git a/qa/Gemfile.lock b/qa/Gemfile.lock index 8f523e55adc..8d28fcacc05 100644 --- a/qa/Gemfile.lock +++ b/qa/Gemfile.lock @@ -32,7 +32,7 @@ GEM diff-lcs (1.3) domain_name (0.5.20170404) unf (>= 0.0.5, < 1.0.0) - ffi (1.9.18) + ffi (1.9.25) http-cookie (1.0.3) domain_name (~> 0.5) i18n (0.9.1) diff --git a/spec/bin/changelog_spec.rb b/spec/bin/changelog_spec.rb index 9dc4edf97d1..c59add88a82 100644 --- a/spec/bin/changelog_spec.rb +++ b/spec/bin/changelog_spec.rb @@ -95,6 +95,7 @@ describe 'bin/changelog' do it 'shows error message and exits the program' do allow($stdin).to receive(:getc).and_return(type) + expect do expect { described_class.read_type }.to raise_error( ChangelogHelpers::Abort, diff --git a/spec/controllers/admin/application_settings_controller_spec.rb b/spec/controllers/admin/application_settings_controller_spec.rb index 9d10d725ff3..10e1bfc30f9 100644 --- a/spec/controllers/admin/application_settings_controller_spec.rb +++ b/spec/controllers/admin/application_settings_controller_spec.rb @@ -78,5 +78,12 @@ describe Admin::ApplicationSettingsController do expect(response).to redirect_to(admin_application_settings_path) expect(ApplicationSetting.current.restricted_visibility_levels).to be_empty end + + it 'updates the receive_max_input_size setting' do + put :update, application_setting: { receive_max_input_size: "1024" } + + expect(response).to redirect_to(admin_application_settings_path) + expect(ApplicationSetting.current.receive_max_input_size).to eq(1024) + end end end diff --git a/spec/controllers/projects/jobs_controller_spec.rb b/spec/controllers/projects/jobs_controller_spec.rb index ca7d30fec83..751919f9501 100644 --- a/spec/controllers/projects/jobs_controller_spec.rb +++ b/spec/controllers/projects/jobs_controller_spec.rb @@ -166,8 +166,8 @@ describe Projects::JobsController, :clean_gitlab_redis_shared_state do expect(response).to match_response_schema('job/job_details') expect(json_response['artifact']['download_path']).to match(%r{artifacts/download}) expect(json_response['artifact']['browse_path']).to match(%r{artifacts/browse}) - expect(json_response['artifact']).not_to have_key(:expired) - expect(json_response['artifact']).not_to have_key(:expired_at) + expect(json_response['artifact']).not_to have_key('expired') + expect(json_response['artifact']).not_to have_key('expired_at') end end @@ -177,8 +177,8 @@ describe Projects::JobsController, :clean_gitlab_redis_shared_state do it 'exposes needed information' do expect(response).to have_gitlab_http_status(:ok) expect(response).to match_response_schema('job/job_details') - expect(json_response['artifact']).not_to have_key(:download_path) - expect(json_response['artifact']).not_to have_key(:browse_path) + expect(json_response['artifact']).not_to have_key('download_path') + expect(json_response['artifact']).not_to have_key('browse_path') expect(json_response['artifact']['expired']).to eq(true) expect(json_response['artifact']['expire_at']).not_to be_empty end diff --git a/spec/controllers/projects/notes_controller_spec.rb b/spec/controllers/projects/notes_controller_spec.rb index 1458113b90c..81badaac76b 100644 --- a/spec/controllers/projects/notes_controller_spec.rb +++ b/spec/controllers/projects/notes_controller_spec.rb @@ -154,7 +154,7 @@ describe Projects::NotesController do get :index, request_params expect(parsed_response[:notes].count).to eq(1) - expect(note_json[:id]).to eq(note.id) + expect(note_json[:id]).to eq(note.id.to_s) end it 'does not result in N+1 queries' do diff --git a/spec/controllers/projects/registry/repositories_controller_spec.rb b/spec/controllers/projects/registry/repositories_controller_spec.rb index 17769a14def..d11e42b411b 100644 --- a/spec/controllers/projects/registry/repositories_controller_spec.rb +++ b/spec/controllers/projects/registry/repositories_controller_spec.rb @@ -86,9 +86,10 @@ describe Projects::Registry::RepositoriesController do stub_container_registry_tags(repository: :any, tags: []) end - it 'deletes a repository' do - expect { delete_repository(repository) }.to change { ContainerRepository.all.count }.by(-1) + it 'schedules a job to delete a repository' do + expect(DeleteContainerRepositoryWorker).to receive(:perform_async).with(user.id, repository.id) + delete_repository(repository) expect(response).to have_gitlab_http_status(:no_content) end end diff --git a/spec/controllers/projects_controller_spec.rb b/spec/controllers/projects_controller_spec.rb index c3a66477b6a..3bc9cbe64c5 100644 --- a/spec/controllers/projects_controller_spec.rb +++ b/spec/controllers/projects_controller_spec.rb @@ -803,37 +803,7 @@ describe ProjectsController do project.add_maintainer(user) end - context 'object storage disabled' do - before do - stub_feature_flags(import_export_object_storage: false) - end - - context 'when project export is enabled' do - it 'returns 302' do - get :download_export, namespace_id: project.namespace, id: project - - expect(response).to have_gitlab_http_status(302) - end - end - - context 'when project export is disabled' do - before do - stub_application_setting(project_export_enabled?: false) - end - - it 'returns 404' do - get :download_export, namespace_id: project.namespace, id: project - - expect(response).to have_gitlab_http_status(404) - end - end - end - context 'object storage enabled' do - before do - stub_feature_flags(import_export_object_storage: true) - end - context 'when project export is enabled' do it 'returns 302' do get :download_export, namespace_id: project.namespace, id: project diff --git a/spec/db/development/import_common_metrics_spec.rb b/spec/db/development/import_common_metrics_spec.rb new file mode 100644 index 00000000000..25061ef0887 --- /dev/null +++ b/spec/db/development/import_common_metrics_spec.rb @@ -0,0 +1,15 @@ +# frozen_string_literal: true + +require 'spec_helper' + +describe 'Import metrics on development seed' do + subject { load Rails.root.join('db', 'fixtures', 'development', '99_common_metrics.rb') } + + it "imports all prometheus metrics" do + expect(PrometheusMetric.common).to be_empty + + subject + + expect(PrometheusMetric.common).not_to be_empty + end +end diff --git a/spec/db/importers/common_metrics_importer_spec.rb b/spec/db/importers/common_metrics_importer_spec.rb new file mode 100644 index 00000000000..16b59e1dfe8 --- /dev/null +++ b/spec/db/importers/common_metrics_importer_spec.rb @@ -0,0 +1,121 @@ +# frozen_string_literal: true + +require 'rails_helper' +require Rails.root.join("db", "importers", "common_metrics_importer.rb") + +describe Importers::PrometheusMetric do + it 'group enum equals ::PrometheusMetric' do + expect(described_class.groups).to eq(::PrometheusMetric.groups) + end + + it 'GROUP_TITLES equals ::PrometheusMetric' do + expect(described_class::GROUP_TITLES).to eq(::PrometheusMetric::GROUP_TITLES) + end +end + +describe Importers::CommonMetricsImporter do + subject { described_class.new } + + context "does import common_metrics.yml" do + let(:groups) { subject.content } + let(:metrics) { groups.map { |group| group['metrics'] }.flatten } + let(:queries) { metrics.map { |group| group['queries'] }.flatten } + let(:query_ids) { queries.map { |query| query['id'] } } + + before do + subject.execute + end + + it "has the same amount of groups" do + expect(PrometheusMetric.common.group(:group).count.count).to eq(groups.count) + end + + it "has the same amount of metrics" do + expect(PrometheusMetric.common.group(:group, :title).count.count).to eq(metrics.count) + end + + it "has the same amount of queries" do + expect(PrometheusMetric.common.count).to eq(queries.count) + end + + it "does not have duplicate IDs" do + expect(query_ids).to eq(query_ids.uniq) + end + + it "imports all IDs" do + expect(PrometheusMetric.common.pluck(:identifier)).to contain_exactly(*query_ids) + end + end + + context 'does import properly all fields' do + let(:query_identifier) { 'response-metric' } + let(:group) do + { + group: 'Response metrics (NGINX Ingress)', + metrics: [{ + title: "Throughput", + y_label: "Requests / Sec", + queries: [{ + id: query_identifier, + query_range: 'my-query', + unit: 'my-unit', + label: 'status code' + }] + }] + } + end + + before do + expect(subject).to receive(:content) { [group.deep_stringify_keys] } + end + + shared_examples 'stores metric' do + let(:metric) { PrometheusMetric.find_by(identifier: query_identifier) } + + it 'with all data' do + expect(metric.group).to eq('nginx_ingress') + expect(metric.title).to eq('Throughput') + expect(metric.y_label).to eq('Requests / Sec') + expect(metric.unit).to eq('my-unit') + expect(metric.legend).to eq('status code') + expect(metric.query).to eq('my-query') + end + end + + context 'if ID is missing' do + let(:query_identifier) { } + + it 'raises exception' do + expect { subject.execute }.to raise_error(described_class::MissingQueryId) + end + end + + context 'for existing common metric with different ID' do + let!(:existing_metric) { create(:prometheus_metric, :common, identifier: 'my-existing-metric') } + + before do + subject.execute + end + + it_behaves_like 'stores metric' do + it 'and existing metric is not changed' do + expect(metric).not_to eq(existing_metric) + end + end + end + + context 'when metric with ID exists ' do + let!(:existing_metric) { create(:prometheus_metric, :common, identifier: 'response-metric') } + + before do + subject.execute + end + + it_behaves_like 'stores metric' do + it 'and existing metric is changed' do + expect(metric).to eq(existing_metric) + end + end + end + end +end diff --git a/spec/db/production/import_common_metrics_spec.rb b/spec/db/production/import_common_metrics_spec.rb new file mode 100644 index 00000000000..1e4ff818a86 --- /dev/null +++ b/spec/db/production/import_common_metrics_spec.rb @@ -0,0 +1,15 @@ +# frozen_string_literal: true + +require 'spec_helper' + +describe 'Import metrics on production seed' do + subject { load Rails.root.join('db', 'fixtures', 'production', '999_common_metrics.rb') } + + it "imports all prometheus metrics" do + expect(PrometheusMetric.common).to be_empty + + subject + + expect(PrometheusMetric.common).not_to be_empty + end +end diff --git a/spec/factories/projects.rb b/spec/factories/projects.rb index dd6525b9622..80801eb1082 100644 --- a/spec/factories/projects.rb +++ b/spec/factories/projects.rb @@ -103,27 +103,11 @@ FactoryBot.define do end trait :with_export do - before(:create) do |_project, _evaluator| - allow(Feature).to receive(:enabled?).with(:import_export_object_storage) { false } - allow(Feature).to receive(:enabled?).with('import_export_object_storage') { false } - end - after(:create) do |project, _evaluator| ProjectExportWorker.new.perform(project.creator.id, project.id) end end - trait :with_object_export do - before(:create) do |_project, _evaluator| - allow(Feature).to receive(:enabled?).with(:import_export_object_storage) { true } - allow(Feature).to receive(:enabled?).with('import_export_object_storage') { true } - end - - after(:create) do |project, evaluator| - ProjectExportWorker.new.perform(project.creator.id, project.id) - end - end - trait :broken_storage do after(:create) do |project| project.update_column(:repository_storage, 'broken') diff --git a/spec/factories/prometheus_metrics.rb b/spec/factories/prometheus_metrics.rb new file mode 100644 index 00000000000..c56644bfb96 --- /dev/null +++ b/spec/factories/prometheus_metrics.rb @@ -0,0 +1,18 @@ +# frozen_string_literal: true + +FactoryBot.define do + factory :prometheus_metric, class: PrometheusMetric do + title 'title' + query 'avg(metric)' + y_label 'y_label' + unit 'm/s' + group :business + project + legend 'legend' + + trait :common do + common true + project nil + end + end +end diff --git a/spec/factories/resource_label_events.rb b/spec/factories/resource_label_events.rb index a67ad78c098..739ba901052 100644 --- a/spec/factories/resource_label_events.rb +++ b/spec/factories/resource_label_events.rb @@ -2,9 +2,12 @@ FactoryBot.define do factory :resource_label_event do - user { issue.project.creator } action :add label - issue + user { issuable&.author || create(:user) } + + after(:build) do |event, evaluator| + event.issue = create(:issue) unless event.issuable + end end end diff --git a/spec/features/issues/resource_label_events_spec.rb b/spec/features/issues/resource_label_events_spec.rb new file mode 100644 index 00000000000..40c452c991a --- /dev/null +++ b/spec/features/issues/resource_label_events_spec.rb @@ -0,0 +1,60 @@ +# frozen_string_literal: true + +require 'rails_helper' + +describe 'List issue resource label events', :js do + let(:user) { create(:user) } + let(:project) { create(:project, :public) } + let(:issue) { create(:issue, project: project, author: user) } + let!(:label) { create(:label, project: project, title: 'foo') } + + context 'when user displays the issue' do + let!(:note) { create(:note_on_issue, author: user, project: project, noteable: issue, note: 'some note') } + let!(:event) { create(:resource_label_event, user: user, issue: issue, label: label) } + + before do + visit project_issue_path(project, issue) + wait_for_requests + end + + it 'shows both notes and resource label events' do + page.within('#notes') do + expect(find("#note_#{note.id}")).to have_content 'some note' + expect(find("#note_#{event.discussion_id}")).to have_content 'added foo label' + end + end + end + + context 'when user adds label to the issue' do + def toggle_labels(labels) + page.within '.labels' do + click_link 'Edit' + wait_for_requests + + labels.each { |label| click_link label } + + click_link 'Edit' + wait_for_requests + end + end + + before do + create(:label, project: project, title: 'bar') + project.add_developer(user) + + sign_in(user) + visit project_issue_path(project, issue) + wait_for_requests + end + + it 'shows add note for newly added labels' do + toggle_labels(%w(foo bar)) + visit project_issue_path(project, issue) + wait_for_requests + + page.within('#notes') do + expect(page).to have_content 'added bar foo labels' + end + end + end +end diff --git a/spec/features/projects/import_export/export_file_spec.rb b/spec/features/projects/import_export/export_file_spec.rb index eb281cd2122..8a418356541 100644 --- a/spec/features/projects/import_export/export_file_spec.rb +++ b/spec/features/projects/import_export/export_file_spec.rb @@ -25,7 +25,6 @@ describe 'Import/Export - project export integration test', :js do before do allow_any_instance_of(Gitlab::ImportExport).to receive(:storage_path).and_return(export_path) - stub_feature_flags(import_export_object_storage: false) end after do diff --git a/spec/features/projects/import_export/import_file_object_storage_spec.rb b/spec/features/projects/import_export/import_file_object_storage_spec.rb deleted file mode 100644 index 0d364543916..00000000000 --- a/spec/features/projects/import_export/import_file_object_storage_spec.rb +++ /dev/null @@ -1,103 +0,0 @@ -require 'spec_helper' - -describe 'Import/Export - project import integration test', :js do - include Select2Helper - - let(:user) { create(:user) } - let(:file) { File.join(Rails.root, 'spec', 'features', 'projects', 'import_export', 'test_project_export.tar.gz') } - let(:export_path) { "#{Dir.tmpdir}/import_file_spec" } - - before do - stub_feature_flags(import_export_object_storage: true) - stub_uploads_object_storage(FileUploader) - allow_any_instance_of(Gitlab::ImportExport).to receive(:storage_path).and_return(export_path) - gitlab_sign_in(user) - end - - after do - FileUtils.rm_rf(export_path, secure: true) - end - - context 'when selecting the namespace' do - let(:user) { create(:admin) } - let!(:namespace) { user.namespace } - let(:project_path) { 'test-project-path' + SecureRandom.hex } - - context 'prefilled the path' do - it 'user imports an exported project successfully' do - visit new_project_path - - select2(namespace.id, from: '#project_namespace_id') - fill_in :project_path, with: project_path, visible: true - click_import_project_tab - click_link 'GitLab export' - - expect(page).to have_content('Import an exported GitLab project') - expect(URI.parse(current_url).query).to eq("namespace_id=#{namespace.id}&path=#{project_path}") - - attach_file('file', file) - click_on 'Import project' - - expect(Project.count).to eq(1) - - project = Project.last - expect(project).not_to be_nil - expect(project.description).to eq("Foo Bar") - expect(project.issues).not_to be_empty - expect(project.merge_requests).not_to be_empty - expect(project_hook_exists?(project)).to be true - expect(wiki_exists?(project)).to be true - expect(project.import_state.status).to eq('finished') - end - end - - context 'path is not prefilled' do - it 'user imports an exported project successfully' do - visit new_project_path - click_import_project_tab - click_link 'GitLab export' - - fill_in :path, with: 'test-project-path', visible: true - attach_file('file', file) - - expect { click_on 'Import project' }.to change { Project.count }.by(1) - - project = Project.last - expect(project).not_to be_nil - expect(page).to have_content("Project 'test-project-path' is being imported") - end - end - end - - it 'invalid project' do - project = create(:project, namespace: user.namespace) - - visit new_project_path - - select2(user.namespace.id, from: '#project_namespace_id') - fill_in :project_path, with: project.name, visible: true - click_import_project_tab - click_link 'GitLab export' - attach_file('file', file) - click_on 'Import project' - - page.within('.flash-container') do - expect(page).to have_content('Project could not be imported') - end - end - - def wiki_exists?(project) - wiki = ProjectWiki.new(project) - wiki.repository.exists? && !wiki.repository.empty? - end - - def project_hook_exists?(project) - Gitlab::GitalyClient::StorageSettings.allow_disk_access do - Gitlab::Git::Hook.new('post-receive', project.repository.raw_repository).exists? - end - end - - def click_import_project_tab - find('#import-project-tab').click - end -end diff --git a/spec/features/projects/import_export/import_file_spec.rb b/spec/features/projects/import_export/import_file_spec.rb index 2d86115de12..2936482a1f7 100644 --- a/spec/features/projects/import_export/import_file_spec.rb +++ b/spec/features/projects/import_export/import_file_spec.rb @@ -8,7 +8,7 @@ describe 'Import/Export - project import integration test', :js do let(:export_path) { "#{Dir.tmpdir}/import_file_spec" } before do - stub_feature_flags(import_export_object_storage: false) + stub_uploads_object_storage(FileUploader) allow_any_instance_of(Gitlab::ImportExport).to receive(:storage_path).and_return(export_path) gitlab_sign_in(user) end @@ -33,7 +33,6 @@ describe 'Import/Export - project import integration test', :js do expect(page).to have_content('Import an exported GitLab project') expect(URI.parse(current_url).query).to eq("namespace_id=#{namespace.id}&path=#{project_path}") - expect(Gitlab::ImportExport).to receive(:import_upload_path).with(filename: /\A\h{32}\z/).and_call_original attach_file('file', file) click_on 'Import project' diff --git a/spec/features/projects/import_export/namespace_export_file_spec.rb b/spec/features/projects/import_export/namespace_export_file_spec.rb deleted file mode 100644 index 9bb8a2063b5..00000000000 --- a/spec/features/projects/import_export/namespace_export_file_spec.rb +++ /dev/null @@ -1,68 +0,0 @@ -require 'spec_helper' - -describe 'Import/Export - Namespace export file cleanup', :js do - let(:export_path) { Dir.mktmpdir('namespace_export_file_spec') } - - before do - allow(Gitlab::ImportExport).to receive(:storage_path).and_return(export_path) - stub_feature_flags(import_export_object_storage: false) - end - - after do - FileUtils.rm_rf(export_path, secure: true) - end - - shared_examples_for 'handling project exports on namespace change' do - let!(:old_export_path) { project.export_path } - - before do - sign_in(create(:admin)) - - setup_export_project - end - - context 'moving the namespace' do - it 'removes the export file' do - expect(File).to exist(old_export_path) - - project.namespace.update!(path: build(:namespace).path) - - expect(File).not_to exist(old_export_path) - end - end - - context 'deleting the namespace' do - it 'removes the export file' do - expect(File).to exist(old_export_path) - - project.namespace.destroy - - expect(File).not_to exist(old_export_path) - end - end - end - - describe 'legacy storage' do - let(:project) { create(:project, :legacy_storage) } - - it_behaves_like 'handling project exports on namespace change' - end - - describe 'hashed storage' do - let(:project) { create(:project) } - - it_behaves_like 'handling project exports on namespace change' - end - - def setup_export_project - visit edit_project_path(project) - - expect(page).to have_content('Export project') - - find(:link, 'Export project').send_keys(:return) - - visit edit_project_path(project) - - expect(page).to have_content('Download export') - end -end diff --git a/spec/features/projects/import_export/test_project_export.tar.gz b/spec/features/projects/import_export/test_project_export.tar.gz Binary files differindex 3b5df47e0b6..730e586b278 100644 --- a/spec/features/projects/import_export/test_project_export.tar.gz +++ b/spec/features/projects/import_export/test_project_export.tar.gz diff --git a/spec/features/usage_stats_consent_spec.rb b/spec/features/usage_stats_consent_spec.rb new file mode 100644 index 00000000000..dd8f3179895 --- /dev/null +++ b/spec/features/usage_stats_consent_spec.rb @@ -0,0 +1,45 @@ +# frozen_string_literal: true + +require 'spec_helper' + +describe 'Usage stats consent' do + context 'when signed in' do + let(:user) { create(:admin, created_at: 8.days.ago) } + let(:message) { 'To help improve GitLab, we would like to periodically collect usage information.' } + + before do + allow(user).to receive(:has_current_license?).and_return false + + gitlab_sign_in(user) + end + + it 'hides the banner permanently when sets usage stats' do + visit root_dashboard_path + + expect(page).to have_content(message) + + click_link 'Send usage data' + + expect(page).not_to have_content(message) + expect(page).to have_content('Application settings saved successfully') + + gitlab_sign_out + gitlab_sign_in(user) + visit root_dashboard_path + + expect(page).not_to have_content(message) + end + + it 'shows banner on next session if user did not set usage stats' do + visit root_dashboard_path + + expect(page).to have_content(message) + + gitlab_sign_out + gitlab_sign_in(user) + visit root_dashboard_path + + expect(page).to have_content(message) + end + end +end diff --git a/spec/finders/contributed_projects_finder_spec.rb b/spec/finders/contributed_projects_finder_spec.rb index 9155a8d6fe9..81fb4e3561c 100644 --- a/spec/finders/contributed_projects_finder_spec.rb +++ b/spec/finders/contributed_projects_finder_spec.rb @@ -8,6 +8,7 @@ describe ContributedProjectsFinder do let!(:public_project) { create(:project, :public) } let!(:private_project) { create(:project, :private) } + let!(:internal_project) { create(:project, :internal) } before do private_project.add_maintainer(source_user) @@ -16,17 +17,18 @@ describe ContributedProjectsFinder do create(:push_event, project: public_project, author: source_user) create(:push_event, project: private_project, author: source_user) + create(:push_event, project: internal_project, author: source_user) end - describe 'without a current user' do + describe 'activity without a current user' do subject { finder.execute } - it { is_expected.to eq([public_project]) } + it { is_expected.to match_array([public_project]) } end - describe 'with a current user' do + describe 'activity with a current user' do subject { finder.execute(current_user) } - it { is_expected.to eq([private_project, public_project]) } + it { is_expected.to match_array([private_project, internal_project, public_project]) } end end diff --git a/spec/finders/user_recent_events_finder_spec.rb b/spec/finders/user_recent_events_finder_spec.rb index 58470f4c84d..c5fcd68eb4c 100644 --- a/spec/finders/user_recent_events_finder_spec.rb +++ b/spec/finders/user_recent_events_finder_spec.rb @@ -13,49 +13,25 @@ describe UserRecentEventsFinder do subject(:finder) { described_class.new(current_user, project_owner) } describe '#execute' do - context 'current user does not have access to projects' do - it 'returns public and internal events' do - records = finder.execute - - expect(records).to include(public_event, internal_event) - expect(records).not_to include(private_event) + context 'when profile is public' do + it 'returns all the events' do + expect(finder.execute).to include(private_event, internal_event, public_event) end end - context 'when current user has access to the projects' do - before do - private_project.add_developer(current_user) - internal_project.add_developer(current_user) - public_project.add_developer(current_user) - end - - context 'when profile is public' do - it 'returns all the events' do - expect(finder.execute).to include(private_event, internal_event, public_event) - end - end - - context 'when profile is private' do - it 'returns no event' do - allow(Ability).to receive(:allowed?).and_call_original - allow(Ability).to receive(:allowed?).with(current_user, :read_user_profile, project_owner).and_return(false) - expect(finder.execute).to be_empty - end - end + context 'when profile is private' do + it 'returns no event' do + allow(Ability).to receive(:allowed?).and_call_original + allow(Ability).to receive(:allowed?).with(current_user, :read_user_profile, project_owner).and_return(false) - it 'does not include the events if the user cannot read cross project' do - expect(Ability).to receive(:allowed?).and_call_original - expect(Ability).to receive(:allowed?).with(current_user, :read_cross_project) { false } expect(finder.execute).to be_empty end end - context 'when current user is anonymous' do - let(:current_user) { nil } - - it 'returns public events only' do - expect(finder.execute).to eq([public_event]) - end + it 'does not include the events if the user cannot read cross project' do + expect(Ability).to receive(:allowed?).and_call_original + expect(Ability).to receive(:allowed?).with(current_user, :read_cross_project) { false } + expect(finder.execute).to be_empty end end end diff --git a/spec/javascripts/boards/mock_data.js b/spec/javascripts/boards/mock_data.js index 81f1a97112f..f380ef450db 100644 --- a/spec/javascripts/boards/mock_data.js +++ b/spec/javascripts/boards/mock_data.js @@ -27,7 +27,7 @@ export const listObjDuplicate = { export const BoardsMockData = { GET: { - '/test/-/boards/1/lists/300/issues?id=300&page=1&=': { + '/test/-/boards/1/lists/300/issues?id=300&page=1': { issues: [ { title: 'Testing', diff --git a/spec/javascripts/boards/utils/query_data_spec.js b/spec/javascripts/boards/utils/query_data_spec.js deleted file mode 100644 index 922215ffc1d..00000000000 --- a/spec/javascripts/boards/utils/query_data_spec.js +++ /dev/null @@ -1,27 +0,0 @@ -import queryData from '~/boards/utils/query_data'; - -describe('queryData', () => { - it('parses path for label with trailing +', () => { - expect( - queryData('label_name[]=label%2B', {}), - ).toEqual({ - label_name: ['label+'], - }); - }); - - it('parses path for milestone with trailing +', () => { - expect( - queryData('milestone_title=A%2B', {}), - ).toEqual({ - milestone_title: 'A+', - }); - }); - - it('parses path for search terms with spaces', () => { - expect( - queryData('search=two+words', {}), - ).toEqual({ - search: 'two words', - }); - }); -}); diff --git a/spec/javascripts/diffs/mock_data/diff_discussions.js b/spec/javascripts/diffs/mock_data/diff_discussions.js index 41d0dfd8939..b29a22da7c2 100644 --- a/spec/javascripts/diffs/mock_data/diff_discussions.js +++ b/spec/javascripts/diffs/mock_data/diff_discussions.js @@ -16,7 +16,7 @@ export default { expanded: true, notes: [ { - id: 1749, + id: '1749', type: 'DiffNote', attachment: null, author: { @@ -68,7 +68,7 @@ export default { '/gitlab-org/gitlab-test/issues/new?discussion_to_resolve=6b232e05bea388c6b043ccc243ba505faac04ea8&merge_request_to_resolve_discussions_of=20', }, { - id: 1753, + id: '1753', type: 'DiffNote', attachment: null, author: { @@ -120,7 +120,7 @@ export default { '/gitlab-org/gitlab-test/issues/new?discussion_to_resolve=6b232e05bea388c6b043ccc243ba505faac04ea8&merge_request_to_resolve_discussions_of=20', }, { - id: 1754, + id: '1754', type: 'DiffNote', attachment: null, author: { @@ -162,7 +162,7 @@ export default { '/gitlab-org/gitlab-test/issues/new?discussion_to_resolve=6b232e05bea388c6b043ccc243ba505faac04ea8&merge_request_to_resolve_discussions_of=20', }, { - id: 1755, + id: '1755', type: 'DiffNote', attachment: null, author: { @@ -204,7 +204,7 @@ export default { '/gitlab-org/gitlab-test/issues/new?discussion_to_resolve=6b232e05bea388c6b043ccc243ba505faac04ea8&merge_request_to_resolve_discussions_of=20', }, { - id: 1756, + id: '1756', type: 'DiffNote', attachment: null, author: { diff --git a/spec/javascripts/ide/components/commit_sidebar/list_item_spec.js b/spec/javascripts/ide/components/commit_sidebar/list_item_spec.js index 41d8bfff7e7..b45ae5bbb0f 100644 --- a/spec/javascripts/ide/components/commit_sidebar/list_item_spec.js +++ b/spec/javascripts/ide/components/commit_sidebar/list_item_spec.js @@ -30,7 +30,7 @@ describe('Multi-file editor commit sidebar list item', () => { }); it('renders file path', () => { - expect(vm.$el.querySelector('.multi-file-commit-list-path').textContent.trim()).toBe(f.path); + expect(vm.$el.querySelector('.multi-file-commit-list-path').textContent).toContain(f.path); }); it('renders actionn button', () => { diff --git a/spec/javascripts/ide/components/commit_sidebar/stage_button_spec.js b/spec/javascripts/ide/components/commit_sidebar/stage_button_spec.js index a5b906da8a1..e09ccbe2a63 100644 --- a/spec/javascripts/ide/components/commit_sidebar/stage_button_spec.js +++ b/spec/javascripts/ide/components/commit_sidebar/stage_button_spec.js @@ -29,7 +29,7 @@ describe('IDE stage file button', () => { }); it('renders button to discard & stage', () => { - expect(vm.$el.querySelectorAll('.btn').length).toBe(2); + expect(vm.$el.querySelectorAll('.btn-blank').length).toBe(2); }); it('calls store with stage button', () => { @@ -39,7 +39,7 @@ describe('IDE stage file button', () => { }); it('calls store with discard button', () => { - vm.$el.querySelector('.dropdown-menu button').click(); + vm.$el.querySelector('.btn-danger').click(); expect(vm.discardFileChanges).toHaveBeenCalledWith(f.path); }); diff --git a/spec/javascripts/ide/components/file_templates/bar_spec.js b/spec/javascripts/ide/components/file_templates/bar_spec.js new file mode 100644 index 00000000000..a688f7f69a6 --- /dev/null +++ b/spec/javascripts/ide/components/file_templates/bar_spec.js @@ -0,0 +1,117 @@ +import Vue from 'vue'; +import { createStore } from '~/ide/stores'; +import Bar from '~/ide/components/file_templates/bar.vue'; +import { mountComponentWithStore } from 'spec/helpers/vue_mount_component_helper'; +import { resetStore, file } from '../../helpers'; + +describe('IDE file templates bar component', () => { + let Component; + let vm; + + beforeAll(() => { + Component = Vue.extend(Bar); + }); + + beforeEach(() => { + const store = createStore(); + + store.state.openFiles.push({ + ...file('file'), + opened: true, + active: true, + }); + + vm = mountComponentWithStore(Component, { store }); + }); + + afterEach(() => { + vm.$destroy(); + resetStore(vm.$store); + }); + + describe('template type dropdown', () => { + it('renders dropdown component', () => { + expect(vm.$el.querySelector('.dropdown').textContent).toContain('Choose a type'); + }); + + it('calls setSelectedTemplateType when clicking item', () => { + spyOn(vm, 'setSelectedTemplateType').and.stub(); + + vm.$el.querySelector('.dropdown-content button').click(); + + expect(vm.setSelectedTemplateType).toHaveBeenCalledWith({ + name: '.gitlab-ci.yml', + key: 'gitlab_ci_ymls', + }); + }); + }); + + describe('template dropdown', () => { + beforeEach(done => { + vm.$store.state.fileTemplates.templates = [ + { + name: 'test', + }, + ]; + vm.$store.state.fileTemplates.selectedTemplateType = { + name: '.gitlab-ci.yml', + key: 'gitlab_ci_ymls', + }; + + vm.$nextTick(done); + }); + + it('renders dropdown component', () => { + expect(vm.$el.querySelectorAll('.dropdown')[1].textContent).toContain('Choose a template'); + }); + + it('calls fetchTemplate on click', () => { + spyOn(vm, 'fetchTemplate').and.stub(); + + vm.$el + .querySelectorAll('.dropdown-content')[1] + .querySelector('button') + .click(); + + expect(vm.fetchTemplate).toHaveBeenCalledWith({ + name: 'test', + }); + }); + }); + + it('shows undo button if updateSuccess is true', done => { + vm.$store.state.fileTemplates.updateSuccess = true; + + vm.$nextTick(() => { + expect(vm.$el.querySelector('.btn-default').style.display).not.toBe('none'); + + done(); + }); + }); + + it('calls undoFileTemplate when clicking undo button', () => { + spyOn(vm, 'undoFileTemplate').and.stub(); + + vm.$el.querySelector('.btn-default').click(); + + expect(vm.undoFileTemplate).toHaveBeenCalled(); + }); + + it('calls setSelectedTemplateType if activeFile name matches a template', done => { + const fileName = '.gitlab-ci.yml'; + + spyOn(vm, 'setSelectedTemplateType'); + vm.$store.state.openFiles[0].name = fileName; + + vm.setInitialType(); + + vm.$nextTick(() => { + expect(vm.setSelectedTemplateType).toHaveBeenCalledWith({ + name: fileName, + key: 'gitlab_ci_ymls', + }); + + done(); + }); + }); +}); diff --git a/spec/javascripts/ide/components/file_templates/dropdown_spec.js b/spec/javascripts/ide/components/file_templates/dropdown_spec.js new file mode 100644 index 00000000000..898796f4fa0 --- /dev/null +++ b/spec/javascripts/ide/components/file_templates/dropdown_spec.js @@ -0,0 +1,201 @@ +import $ from 'jquery'; +import Vue from 'vue'; +import { createStore } from '~/ide/stores'; +import Dropdown from '~/ide/components/file_templates/dropdown.vue'; +import { createComponentWithStore } from 'spec/helpers/vue_mount_component_helper'; +import { resetStore } from '../../helpers'; + +describe('IDE file templates dropdown component', () => { + let Component; + let vm; + + beforeAll(() => { + Component = Vue.extend(Dropdown); + }); + + beforeEach(() => { + const store = createStore(); + + vm = createComponentWithStore(Component, store, { + label: 'Test', + }).$mount(); + }); + + afterEach(() => { + vm.$destroy(); + resetStore(vm.$store); + }); + + describe('async', () => { + beforeEach(() => { + vm.isAsyncData = true; + }); + + it('calls async store method on Bootstrap dropdown event', () => { + spyOn(vm, 'fetchTemplateTypes').and.stub(); + + $(vm.$el).trigger('show.bs.dropdown'); + + expect(vm.fetchTemplateTypes).toHaveBeenCalled(); + }); + + it('renders templates when async', done => { + vm.$store.state.fileTemplates.templates = [ + { + name: 'test', + }, + ]; + + vm.$nextTick(() => { + expect(vm.$el.querySelector('.dropdown-content').textContent).toContain('test'); + + done(); + }); + }); + + it('renders loading icon when isLoading is true', done => { + vm.$store.state.fileTemplates.isLoading = true; + + vm.$nextTick(() => { + expect(vm.$el.querySelector('.loading-container')).not.toBe(null); + + done(); + }); + }); + + it('searches template data', () => { + vm.$store.state.fileTemplates.templates = [ + { + name: 'test', + }, + ]; + vm.searchable = true; + vm.search = 'hello'; + + expect(vm.outputData).toEqual([]); + }); + + it('does not filter data is searchable is false', () => { + vm.$store.state.fileTemplates.templates = [ + { + name: 'test', + }, + ]; + vm.search = 'hello'; + + expect(vm.outputData).toEqual([ + { + name: 'test', + }, + ]); + }); + + it('calls clickItem on click', done => { + spyOn(vm, 'clickItem').and.stub(); + + vm.$store.state.fileTemplates.templates = [ + { + name: 'test', + }, + ]; + + vm.$nextTick(() => { + vm.$el.querySelector('.dropdown-content button').click(); + + expect(vm.clickItem).toHaveBeenCalledWith({ + name: 'test', + }); + + done(); + }); + }); + + it('renders input when searchable is true', done => { + vm.searchable = true; + + vm.$nextTick(() => { + expect(vm.$el.querySelector('.dropdown-input')).not.toBe(null); + + done(); + }); + }); + + it('does not render input when searchable is true & showLoading is true', done => { + vm.searchable = true; + vm.$store.state.fileTemplates.isLoading = true; + + vm.$nextTick(() => { + expect(vm.$el.querySelector('.dropdown-input')).toBe(null); + + done(); + }); + }); + }); + + describe('sync', () => { + beforeEach(done => { + vm.data = [ + { + name: 'test sync', + }, + ]; + + vm.$nextTick(done); + }); + + it('renders props data', () => { + expect(vm.$el.querySelector('.dropdown-content').textContent).toContain('test sync'); + }); + + it('renders input when searchable is true', done => { + vm.searchable = true; + + vm.$nextTick(() => { + expect(vm.$el.querySelector('.dropdown-input')).not.toBe(null); + + done(); + }); + }); + + it('calls clickItem on click', done => { + spyOn(vm, 'clickItem').and.stub(); + + vm.$nextTick(() => { + vm.$el.querySelector('.dropdown-content button').click(); + + expect(vm.clickItem).toHaveBeenCalledWith({ + name: 'test sync', + }); + + done(); + }); + }); + + it('searches template data', () => { + vm.searchable = true; + vm.search = 'hello'; + + expect(vm.outputData).toEqual([]); + }); + + it('does not filter data is searchable is false', () => { + vm.search = 'hello'; + + expect(vm.outputData).toEqual([ + { + name: 'test sync', + }, + ]); + }); + + it('renders dropdown title', done => { + vm.title = 'Test title'; + + vm.$nextTick(() => { + expect(vm.$el.querySelector('.dropdown-title').textContent).toContain('Test title'); + + done(); + }); + }); + }); +}); diff --git a/spec/javascripts/ide/components/repo_commit_section_spec.js b/spec/javascripts/ide/components/repo_commit_section_spec.js index 30cd92b2ca4..d09ccd7ac34 100644 --- a/spec/javascripts/ide/components/repo_commit_section_spec.js +++ b/spec/javascripts/ide/components/repo_commit_section_spec.js @@ -111,7 +111,7 @@ describe('RepoCommitSection', () => { .then(vm.$nextTick) .then(() => { expect(vm.$el.querySelector('.ide-commit-list-container').textContent).toContain( - 'No changes', + 'There are no unstaged changes', ); }) .then(done) @@ -133,7 +133,7 @@ describe('RepoCommitSection', () => { }); it('discards a single file', done => { - vm.$el.querySelector('.multi-file-discard-btn .dropdown-menu button').click(); + vm.$el.querySelector('.multi-file-commit-list li:first-child .js-modal-primary-action').click(); Vue.nextTick(() => { expect(vm.$el.querySelector('.ide-commit-list-container').textContent).not.toContain('file1'); diff --git a/spec/javascripts/ide/helpers.js b/spec/javascripts/ide/helpers.js index c11c482fef8..3ce9c9fcda1 100644 --- a/spec/javascripts/ide/helpers.js +++ b/spec/javascripts/ide/helpers.js @@ -5,6 +5,7 @@ import commitState from '~/ide/stores/modules/commit/state'; import mergeRequestsState from '~/ide/stores/modules/merge_requests/state'; import pipelinesState from '~/ide/stores/modules/pipelines/state'; import branchesState from '~/ide/stores/modules/branches/state'; +import fileTemplatesState from '~/ide/stores/modules/file_templates/state'; export const resetStore = store => { const newState = { @@ -13,6 +14,7 @@ export const resetStore = store => { mergeRequests: mergeRequestsState(), pipelines: pipelinesState(), branches: branchesState(), + fileTemplates: fileTemplatesState(), }; store.replaceState(newState); }; diff --git a/spec/javascripts/ide/stores/actions/file_spec.js b/spec/javascripts/ide/stores/actions/file_spec.js index bca2033ff97..1ca811e996b 100644 --- a/spec/javascripts/ide/stores/actions/file_spec.js +++ b/spec/javascripts/ide/stores/actions/file_spec.js @@ -692,21 +692,6 @@ describe('IDE store file actions', () => { .then(done) .catch(done.fail); }); - - it('calls scrollToTab', done => { - const scrollToTabSpy = jasmine.createSpy('scrollToTab'); - const oldScrollToTab = store._actions.scrollToTab; // eslint-disable-line - store._actions.scrollToTab = [scrollToTabSpy]; // eslint-disable-line - - store - .dispatch('openPendingTab', { file: f, keyPrefix: 'pending' }) - .then(() => { - expect(scrollToTabSpy).toHaveBeenCalled(); - store._actions.scrollToTab = oldScrollToTab; // eslint-disable-line - }) - .then(done) - .catch(done.fail); - }); }); describe('removePendingTab', () => { diff --git a/spec/javascripts/ide/stores/actions_spec.js b/spec/javascripts/ide/stores/actions_spec.js index d84f1717a61..c9a1158a14e 100644 --- a/spec/javascripts/ide/stores/actions_spec.js +++ b/spec/javascripts/ide/stores/actions_spec.js @@ -305,7 +305,11 @@ describe('Multi-file store actions', () => { describe('stageAllChanges', () => { it('adds all files from changedFiles to stagedFiles', done => { - store.state.changedFiles.push(file(), file('new')); + const openFile = { ...file(), path: 'test' }; + + store.state.openFiles.push(openFile); + store.state.stagedFiles.push(openFile); + store.state.changedFiles.push(openFile, file('new')); testAction( stageAllChanges, @@ -316,7 +320,12 @@ describe('Multi-file store actions', () => { { type: types.STAGE_CHANGE, payload: store.state.changedFiles[0].path }, { type: types.STAGE_CHANGE, payload: store.state.changedFiles[1].path }, ], - [], + [ + { + type: 'openPendingTab', + payload: { file: openFile, keyPrefix: 'staged' }, + }, + ], done, ); }); @@ -324,7 +333,11 @@ describe('Multi-file store actions', () => { describe('unstageAllChanges', () => { it('removes all files from stagedFiles after unstaging', done => { - store.state.stagedFiles.push(file(), file('new')); + const openFile = { ...file(), path: 'test' }; + + store.state.openFiles.push(openFile); + store.state.changedFiles.push(openFile); + store.state.stagedFiles.push(openFile, file('new')); testAction( unstageAllChanges, @@ -334,7 +347,12 @@ describe('Multi-file store actions', () => { { type: types.UNSTAGE_CHANGE, payload: store.state.stagedFiles[0].path }, { type: types.UNSTAGE_CHANGE, payload: store.state.stagedFiles[1].path }, ], - [], + [ + { + type: 'openPendingTab', + payload: { file: openFile, keyPrefix: 'unstaged' }, + }, + ], done, ); }); diff --git a/spec/javascripts/ide/stores/modules/file_templates/actions_spec.js b/spec/javascripts/ide/stores/modules/file_templates/actions_spec.js index f831a9f0a5d..c29dd9f0d06 100644 --- a/spec/javascripts/ide/stores/modules/file_templates/actions_spec.js +++ b/spec/javascripts/ide/stores/modules/file_templates/actions_spec.js @@ -148,14 +148,66 @@ describe('IDE file templates actions', () => { }); describe('setSelectedTemplateType', () => { - it('commits SET_SELECTED_TEMPLATE_TYPE', done => { - testAction( - actions.setSelectedTemplateType, - 'test', - state, - [{ type: types.SET_SELECTED_TEMPLATE_TYPE, payload: 'test' }], - [], - done, + it('commits SET_SELECTED_TEMPLATE_TYPE', () => { + const commit = jasmine.createSpy('commit'); + const options = { + commit, + dispatch() {}, + rootGetters: { + activeFile: { + name: 'test', + prevPath: '', + }, + }, + }; + + actions.setSelectedTemplateType(options, { name: 'test' }); + + expect(commit).toHaveBeenCalledWith(types.SET_SELECTED_TEMPLATE_TYPE, { name: 'test' }); + }); + + it('dispatches discardFileChanges if prevPath matches templates name', () => { + const dispatch = jasmine.createSpy('dispatch'); + const options = { + commit() {}, + dispatch, + rootGetters: { + activeFile: { + name: 'test', + path: 'test', + prevPath: 'test', + }, + }, + }; + + actions.setSelectedTemplateType(options, { name: 'test' }); + + expect(dispatch).toHaveBeenCalledWith('discardFileChanges', 'test', { root: true }); + }); + + it('dispatches renameEntry if file name doesnt match', () => { + const dispatch = jasmine.createSpy('dispatch'); + const options = { + commit() {}, + dispatch, + rootGetters: { + activeFile: { + name: 'oldtest', + path: 'oldtest', + prevPath: '', + }, + }, + }; + + actions.setSelectedTemplateType(options, { name: 'test' }); + + expect(dispatch).toHaveBeenCalledWith( + 'renameEntry', + { + path: 'oldtest', + name: 'test', + }, + { root: true }, ); }); }); @@ -332,5 +384,20 @@ describe('IDE file templates actions', () => { expect(commit).toHaveBeenCalledWith('SET_UPDATE_SUCCESS', false); }); + + it('dispatches discardFileChanges if file has prevPath', () => { + const dispatch = jasmine.createSpy('dispatch'); + const rootGetters = { + activeFile: { path: 'test', prevPath: 'newtest', raw: 'raw content' }, + }; + + actions.undoFileTemplate({ dispatch, commit() {}, rootGetters }); + + expect(dispatch.calls.mostRecent().args).toEqual([ + 'discardFileChanges', + 'test', + { root: true }, + ]); + }); }); }); diff --git a/spec/javascripts/ide/stores/modules/file_templates/getters_spec.js b/spec/javascripts/ide/stores/modules/file_templates/getters_spec.js index e337c3f331b..17cb457881f 100644 --- a/spec/javascripts/ide/stores/modules/file_templates/getters_spec.js +++ b/spec/javascripts/ide/stores/modules/file_templates/getters_spec.js @@ -1,3 +1,5 @@ +import createState from '~/ide/stores/state'; +import { activityBarViews } from '~/ide/constants'; import * as getters from '~/ide/stores/modules/file_templates/getters'; describe('IDE file templates getters', () => { @@ -8,22 +10,49 @@ describe('IDE file templates getters', () => { }); describe('showFileTemplatesBar', () => { - it('finds template type by name', () => { + let rootState; + + beforeEach(() => { + rootState = createState(); + }); + + it('returns true if template is found and currentActivityView is edit', () => { + rootState.currentActivityView = activityBarViews.edit; + + expect( + getters.showFileTemplatesBar( + null, + { + templateTypes: getters.templateTypes(), + }, + rootState, + )('LICENSE'), + ).toBe(true); + }); + + it('returns false if template is found and currentActivityView is not edit', () => { + rootState.currentActivityView = activityBarViews.commit; + expect( - getters.showFileTemplatesBar(null, { - templateTypes: getters.templateTypes(), - })('LICENSE'), - ).toEqual({ - name: 'LICENSE', - key: 'licenses', - }); + getters.showFileTemplatesBar( + null, + { + templateTypes: getters.templateTypes(), + }, + rootState, + )('LICENSE'), + ).toBe(false); }); it('returns undefined if not found', () => { expect( - getters.showFileTemplatesBar(null, { - templateTypes: getters.templateTypes(), - })('test'), + getters.showFileTemplatesBar( + null, + { + templateTypes: getters.templateTypes(), + }, + rootState, + )('test'), ).toBe(undefined); }); }); diff --git a/spec/javascripts/ide/stores/mutations_spec.js b/spec/javascripts/ide/stores/mutations_spec.js index 6ce76aaa03b..41dd3d3c67f 100644 --- a/spec/javascripts/ide/stores/mutations_spec.js +++ b/spec/javascripts/ide/stores/mutations_spec.js @@ -339,5 +339,13 @@ describe('Multi-file store mutations', () => { expect(localState.entries.parentPath.tree.length).toBe(1); }); + + it('adds to openFiles if previously opened', () => { + localState.entries.oldPath.opened = true; + + mutations.RENAME_ENTRY(localState, { path: 'oldPath', name: 'newPath' }); + + expect(localState.openFiles).toEqual([localState.entries.newPath]); + }); }); }); diff --git a/spec/javascripts/lib/utils/common_utils_spec.js b/spec/javascripts/lib/utils/common_utils_spec.js index babad296f09..28151c7e658 100644 --- a/spec/javascripts/lib/utils/common_utils_spec.js +++ b/spec/javascripts/lib/utils/common_utils_spec.js @@ -29,24 +29,46 @@ describe('common_utils', () => { }); }); - describe('getUrlParamsArray', () => { - it('should return params array', () => { - expect(commonUtils.getUrlParamsArray() instanceof Array).toBe(true); + describe('urlParamsToArray', () => { + it('returns empty array for empty querystring', () => { + expect(commonUtils.urlParamsToArray('')).toEqual([]); + }); + + it('should decode params', () => { + expect( + commonUtils.urlParamsToArray('?label_name%5B%5D=test')[0], + ).toBe('label_name[]=test'); }); it('should remove the question mark from the search params', () => { - const paramsArray = commonUtils.getUrlParamsArray(); + const paramsArray = commonUtils.urlParamsToArray('?test=thing'); expect(paramsArray[0][0] !== '?').toBe(true); }); + }); - it('should decode params', () => { - window.history.pushState('', '', '?label_name%5B%5D=test'); + describe('urlParamsToObject', () => { + it('parses path for label with trailing +', () => { + expect( + commonUtils.urlParamsToObject('label_name[]=label%2B', {}), + ).toEqual({ + label_name: ['label+'], + }); + }); + it('parses path for milestone with trailing +', () => { expect( - commonUtils.getUrlParamsArray()[0], - ).toBe('label_name[]=test'); + commonUtils.urlParamsToObject('milestone_title=A%2B', {}), + ).toEqual({ + milestone_title: 'A+', + }); + }); - window.history.pushState('', '', '?'); + it('parses path for search terms with spaces', () => { + expect( + commonUtils.urlParamsToObject('search=two+words', {}), + ).toEqual({ + search: 'two words', + }); }); }); diff --git a/spec/javascripts/notes/components/note_actions_spec.js b/spec/javascripts/notes/components/note_actions_spec.js index 52cc42cb53d..d7298cb3483 100644 --- a/spec/javascripts/notes/components/note_actions_spec.js +++ b/spec/javascripts/notes/components/note_actions_spec.js @@ -28,7 +28,7 @@ describe('issue_note_actions component', () => { canEdit: true, canAwardEmoji: true, canReportAsAbuse: true, - noteId: 539, + noteId: '539', noteUrl: 'https://localhost:3000/group/project/merge_requests/1#note_1', reportAbusePath: '/abuse_reports/new?ref_url=http%3A%2F%2Flocalhost%3A3000%2Fgitlab-org%2Fgitlab-ce%2Fissues%2F7%23note_539&user_id=26', @@ -59,6 +59,20 @@ describe('issue_note_actions component', () => { expect(vm.$el.querySelector(`a[href="${props.reportAbusePath}"]`)).toBeDefined(); }); + it('should be possible to copy link to a note', () => { + expect(vm.$el.querySelector('.js-btn-copy-note-link')).not.toBeNull(); + }); + + it('should not show copy link action when `noteUrl` prop is empty', done => { + vm.noteUrl = ''; + Vue.nextTick() + .then(() => { + expect(vm.$el.querySelector('.js-btn-copy-note-link')).toBeNull(); + }) + .then(done) + .catch(done.fail); + }); + it('should be possible to delete comment', () => { expect(vm.$el.querySelector('.js-note-delete')).toBeDefined(); }); @@ -77,7 +91,7 @@ describe('issue_note_actions component', () => { canEdit: false, canAwardEmoji: false, canReportAsAbuse: false, - noteId: 539, + noteId: '539', noteUrl: 'https://localhost:3000/group/project/merge_requests/1#note_1', reportAbusePath: '/abuse_reports/new?ref_url=http%3A%2F%2Flocalhost%3A3000%2Fgitlab-org%2Fgitlab-ce%2Fissues%2F7%23note_539&user_id=26', diff --git a/spec/javascripts/notes/components/note_awards_list_spec.js b/spec/javascripts/notes/components/note_awards_list_spec.js index 9d98ba219da..6a6a810acff 100644 --- a/spec/javascripts/notes/components/note_awards_list_spec.js +++ b/spec/javascripts/notes/components/note_awards_list_spec.js @@ -30,7 +30,7 @@ describe('note_awards_list component', () => { propsData: { awards: awardsMock, noteAuthorId: 2, - noteId: 545, + noteId: '545', canAwardEmoji: true, toggleAwardPath: '/gitlab-org/gitlab-ce/notes/545/toggle_award_emoji', }, @@ -70,7 +70,7 @@ describe('note_awards_list component', () => { propsData: { awards: awardsMock, noteAuthorId: 2, - noteId: 545, + noteId: '545', canAwardEmoji: false, toggleAwardPath: '/gitlab-org/gitlab-ce/notes/545/toggle_award_emoji', }, diff --git a/spec/javascripts/notes/components/note_form_spec.js b/spec/javascripts/notes/components/note_form_spec.js index 95d400ab3df..147ffcf1b81 100644 --- a/spec/javascripts/notes/components/note_form_spec.js +++ b/spec/javascripts/notes/components/note_form_spec.js @@ -19,7 +19,7 @@ describe('issue_note_form component', () => { props = { isEditing: false, noteBody: 'Magni suscipit eius consectetur enim et ex et commodi.', - noteId: 545, + noteId: '545', }; vm = new Component({ @@ -32,6 +32,22 @@ describe('issue_note_form component', () => { vm.$destroy(); }); + describe('noteHash', () => { + it('returns note hash string based on `noteId`', () => { + expect(vm.noteHash).toBe(`#note_${props.noteId}`); + }); + + it('return note hash as `#` when `noteId` is empty', done => { + vm.noteId = ''; + Vue.nextTick() + .then(() => { + expect(vm.noteHash).toBe('#'); + }) + .then(done) + .catch(done.fail); + }); + }); + describe('conflicts editing', () => { it('should show conflict message if note changes outside the component', done => { vm.isEditing = true; diff --git a/spec/javascripts/notes/components/note_header_spec.js b/spec/javascripts/notes/components/note_header_spec.js index a3c6bf78988..379780f43a0 100644 --- a/spec/javascripts/notes/components/note_header_spec.js +++ b/spec/javascripts/notes/components/note_header_spec.js @@ -33,7 +33,7 @@ describe('note_header component', () => { }, createdAt: '2017-08-02T10:51:58.559Z', includeToggle: false, - noteId: 1394, + noteId: '1394', expanded: true, }, }).$mount(); @@ -47,6 +47,16 @@ describe('note_header component', () => { it('should render timestamp link', () => { expect(vm.$el.querySelector('a[href="#note_1394"]')).toBeDefined(); }); + + it('should not render user information when prop `author` is empty object', done => { + vm.author = {}; + Vue.nextTick() + .then(() => { + expect(vm.$el.querySelector('.note-header-author-name')).toBeNull(); + }) + .then(done) + .catch(done.fail); + }); }); describe('discussion', () => { @@ -66,7 +76,7 @@ describe('note_header component', () => { }, createdAt: '2017-08-02T10:51:58.559Z', includeToggle: true, - noteId: 1395, + noteId: '1395', expanded: true, }, }).$mount(); diff --git a/spec/javascripts/notes/mock_data.js b/spec/javascripts/notes/mock_data.js index 0423fcb6ec4..1f030e5af28 100644 --- a/spec/javascripts/notes/mock_data.js +++ b/spec/javascripts/notes/mock_data.js @@ -66,7 +66,7 @@ export const individualNote = { individual_note: true, notes: [ { - id: 1390, + id: '1390', attachment: { url: null, filename: null, @@ -111,7 +111,7 @@ export const individualNote = { }; export const note = { - id: 546, + id: '546', attachment: { url: null, filename: null, @@ -174,7 +174,7 @@ export const discussionMock = { expanded: true, notes: [ { - id: 1395, + id: '1395', attachment: { url: null, filename: null, @@ -211,7 +211,7 @@ export const discussionMock = { path: '/gitlab-org/gitlab-ce/notes/1395', }, { - id: 1396, + id: '1396', attachment: { url: null, filename: null, @@ -257,7 +257,7 @@ export const discussionMock = { path: '/gitlab-org/gitlab-ce/notes/1396', }, { - id: 1437, + id: '1437', attachment: { url: null, filename: null, @@ -308,7 +308,7 @@ export const discussionMock = { }; export const loggedOutnoteableData = { - id: 98, + id: '98', iid: 26, author_id: 1, description: '', @@ -358,7 +358,7 @@ export const collapseNotesMock = [ individual_note: true, notes: [ { - id: 1390, + id: '1390', attachment: null, author: { id: 1, @@ -393,7 +393,7 @@ export const collapseNotesMock = [ individual_note: true, notes: [ { - id: 1391, + id: '1391', attachment: null, author: { id: 1, @@ -433,7 +433,7 @@ export const INDIVIDUAL_NOTE_RESPONSE_MAP = { expanded: true, notes: [ { - id: 1390, + id: '1390', attachment: { url: null, filename: null, @@ -495,7 +495,7 @@ export const INDIVIDUAL_NOTE_RESPONSE_MAP = { expanded: true, notes: [ { - id: 1391, + id: '1391', attachment: { url: null, filename: null, @@ -544,7 +544,7 @@ export const INDIVIDUAL_NOTE_RESPONSE_MAP = { '/gitlab-org/gitlab-ce/notes/1471': { commands_changes: null, valid: true, - id: 1471, + id: '1471', attachment: null, author: { id: 1, @@ -600,7 +600,7 @@ export const DISCUSSION_NOTE_RESPONSE_MAP = { expanded: true, notes: [ { - id: 1471, + id: '1471', attachment: { url: null, filename: null, @@ -671,7 +671,7 @@ export const notesWithDescriptionChanges = [ expanded: true, notes: [ { - id: 901, + id: '901', type: null, attachment: null, author: { @@ -718,7 +718,7 @@ export const notesWithDescriptionChanges = [ expanded: true, notes: [ { - id: 902, + id: '902', type: null, attachment: null, author: { @@ -765,7 +765,7 @@ export const notesWithDescriptionChanges = [ expanded: true, notes: [ { - id: 903, + id: '903', type: null, attachment: null, author: { @@ -809,7 +809,7 @@ export const notesWithDescriptionChanges = [ expanded: true, notes: [ { - id: 904, + id: '904', type: null, attachment: null, author: { @@ -854,7 +854,7 @@ export const notesWithDescriptionChanges = [ expanded: true, notes: [ { - id: 905, + id: '905', type: null, attachment: null, author: { @@ -898,7 +898,7 @@ export const notesWithDescriptionChanges = [ expanded: true, notes: [ { - id: 906, + id: '906', type: null, attachment: null, author: { @@ -945,7 +945,7 @@ export const collapsedSystemNotes = [ expanded: true, notes: [ { - id: 901, + id: '901', type: null, attachment: null, author: { @@ -992,7 +992,7 @@ export const collapsedSystemNotes = [ expanded: true, notes: [ { - id: 902, + id: '902', type: null, attachment: null, author: { @@ -1039,7 +1039,7 @@ export const collapsedSystemNotes = [ expanded: true, notes: [ { - id: 904, + id: '904', type: null, attachment: null, author: { @@ -1084,7 +1084,7 @@ export const collapsedSystemNotes = [ expanded: true, notes: [ { - id: 905, + id: '905', type: null, attachment: null, author: { @@ -1129,7 +1129,7 @@ export const collapsedSystemNotes = [ expanded: true, notes: [ { - id: 906, + id: '906', type: null, attachment: null, author: { diff --git a/spec/javascripts/vue_shared/components/notes/system_note_spec.js b/spec/javascripts/vue_shared/components/notes/system_note_spec.js index 2a6015fe35f..adcb1c858aa 100644 --- a/spec/javascripts/vue_shared/components/notes/system_note_spec.js +++ b/spec/javascripts/vue_shared/components/notes/system_note_spec.js @@ -9,7 +9,7 @@ describe('system note component', () => { beforeEach(() => { props = { note: { - id: 1424, + id: '1424', author: { id: 1, name: 'Root', diff --git a/spec/lib/api/helpers/pagination_spec.rb b/spec/lib/api/helpers/pagination_spec.rb index c73c6023b60..0a7682d906b 100644 --- a/spec/lib/api/helpers/pagination_spec.rb +++ b/spec/lib/api/helpers/pagination_spec.rb @@ -189,9 +189,9 @@ describe API::Helpers::Pagination do it 'it returns the right link to the next page' do allow(subject).to receive(:params) .and_return({ pagination: 'keyset', ks_prev_id: projects[3].id, ks_prev_name: projects[3].name, per_page: 2 }) + expect_header('X-Per-Page', '2') expect_header('X-Next-Page', "#{value}?ks_prev_id=#{projects[6].id}&ks_prev_name=#{projects[6].name}&pagination=keyset&per_page=2") - expect_header('Link', anything) do |_key, val| expect(val).to include('rel="next"') end diff --git a/spec/lib/banzai/filter/spaced_link_filter_spec.rb b/spec/lib/banzai/filter/spaced_link_filter_spec.rb index 4463c011522..1ad7f3ff567 100644 --- a/spec/lib/banzai/filter/spaced_link_filter_spec.rb +++ b/spec/lib/banzai/filter/spaced_link_filter_spec.rb @@ -3,49 +3,73 @@ require 'spec_helper' describe Banzai::Filter::SpacedLinkFilter do include FilterSpecHelper - let(:link) { '[example](page slug)' } + let(:link) { '[example](page slug)' } + let(:image) { '![example](img test.jpg)' } - it 'converts slug with spaces to a link' do - doc = filter("See #{link}") + context 'when a link is detected' do + it 'converts slug with spaces to a link' do + doc = filter("See #{link}") - expect(doc.at_css('a').text).to eq 'example' - expect(doc.at_css('a')['href']).to eq 'page%20slug' - expect(doc.at_css('p')).to eq nil - end + expect(doc.at_css('a').text).to eq 'example' + expect(doc.at_css('a')['href']).to eq 'page%20slug' + expect(doc.at_css('a')['title']).to be_nil + expect(doc.at_css('p')).to be_nil + end - it 'converts slug with spaces and a title to a link' do - link = '[example](page slug "title")' - doc = filter("See #{link}") + it 'converts slug with spaces and a title to a link' do + link = '[example](page slug "title")' + doc = filter("See #{link}") - expect(doc.at_css('a').text).to eq 'example' - expect(doc.at_css('a')['href']).to eq 'page%20slug' - expect(doc.at_css('a')['title']).to eq 'title' - expect(doc.at_css('p')).to eq nil - end + expect(doc.at_css('a').text).to eq 'example' + expect(doc.at_css('a')['href']).to eq 'page%20slug' + expect(doc.at_css('a')['title']).to eq 'title' + expect(doc.at_css('p')).to be_nil + end - it 'does nothing when markdown_engine is redcarpet' do - exp = act = link - expect(filter(act, markdown_engine: :redcarpet).to_html).to eq exp - end + it 'does nothing when markdown_engine is redcarpet' do + exp = act = link + expect(filter(act, markdown_engine: :redcarpet).to_html).to eq exp + end + + it 'does nothing with empty text' do + link = '[](page slug)' + doc = filter("See #{link}") + + expect(doc.at_css('a')).to be_nil + end - it 'does nothing with empty text' do - link = '[](page slug)' - doc = filter("See #{link}") + it 'does nothing with an empty slug' do + link = '[example]()' + doc = filter("See #{link}") - expect(doc.at_css('a')).to eq nil + expect(doc.at_css('a')).to be_nil + end end - it 'does nothing with an empty slug' do - link = '[example]()' - doc = filter("See #{link}") + context 'when an image is detected' do + it 'converts slug with spaces to an iamge' do + doc = filter("See #{image}") + + expect(doc.at_css('img')['src']).to eq 'img%20test.jpg' + expect(doc.at_css('img')['alt']).to eq 'example' + expect(doc.at_css('p')).to be_nil + end + + it 'converts slug with spaces and a title to an image' do + image = '![example](img test.jpg "title")' + doc = filter("See #{image}") - expect(doc.at_css('a')).to eq nil + expect(doc.at_css('img')['src']).to eq 'img%20test.jpg' + expect(doc.at_css('img')['alt']).to eq 'example' + expect(doc.at_css('img')['title']).to eq 'title' + expect(doc.at_css('p')).to be_nil + end end it 'converts multiple URLs' do link1 = '[first](slug one)' link2 = '[second](http://example.com/slug two)' - doc = filter("See #{link1} and #{link2}") + doc = filter("See #{link1} and #{image} and #{link2}") found_links = doc.css('a') @@ -54,6 +78,12 @@ describe Banzai::Filter::SpacedLinkFilter do expect(found_links[0]['href']).to eq 'slug%20one' expect(found_links[1].text).to eq 'second' expect(found_links[1]['href']).to eq 'http://example.com/slug%20two' + + found_images = doc.css('img') + + expect(found_images.size).to eq(1) + expect(found_images[0]['src']).to eq 'img%20test.jpg' + expect(found_images[0]['alt']).to eq 'example' end described_class::IGNORE_PARENTS.each do |elem| diff --git a/spec/lib/banzai/pipeline/wiki_pipeline_spec.rb b/spec/lib/banzai/pipeline/wiki_pipeline_spec.rb index 52b8c9be647..64ca3ec345d 100644 --- a/spec/lib/banzai/pipeline/wiki_pipeline_spec.rb +++ b/spec/lib/banzai/pipeline/wiki_pipeline_spec.rb @@ -178,4 +178,25 @@ describe Banzai::Pipeline::WikiPipeline do end end end + + describe 'videos' do + let(:namespace) { create(:namespace, name: "wiki_link_ns") } + let(:project) { create(:project, :public, name: "wiki_link_project", namespace: namespace) } + let(:project_wiki) { ProjectWiki.new(project, double(:user)) } + let(:page) { build(:wiki_page, wiki: project_wiki, page: OpenStruct.new(url_path: 'nested/twice/start-page')) } + + it 'generates video html structure' do + markdown = "![video_file](video_file_name.mp4)" + output = described_class.to_html(markdown, project: project, project_wiki: project_wiki, page_slug: page.slug) + + expect(output).to include('<video src="/wiki_link_ns/wiki_link_project/wikis/nested/twice/video_file_name.mp4"') + end + + it 'rewrites and replaces video links names with white spaces to %20' do + markdown = "![video file](video file name.mp4)" + output = described_class.to_html(markdown, project: project, project_wiki: project_wiki, page_slug: page.slug) + + expect(output).to include('<video src="/wiki_link_ns/wiki_link_project/wikis/nested/twice/video%20file%20name.mp4"') + end + end end diff --git a/spec/lib/forever_spec.rb b/spec/lib/forever_spec.rb index cf40c467c72..494c0561975 100644 --- a/spec/lib/forever_spec.rb +++ b/spec/lib/forever_spec.rb @@ -7,6 +7,7 @@ describe Forever do context 'when using PostgreSQL' do it 'should return Postgresql future date' do allow(Gitlab::Database).to receive(:postgresql?).and_return(true) + expect(subject).to eq(described_class::POSTGRESQL_DATE) end end @@ -14,6 +15,7 @@ describe Forever do context 'when using MySQL' do it 'should return MySQL future date' do allow(Gitlab::Database).to receive(:postgresql?).and_return(false) + expect(subject).to eq(described_class::MYSQL_DATE) end end diff --git a/spec/lib/gitlab/cleanup/project_uploads_spec.rb b/spec/lib/gitlab/cleanup/project_uploads_spec.rb index 11e605eece6..bf130b8fabd 100644 --- a/spec/lib/gitlab/cleanup/project_uploads_spec.rb +++ b/spec/lib/gitlab/cleanup/project_uploads_spec.rb @@ -132,7 +132,6 @@ describe Gitlab::Cleanup::ProjectUploads do let!(:path) { File.join(FileUploader.root, orphaned.model.full_path, orphaned.path) } before do - stub_feature_flags(import_export_object_storage: true) stub_uploads_object_storage(FileUploader) FileUtils.mkdir_p(File.dirname(path)) @@ -156,7 +155,6 @@ describe Gitlab::Cleanup::ProjectUploads do let!(:new_path) { File.join(FileUploader.root, '-', 'project-lost-found', 'wrong', orphaned.path) } before do - stub_feature_flags(import_export_object_storage: true) stub_uploads_object_storage(FileUploader) FileUtils.mkdir_p(File.dirname(path)) diff --git a/spec/lib/gitlab/contributions_calendar_spec.rb b/spec/lib/gitlab/contributions_calendar_spec.rb index 2c63f3b0455..6d29044ffd5 100644 --- a/spec/lib/gitlab/contributions_calendar_spec.rb +++ b/spec/lib/gitlab/contributions_calendar_spec.rb @@ -62,13 +62,16 @@ describe Gitlab::ContributionsCalendar do expect(calendar.activity_dates).to eq(last_week => 2, today => 1) end - it "only shows private events to authorized users" do - create_event(private_project, today) - create_event(feature_project, today) + context "when the user has opted-in for private contributions" do + it "shows private and public events to all users" do + user.update_column(:include_private_contributions, true) + create_event(private_project, today) + create_event(public_project, today) - expect(calendar.activity_dates[today]).to eq(0) - expect(calendar(user).activity_dates[today]).to eq(0) - expect(calendar(contributor).activity_dates[today]).to eq(2) + expect(calendar.activity_dates[today]).to eq(1) + expect(calendar(user).activity_dates[today]).to eq(1) + expect(calendar(contributor).activity_dates[today]).to eq(2) + end end it "counts the diff notes on merge request" do @@ -128,7 +131,7 @@ describe Gitlab::ContributionsCalendar do e3 = create_event(feature_project, today) create_event(public_project, last_week) - expect(calendar.events_by_date(today)).to contain_exactly(e1) + expect(calendar.events_by_date(today)).to contain_exactly(e1, e3) expect(calendar(contributor).events_by_date(today)).to contain_exactly(e1, e2, e3) end diff --git a/spec/lib/gitlab/diff/highlight_cache_spec.rb b/spec/lib/gitlab/diff/highlight_cache_spec.rb new file mode 100644 index 00000000000..bfcfed4231f --- /dev/null +++ b/spec/lib/gitlab/diff/highlight_cache_spec.rb @@ -0,0 +1,70 @@ +# frozen_string_literal: true + +require 'spec_helper' + +describe Gitlab::Diff::HighlightCache do + let(:merge_request) { create(:merge_request_with_diffs) } + + subject(:cache) { described_class.new(merge_request.diffs, backend: backend) } + + describe '#decorate' do + let(:backend) { double('backend').as_null_object } + + # Manually creates a Diff::File object to avoid triggering the cache on + # the FileCollection::MergeRequestDiff + let(:diff_file) do + diffs = merge_request.diffs + raw_diff = diffs.diffable.raw_diffs(diffs.diff_options.merge(paths: ['CHANGELOG'])).first + Gitlab::Diff::File.new(raw_diff, + repository: diffs.project.repository, + diff_refs: diffs.diff_refs, + fallback_diff_refs: diffs.fallback_diff_refs) + end + + it 'does not calculate highlighting when reading from cache' do + cache.write_if_empty + cache.decorate(diff_file) + + expect_any_instance_of(Gitlab::Diff::Highlight).not_to receive(:highlight) + + diff_file.highlighted_diff_lines + end + + it 'assigns highlighted diff lines to the DiffFile' do + cache.write_if_empty + cache.decorate(diff_file) + + expect(diff_file.highlighted_diff_lines.size).to be > 5 + end + + it 'submits a single reading from the cache' do + cache.decorate(diff_file) + cache.decorate(diff_file) + + expect(backend).to have_received(:read).with(cache.key).once + end + end + + describe '#write_if_empty' do + let(:backend) { double('backend', read: {}).as_null_object } + + it 'submits a single writing to the cache' do + cache.write_if_empty + cache.write_if_empty + + expect(backend).to have_received(:write).with(cache.key, + hash_including('CHANGELOG-false-false-false'), + expires_in: 1.week).once + end + end + + describe '#clear' do + let(:backend) { double('backend').as_null_object } + + it 'clears cache' do + cache.clear + + expect(backend).to have_received(:delete).with(cache.key) + end + end +end diff --git a/spec/lib/gitlab/git/repository_spec.rb b/spec/lib/gitlab/git/repository_spec.rb index 1098a266140..28c34e234f7 100644 --- a/spec/lib/gitlab/git/repository_spec.rb +++ b/spec/lib/gitlab/git/repository_spec.rb @@ -591,6 +591,10 @@ describe Gitlab::Git::Repository, :seed_helper do expect(repository.find_remote_root_ref('origin')).to eq 'master' end + it 'returns UTF-8' do + expect(repository.find_remote_root_ref('origin')).to be_utf8 + end + it 'returns nil when remote name is nil' do expect_any_instance_of(Gitlab::GitalyClient::RemoteService) .not_to receive(:find_remote_root_ref) diff --git a/spec/lib/gitlab/gitaly_client/commit_service_spec.rb b/spec/lib/gitlab/gitaly_client/commit_service_spec.rb index 54f2ea33f90..bcdf12a00a0 100644 --- a/spec/lib/gitlab/gitaly_client/commit_service_spec.rb +++ b/spec/lib/gitlab/gitaly_client/commit_service_spec.rb @@ -19,7 +19,14 @@ describe Gitlab::GitalyClient::CommitService do right_commit_id: commit.id, collapse_diffs: false, enforce_limits: true, - **Gitlab::Git::DiffCollection.collection_limits.to_h + # Tests limitation parameters explicitly + max_files: 100, + max_lines: 5000, + max_bytes: 512000, + safe_max_files: 100, + safe_max_lines: 5000, + safe_max_bytes: 512000, + max_patch_bytes: 102400 ) expect_any_instance_of(Gitaly::DiffService::Stub).to receive(:commit_diff).with(request, kind_of(Hash)) @@ -37,7 +44,14 @@ describe Gitlab::GitalyClient::CommitService do right_commit_id: initial_commit.id, collapse_diffs: false, enforce_limits: true, - **Gitlab::Git::DiffCollection.collection_limits.to_h + # Tests limitation parameters explicitly + max_files: 100, + max_lines: 5000, + max_bytes: 512000, + safe_max_files: 100, + safe_max_lines: 5000, + safe_max_bytes: 512000, + max_patch_bytes: 102400 ) expect_any_instance_of(Gitaly::DiffService::Stub).to receive(:commit_diff).with(request, kind_of(Hash)) diff --git a/spec/lib/gitlab/gitaly_client/remote_service_spec.rb b/spec/lib/gitlab/gitaly_client/remote_service_spec.rb index b8831c54aba..9030a49983d 100644 --- a/spec/lib/gitlab/gitaly_client/remote_service_spec.rb +++ b/spec/lib/gitlab/gitaly_client/remote_service_spec.rb @@ -54,6 +54,15 @@ describe Gitlab::GitalyClient::RemoteService do expect(client.find_remote_root_ref('origin')).to eq 'master' end + + it 'ensure ref is a valid UTF-8 string' do + expect_any_instance_of(Gitaly::RemoteService::Stub) + .to receive(:find_remote_root_ref) + .with(gitaly_request_with_path(storage_name, relative_path), kind_of(Hash)) + .and_return(double(ref: "an_invalid_ref_\xE5")) + + expect(client.find_remote_root_ref('origin')).to eq "an_invalid_ref_å" + end end describe '#update_remote_mirror' do diff --git a/spec/lib/gitlab/import_export/after_export_strategies/base_after_export_strategy_object_storage_spec.rb b/spec/lib/gitlab/import_export/after_export_strategies/base_after_export_strategy_object_storage_spec.rb deleted file mode 100644 index 5059d68e54b..00000000000 --- a/spec/lib/gitlab/import_export/after_export_strategies/base_after_export_strategy_object_storage_spec.rb +++ /dev/null @@ -1,105 +0,0 @@ -require 'spec_helper' - -describe Gitlab::ImportExport::AfterExportStrategies::BaseAfterExportStrategy do - let!(:service) { described_class.new } - let!(:project) { create(:project, :with_object_export) } - let(:shared) { project.import_export_shared } - let!(:user) { create(:user) } - - describe '#execute' do - before do - allow(service).to receive(:strategy_execute) - stub_feature_flags(import_export_object_storage: true) - end - - it 'returns if project exported file is not found' do - allow(project).to receive(:export_project_object_exists?).and_return(false) - - expect(service).not_to receive(:strategy_execute) - - service.execute(user, project) - end - - it 'creates a lock file in the export dir' do - allow(service).to receive(:delete_after_export_lock) - - service.execute(user, project) - - expect(lock_path_exist?).to be_truthy - end - - context 'when the method succeeds' do - it 'removes the lock file' do - service.execute(user, project) - - expect(lock_path_exist?).to be_falsey - end - end - - context 'when the method fails' do - before do - allow(service).to receive(:strategy_execute).and_call_original - end - - context 'when validation fails' do - before do - allow(service).to receive(:invalid?).and_return(true) - end - - it 'does not create the lock file' do - expect(service).not_to receive(:create_or_update_after_export_lock) - - service.execute(user, project) - end - - it 'does not execute main logic' do - expect(service).not_to receive(:strategy_execute) - - service.execute(user, project) - end - - it 'logs validation errors in shared context' do - expect(service).to receive(:log_validation_errors) - - service.execute(user, project) - end - end - - context 'when an exception is raised' do - it 'removes the lock' do - expect { service.execute(user, project) }.to raise_error(NotImplementedError) - - expect(lock_path_exist?).to be_falsey - end - end - end - end - - describe '#log_validation_errors' do - it 'add the message to the shared context' do - errors = %w(test_message test_message2) - - allow(service).to receive(:invalid?).and_return(true) - allow(service.errors).to receive(:full_messages).and_return(errors) - - expect(shared).to receive(:add_error_message).twice.and_call_original - - service.execute(user, project) - - expect(shared.errors).to eq errors - end - end - - describe '#to_json' do - it 'adds the current strategy class to the serialized attributes' do - params = { param1: 1 } - result = params.merge(klass: described_class.to_s).to_json - - expect(described_class.new(params).to_json).to eq result - end - end - - def lock_path_exist? - File.exist?(described_class.lock_file_path(project)) - end -end diff --git a/spec/lib/gitlab/import_export/after_export_strategies/base_after_export_strategy_spec.rb b/spec/lib/gitlab/import_export/after_export_strategies/base_after_export_strategy_spec.rb index 566b7f46c87..9a442de2900 100644 --- a/spec/lib/gitlab/import_export/after_export_strategies/base_after_export_strategy_spec.rb +++ b/spec/lib/gitlab/import_export/after_export_strategies/base_after_export_strategy_spec.rb @@ -9,11 +9,10 @@ describe Gitlab::ImportExport::AfterExportStrategies::BaseAfterExportStrategy do describe '#execute' do before do allow(service).to receive(:strategy_execute) - stub_feature_flags(import_export_object_storage: false) end it 'returns if project exported file is not found' do - allow(project).to receive(:export_project_path).and_return(nil) + allow(project).to receive(:export_file_exists?).and_return(false) expect(service).not_to receive(:strategy_execute) diff --git a/spec/lib/gitlab/import_export/after_export_strategies/web_upload_strategy_spec.rb b/spec/lib/gitlab/import_export/after_export_strategies/web_upload_strategy_spec.rb index 7f2e0a4ee2c..ec17ad8541f 100644 --- a/spec/lib/gitlab/import_export/after_export_strategies/web_upload_strategy_spec.rb +++ b/spec/lib/gitlab/import_export/after_export_strategies/web_upload_strategy_spec.rb @@ -24,34 +24,13 @@ describe Gitlab::ImportExport::AfterExportStrategies::WebUploadStrategy do end describe '#execute' do - context 'without object storage' do - before do - stub_feature_flags(import_export_object_storage: false) - end - - it 'removes the exported project file after the upload' do - allow(strategy).to receive(:send_file) - allow(strategy).to receive(:handle_response_error) - - expect(project).to receive(:remove_exported_project_file) - - strategy.execute(user, project) - end - end - - context 'with object storage' do - before do - stub_feature_flags(import_export_object_storage: true) - end + it 'removes the exported project file after the upload' do + allow(strategy).to receive(:send_file) + allow(strategy).to receive(:handle_response_error) - it 'removes the exported project file after the upload' do - allow(strategy).to receive(:send_file) - allow(strategy).to receive(:handle_response_error) + expect(project).to receive(:remove_exports) - expect(project).to receive(:remove_exported_project_file) - - strategy.execute(user, project) - end + strategy.execute(user, project) end end end diff --git a/spec/lib/gitlab/import_export/all_models.yml b/spec/lib/gitlab/import_export/all_models.yml index b4269bd5786..ec2bdbe22e1 100644 --- a/spec/lib/gitlab/import_export/all_models.yml +++ b/spec/lib/gitlab/import_export/all_models.yml @@ -288,6 +288,7 @@ project: - fork_network_member - fork_network - custom_attributes +- prometheus_metrics - lfs_file_locks - project_badges - source_of_merge_requests @@ -303,6 +304,8 @@ award_emoji: - user priorities: - label +prometheus_metrics: +- project timelogs: - issue - merge_request @@ -321,3 +324,9 @@ metrics: - latest_closed_by - merged_by - pipeline +resource_label_events: +- user +- issue +- merge_request +- epic +- label diff --git a/spec/lib/gitlab/import_export/avatar_saver_spec.rb b/spec/lib/gitlab/import_export/avatar_saver_spec.rb index 90e6d653d34..2bd1b9924c6 100644 --- a/spec/lib/gitlab/import_export/avatar_saver_spec.rb +++ b/spec/lib/gitlab/import_export/avatar_saver_spec.rb @@ -8,8 +8,7 @@ describe Gitlab::ImportExport::AvatarSaver do before do FileUtils.mkdir_p("#{shared.export_path}/avatar/") - allow_any_instance_of(Gitlab::ImportExport).to receive(:storage_path).and_return(export_path) - stub_feature_flags(import_export_object_storage: false) + allow_any_instance_of(Gitlab::ImportExport::Shared).to receive(:export_path).and_return(export_path) end after do @@ -19,7 +18,7 @@ describe Gitlab::ImportExport::AvatarSaver do it 'saves a project avatar' do described_class.new(project: project_with_avatar, shared: shared).save - expect(File).to exist("#{shared.export_path}/avatar/dk.png") + expect(File).to exist(Dir["#{shared.export_path}/avatar/**/dk.png"].first) end it 'is fine not to have an avatar' do diff --git a/spec/lib/gitlab/import_export/file_importer_object_storage_spec.rb b/spec/lib/gitlab/import_export/file_importer_object_storage_spec.rb deleted file mode 100644 index 287745eb40e..00000000000 --- a/spec/lib/gitlab/import_export/file_importer_object_storage_spec.rb +++ /dev/null @@ -1,89 +0,0 @@ -require 'spec_helper' - -describe Gitlab::ImportExport::FileImporter do - let(:shared) { Gitlab::ImportExport::Shared.new(nil) } - let(:storage_path) { "#{Dir.tmpdir}/file_importer_spec" } - let(:valid_file) { "#{shared.export_path}/valid.json" } - let(:symlink_file) { "#{shared.export_path}/invalid.json" } - let(:hidden_symlink_file) { "#{shared.export_path}/.hidden" } - let(:subfolder_symlink_file) { "#{shared.export_path}/subfolder/invalid.json" } - let(:evil_symlink_file) { "#{shared.export_path}/.\nevil" } - - before do - stub_const('Gitlab::ImportExport::FileImporter::MAX_RETRIES', 0) - stub_feature_flags(import_export_object_storage: true) - stub_uploads_object_storage(FileUploader) - - allow_any_instance_of(Gitlab::ImportExport).to receive(:storage_path).and_return(storage_path) - allow_any_instance_of(Gitlab::ImportExport::CommandLineUtil).to receive(:untar_zxf).and_return(true) - allow_any_instance_of(Gitlab::ImportExport::Shared).to receive(:relative_archive_path).and_return('test') - allow(SecureRandom).to receive(:hex).and_return('abcd') - setup_files - end - - after do - FileUtils.rm_rf(storage_path) - end - - context 'normal run' do - before do - described_class.import(project: build(:project), archive_file: '', shared: shared) - end - - it 'removes symlinks in root folder' do - expect(File.exist?(symlink_file)).to be false - end - - it 'removes hidden symlinks in root folder' do - expect(File.exist?(hidden_symlink_file)).to be false - end - - it 'removes evil symlinks in root folder' do - expect(File.exist?(evil_symlink_file)).to be false - end - - it 'removes symlinks in subfolders' do - expect(File.exist?(subfolder_symlink_file)).to be false - end - - it 'does not remove a valid file' do - expect(File.exist?(valid_file)).to be true - end - - it 'creates the file in the right subfolder' do - expect(shared.export_path).to include('test/abcd') - end - end - - context 'error' do - before do - allow_any_instance_of(described_class).to receive(:wait_for_archived_file).and_raise(StandardError) - described_class.import(project: build(:project), archive_file: '', shared: shared) - end - - it 'removes symlinks in root folder' do - expect(File.exist?(symlink_file)).to be false - end - - it 'removes hidden symlinks in root folder' do - expect(File.exist?(hidden_symlink_file)).to be false - end - - it 'removes symlinks in subfolders' do - expect(File.exist?(subfolder_symlink_file)).to be false - end - - it 'does not remove a valid file' do - expect(File.exist?(valid_file)).to be true - end - end - - def setup_files - FileUtils.mkdir_p("#{shared.export_path}/subfolder/") - FileUtils.touch(valid_file) - FileUtils.ln_s(valid_file, symlink_file) - FileUtils.ln_s(valid_file, subfolder_symlink_file) - FileUtils.ln_s(valid_file, hidden_symlink_file) - FileUtils.ln_s(valid_file, evil_symlink_file) - end -end diff --git a/spec/lib/gitlab/import_export/file_importer_spec.rb b/spec/lib/gitlab/import_export/file_importer_spec.rb index 78fccdf1dfc..bf34cefe18f 100644 --- a/spec/lib/gitlab/import_export/file_importer_spec.rb +++ b/spec/lib/gitlab/import_export/file_importer_spec.rb @@ -2,7 +2,7 @@ require 'spec_helper' describe Gitlab::ImportExport::FileImporter do let(:shared) { Gitlab::ImportExport::Shared.new(nil) } - let(:export_path) { "#{Dir.tmpdir}/file_importer_spec" } + let(:storage_path) { "#{Dir.tmpdir}/file_importer_spec" } let(:valid_file) { "#{shared.export_path}/valid.json" } let(:symlink_file) { "#{shared.export_path}/invalid.json" } let(:hidden_symlink_file) { "#{shared.export_path}/.hidden" } @@ -11,7 +11,9 @@ describe Gitlab::ImportExport::FileImporter do before do stub_const('Gitlab::ImportExport::FileImporter::MAX_RETRIES', 0) - allow_any_instance_of(Gitlab::ImportExport).to receive(:storage_path).and_return(export_path) + stub_uploads_object_storage(FileUploader) + + allow_any_instance_of(Gitlab::ImportExport).to receive(:storage_path).and_return(storage_path) allow_any_instance_of(Gitlab::ImportExport::CommandLineUtil).to receive(:untar_zxf).and_return(true) allow_any_instance_of(Gitlab::ImportExport::Shared).to receive(:relative_archive_path).and_return('test') allow(SecureRandom).to receive(:hex).and_return('abcd') @@ -19,12 +21,12 @@ describe Gitlab::ImportExport::FileImporter do end after do - FileUtils.rm_rf(export_path) + FileUtils.rm_rf(storage_path) end context 'normal run' do before do - described_class.import(project: nil, archive_file: '', shared: shared) + described_class.import(project: build(:project), archive_file: '', shared: shared) end it 'removes symlinks in root folder' do @@ -55,7 +57,7 @@ describe Gitlab::ImportExport::FileImporter do context 'error' do before do allow_any_instance_of(described_class).to receive(:wait_for_archived_file).and_raise(StandardError) - described_class.import(project: nil, archive_file: '', shared: shared) + described_class.import(project: build(:project), archive_file: '', shared: shared) end it 'removes symlinks in root folder' do diff --git a/spec/lib/gitlab/import_export/importer_object_storage_spec.rb b/spec/lib/gitlab/import_export/importer_object_storage_spec.rb deleted file mode 100644 index 24a994b3611..00000000000 --- a/spec/lib/gitlab/import_export/importer_object_storage_spec.rb +++ /dev/null @@ -1,115 +0,0 @@ -require 'spec_helper' - -describe Gitlab::ImportExport::Importer do - let(:user) { create(:user) } - let(:test_path) { "#{Dir.tmpdir}/importer_spec" } - let(:shared) { project.import_export_shared } - let(:project) { create(:project) } - let(:import_file) { fixture_file_upload('spec/features/projects/import_export/test_project_export.tar.gz') } - - subject(:importer) { described_class.new(project) } - - before do - allow_any_instance_of(Gitlab::ImportExport).to receive(:storage_path).and_return(test_path) - allow_any_instance_of(Gitlab::ImportExport::FileImporter).to receive(:remove_import_file) - stub_feature_flags(import_export_object_storage: true) - stub_uploads_object_storage(FileUploader) - - FileUtils.mkdir_p(shared.export_path) - ImportExportUpload.create(project: project, import_file: import_file) - end - - after do - FileUtils.rm_rf(test_path) - end - - describe '#execute' do - it 'succeeds' do - importer.execute - - expect(shared.errors).to be_empty - end - - it 'extracts the archive' do - expect(Gitlab::ImportExport::FileImporter).to receive(:import).and_call_original - - importer.execute - end - - it 'checks the version' do - expect(Gitlab::ImportExport::VersionChecker).to receive(:check!).and_call_original - - importer.execute - end - - context 'all restores are executed' do - [ - Gitlab::ImportExport::AvatarRestorer, - Gitlab::ImportExport::RepoRestorer, - Gitlab::ImportExport::WikiRestorer, - Gitlab::ImportExport::UploadsRestorer, - Gitlab::ImportExport::LfsRestorer, - Gitlab::ImportExport::StatisticsRestorer - ].each do |restorer| - it "calls the #{restorer}" do - fake_restorer = double(restorer.to_s) - - expect(fake_restorer).to receive(:restore).and_return(true).at_least(1) - expect(restorer).to receive(:new).and_return(fake_restorer).at_least(1) - - importer.execute - end - end - - it 'restores the ProjectTree' do - expect(Gitlab::ImportExport::ProjectTreeRestorer).to receive(:new).and_call_original - - importer.execute - end - - it 'removes the import file' do - expect(importer).to receive(:remove_import_file).and_call_original - - importer.execute - - expect(project.import_export_upload.import_file&.file).to be_nil - end - end - - context 'when project successfully restored' do - let!(:existing_project) { create(:project, namespace: user.namespace) } - let(:project) { create(:project, namespace: user.namespace, name: 'whatever', path: 'whatever') } - - before do - restorers = double(:restorers, all?: true) - - allow(subject).to receive(:import_file).and_return(true) - allow(subject).to receive(:check_version!).and_return(true) - allow(subject).to receive(:restorers).and_return(restorers) - allow(project).to receive(:import_data).and_return(double(data: { 'original_path' => existing_project.path })) - end - - context 'when import_data' do - context 'has original_path' do - it 'overwrites existing project' do - expect_any_instance_of(::Projects::OverwriteProjectService).to receive(:execute).with(existing_project) - - subject.execute - end - end - - context 'has not original_path' do - before do - allow(project).to receive(:import_data).and_return(double(data: {})) - end - - it 'does not call the overwrite service' do - expect_any_instance_of(::Projects::OverwriteProjectService).not_to receive(:execute).with(existing_project) - - subject.execute - end - end - end - end - end -end diff --git a/spec/lib/gitlab/import_export/importer_spec.rb b/spec/lib/gitlab/import_export/importer_spec.rb index 8053c48ad6c..11f98d782b1 100644 --- a/spec/lib/gitlab/import_export/importer_spec.rb +++ b/spec/lib/gitlab/import_export/importer_spec.rb @@ -4,16 +4,18 @@ describe Gitlab::ImportExport::Importer do let(:user) { create(:user) } let(:test_path) { "#{Dir.tmpdir}/importer_spec" } let(:shared) { project.import_export_shared } - let(:project) { create(:project, import_source: File.join(test_path, 'test_project_export.tar.gz')) } + let(:project) { create(:project) } + let(:import_file) { fixture_file_upload('spec/features/projects/import_export/test_project_export.tar.gz') } subject(:importer) { described_class.new(project) } before do allow_any_instance_of(Gitlab::ImportExport).to receive(:storage_path).and_return(test_path) allow_any_instance_of(Gitlab::ImportExport::FileImporter).to receive(:remove_import_file) + stub_uploads_object_storage(FileUploader) FileUtils.mkdir_p(shared.export_path) - FileUtils.cp(Rails.root.join('spec/features/projects/import_export/test_project_export.tar.gz'), test_path) + ImportExportUpload.create(project: project, import_file: import_file) end after do @@ -64,6 +66,14 @@ describe Gitlab::ImportExport::Importer do importer.execute end + it 'removes the import file' do + expect(importer).to receive(:remove_import_file).and_call_original + + importer.execute + + expect(project.import_export_upload.import_file&.file).to be_nil + end + it 'sets the correct visibility_level when visibility level is a string' do project.create_or_update_import_data( data: { override_params: { visibility_level: Gitlab::VisibilityLevel::PRIVATE.to_s } } @@ -85,7 +95,6 @@ describe Gitlab::ImportExport::Importer do allow(subject).to receive(:import_file).and_return(true) allow(subject).to receive(:check_version!).and_return(true) allow(subject).to receive(:restorers).and_return(restorers) - allow(restorers).to receive(:all?).and_return(true) allow(project).to receive(:import_data).and_return(double(data: { 'original_path' => existing_project.path })) end diff --git a/spec/lib/gitlab/import_export/project.json b/spec/lib/gitlab/import_export/project.json index 1b7fa11cb3c..eefd00e7383 100644 --- a/spec/lib/gitlab/import_export/project.json +++ b/spec/lib/gitlab/import_export/project.json @@ -331,6 +331,28 @@ }, "events": [] } + ], + "resource_label_events": [ + { + "id":244, + "action":"remove", + "issue_id":40, + "merge_request_id":null, + "label_id":2, + "user_id":1, + "created_at":"2018-08-28T08:24:00.494Z", + "label": { + "id": 2, + "title": "test2", + "color": "#428bca", + "project_id": 8, + "created_at": "2016-07-22T08:55:44.161Z", + "updated_at": "2016-07-22T08:55:44.161Z", + "template": false, + "description": "", + "type": "ProjectLabel" + } + } ] }, { @@ -2515,6 +2537,17 @@ "events": [] } ], + "resource_label_events": [ + { + "id":243, + "action":"add", + "issue_id":null, + "merge_request_id":27, + "label_id":null, + "user_id":1, + "created_at":"2018-08-28T08:24:00.494Z" + } + ], "merge_request_diff": { "id": 27, "state": "collected", 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 a88ac0a091e..3ff6be595a8 100644 --- a/spec/lib/gitlab/import_export/project_tree_restorer_spec.rb +++ b/spec/lib/gitlab/import_export/project_tree_restorer_spec.rb @@ -89,6 +89,14 @@ describe Gitlab::ImportExport::ProjectTreeRestorer do expect(ProtectedTag.first.create_access_levels).not_to be_empty end + it 'restores issue resource label events' do + expect(Issue.find_by(title: 'Voluptatem').resource_label_events).not_to be_empty + end + + it 'restores merge requests resource label events' do + expect(MergeRequest.find_by(title: 'MR1').resource_label_events).not_to be_empty + end + context 'event at forth level of the tree' do let(:event) { Event.where(action: 6).first } diff --git a/spec/lib/gitlab/import_export/project_tree_saver_spec.rb b/spec/lib/gitlab/import_export/project_tree_saver_spec.rb index fec8a2af9ab..5dc372263ad 100644 --- a/spec/lib/gitlab/import_export/project_tree_saver_spec.rb +++ b/spec/lib/gitlab/import_export/project_tree_saver_spec.rb @@ -169,6 +169,14 @@ describe Gitlab::ImportExport::ProjectTreeSaver do expect(priorities.flatten).not_to be_empty end + it 'has issue resource label events' do + expect(saved_project_json['issues'].first['resource_label_events']).not_to be_empty + end + + it 'has merge request resource label events' do + expect(saved_project_json['merge_requests'].first['resource_label_events']).not_to be_empty + end + it 'saves the correct service type' do expect(saved_project_json['services'].first['type']).to eq('CustomIssueTrackerService') end @@ -291,6 +299,9 @@ describe Gitlab::ImportExport::ProjectTreeSaver do project: project, commit_id: ci_build.pipeline.sha) + create(:resource_label_event, label: project_label, issue: issue) + create(:resource_label_event, label: group_label, merge_request: merge_request) + create(:event, :created, target: milestone, project: project, author: user) create(:service, project: project, type: 'CustomIssueTrackerService', category: 'issue_tracker', properties: { one: 'value' }) diff --git a/spec/lib/gitlab/import_export/safe_model_attributes.yml b/spec/lib/gitlab/import_export/safe_model_attributes.yml index 579f175c4a8..e9f1be172b0 100644 --- a/spec/lib/gitlab/import_export/safe_model_attributes.yml +++ b/spec/lib/gitlab/import_export/safe_model_attributes.yml @@ -555,6 +555,19 @@ ProjectCustomAttribute: - project_id - key - value +PrometheusMetric: +- id +- created_at +- updated_at +- project_id +- y_label +- unit +- legend +- title +- query +- group +- common +- identifier Badge: - id - link_url @@ -566,3 +579,11 @@ Badge: - type ProjectCiCdSetting: - group_runners_enabled +ResourceLabelEvent: +- id +- action +- issue_id +- merge_request_id +- label_id +- user_id +- created_at diff --git a/spec/lib/gitlab/import_export/saver_spec.rb b/spec/lib/gitlab/import_export/saver_spec.rb index 02f1a4b81aa..d185ff2dfcc 100644 --- a/spec/lib/gitlab/import_export/saver_spec.rb +++ b/spec/lib/gitlab/import_export/saver_spec.rb @@ -18,26 +18,12 @@ describe Gitlab::ImportExport::Saver do FileUtils.rm_rf(export_path) end - context 'local archive' do - it 'saves the repo to disk' do - stub_feature_flags(import_export_object_storage: false) + it 'saves the repo using object storage' do + stub_uploads_object_storage(ImportExportUploader) - subject.save + subject.save - expect(shared.errors).to be_empty - expect(Dir.empty?(shared.archive_path)).to be false - end - end - - context 'object storage' do - it 'saves the repo using object storage' do - stub_feature_flags(import_export_object_storage: true) - stub_uploads_object_storage(ImportExportUploader) - - subject.save - - expect(ImportExportUpload.find_by(project: project).export_file.url) - .to match(%r[\/uploads\/-\/system\/import_export_upload\/export_file.*]) - end + expect(ImportExportUpload.find_by(project: project).export_file.url) + .to match(%r[\/uploads\/-\/system\/import_export_upload\/export_file.*]) end end diff --git a/spec/lib/gitlab/import_export/uploads_manager_spec.rb b/spec/lib/gitlab/import_export/uploads_manager_spec.rb index f799de18cd0..792117e1df1 100644 --- a/spec/lib/gitlab/import_export/uploads_manager_spec.rb +++ b/spec/lib/gitlab/import_export/uploads_manager_spec.rb @@ -4,6 +4,7 @@ describe Gitlab::ImportExport::UploadsManager do let(:shared) { project.import_export_shared } let(:export_path) { "#{Dir.tmpdir}/project_tree_saver_spec" } let(:project) { create(:project) } + let(:upload) { create(:upload, :issuable_upload, :object_storage, model: project) } let(:exported_file_path) { "#{shared.export_path}/uploads/#{upload.secret}/#{File.basename(upload.path)}" } subject(:manager) { described_class.new(project: project, shared: shared) } @@ -69,44 +70,20 @@ describe Gitlab::ImportExport::UploadsManager do end end end + end - context 'using object storage' do - let!(:upload) { create(:upload, :issuable_upload, :object_storage, model: project) } - - before do - stub_feature_flags(import_export_object_storage: true) - stub_uploads_object_storage(FileUploader) - end - - it 'saves the file' do - fake_uri = double - - expect(fake_uri).to receive(:open).and_return(StringIO.new('File content')) - expect(URI).to receive(:parse).and_return(fake_uri) - - manager.save + describe '#restore' do + before do + stub_uploads_object_storage(FileUploader) - expect(File.read(exported_file_path)).to eq('File content') - end + FileUtils.mkdir_p(File.join(shared.export_path, 'uploads/72a497a02fe3ee09edae2ed06d390038')) + FileUtils.touch(File.join(shared.export_path, 'uploads/72a497a02fe3ee09edae2ed06d390038', "dummy.txt")) end - describe '#restore' do - context 'using object storage' do - before do - stub_feature_flags(import_export_object_storage: true) - stub_uploads_object_storage(FileUploader) - - FileUtils.mkdir_p(File.join(shared.export_path, 'uploads/72a497a02fe3ee09edae2ed06d390038')) - FileUtils.touch(File.join(shared.export_path, 'uploads/72a497a02fe3ee09edae2ed06d390038', "dummy.txt")) - end + it 'restores the file' do + manager.restore - it 'restores the file' do - manager.restore - - expect(project.uploads.size).to eq(1) - expect(project.uploads.first.build_uploader.filename).to eq('dummy.txt') - end - end + expect(project.uploads.map { |u| u.build_uploader.filename }).to include('dummy.txt') end end end diff --git a/spec/lib/gitlab/import_export/uploads_restorer_spec.rb b/spec/lib/gitlab/import_export/uploads_restorer_spec.rb index acef97459b8..6072f18b8c7 100644 --- a/spec/lib/gitlab/import_export/uploads_restorer_spec.rb +++ b/spec/lib/gitlab/import_export/uploads_restorer_spec.rb @@ -8,7 +8,7 @@ describe Gitlab::ImportExport::UploadsRestorer do before do allow_any_instance_of(Gitlab::ImportExport).to receive(:storage_path).and_return(export_path) FileUtils.mkdir_p(File.join(shared.export_path, 'uploads/random')) - FileUtils.touch(File.join(shared.export_path, 'uploads/random', "dummy.txt")) + FileUtils.touch(File.join(shared.export_path, 'uploads/random', 'dummy.txt')) end after do @@ -27,9 +27,7 @@ describe Gitlab::ImportExport::UploadsRestorer do it 'copies the uploads to the project path' do subject.restore - uploads = Dir.glob(File.join(subject.uploads_path, '**/*')).map { |file| File.basename(file) } - - expect(uploads).to include('dummy.txt') + expect(project.uploads.map { |u| u.build_uploader.filename }).to include('dummy.txt') end end @@ -45,9 +43,7 @@ describe Gitlab::ImportExport::UploadsRestorer do it 'copies the uploads to the project path' do subject.restore - uploads = Dir.glob(File.join(subject.uploads_path, '**/*')).map { |file| File.basename(file) } - - expect(uploads).to include('dummy.txt') + expect(project.uploads.map { |u| u.build_uploader.filename }).to include('dummy.txt') end end end diff --git a/spec/lib/gitlab/import_export/uploads_saver_spec.rb b/spec/lib/gitlab/import_export/uploads_saver_spec.rb index c716edd9397..24993460e51 100644 --- a/spec/lib/gitlab/import_export/uploads_saver_spec.rb +++ b/spec/lib/gitlab/import_export/uploads_saver_spec.rb @@ -7,7 +7,6 @@ describe Gitlab::ImportExport::UploadsSaver do let(:shared) { project.import_export_shared } before do - stub_feature_flags(import_export_object_storage: false) allow_any_instance_of(Gitlab::ImportExport).to receive(:storage_path).and_return(export_path) end diff --git a/spec/lib/gitlab/prometheus/additional_metrics_parser_spec.rb b/spec/lib/gitlab/prometheus/additional_metrics_parser_spec.rb index 5589db92b1d..1a108003bc2 100644 --- a/spec/lib/gitlab/prometheus/additional_metrics_parser_spec.rb +++ b/spec/lib/gitlab/prometheus/additional_metrics_parser_spec.rb @@ -6,7 +6,7 @@ describe Gitlab::Prometheus::AdditionalMetricsParser do let(:parser_error_class) { Gitlab::Prometheus::ParsingError } describe '#load_groups_from_yaml' do - subject { described_class.load_groups_from_yaml } + subject { described_class.load_groups_from_yaml('dummy.yaml') } describe 'parsing sample yaml' do let(:sample_yaml) do diff --git a/spec/lib/gitlab/prometheus/metric_group_spec.rb b/spec/lib/gitlab/prometheus/metric_group_spec.rb new file mode 100644 index 00000000000..e7d16e73663 --- /dev/null +++ b/spec/lib/gitlab/prometheus/metric_group_spec.rb @@ -0,0 +1,41 @@ +# frozen_string_literal: true + +require 'rails_helper' + +describe Gitlab::Prometheus::MetricGroup do + describe '.common_metrics' do + let!(:project_metric) { create(:prometheus_metric) } + let!(:common_metric_group_a) { create(:prometheus_metric, :common, group: :aws_elb) } + let!(:common_metric_group_b_q1) { create(:prometheus_metric, :common, group: :kubernetes) } + let!(:common_metric_group_b_q2) { create(:prometheus_metric, :common, group: :kubernetes) } + + subject { described_class.common_metrics } + + it 'returns exactly two groups' do + expect(subject.map(&:name)).to contain_exactly( + 'Response metrics (AWS ELB)', 'System metrics (Kubernetes)') + end + + it 'returns exactly three metric queries' do + expect(subject.map(&:metrics).flatten.map(&:id)).to contain_exactly( + common_metric_group_a.id, common_metric_group_b_q1.id, + common_metric_group_b_q2.id) + end + end + + describe '.for_project' do + let!(:other_project) { create(:project) } + let!(:project_metric) { create(:prometheus_metric) } + let!(:common_metric) { create(:prometheus_metric, :common, group: :aws_elb) } + + subject do + described_class.for_project(other_project) + .map(&:metrics).flatten + .map(&:id) + end + + it 'returns exactly one common metric' do + is_expected.to contain_exactly(common_metric.id) + end + end +end diff --git a/spec/lib/gitlab/workhorse_spec.rb b/spec/lib/gitlab/workhorse_spec.rb index 23869f3d2da..b3f55a2e1bd 100644 --- a/spec/lib/gitlab/workhorse_spec.rb +++ b/spec/lib/gitlab/workhorse_spec.rb @@ -336,6 +336,22 @@ describe Gitlab::Workhorse do it { expect { subject }.to raise_exception('Unsupported action: download') } end end + + context 'when receive_max_input_size has been updated' do + it 'returns custom git config' do + allow(Gitlab::CurrentSettings).to receive(:receive_max_input_size) { 1 } + + expect(subject[:GitConfigOptions]).to be_present + end + end + + context 'when receive_max_input_size is empty' do + it 'returns an empty git config' do + allow(Gitlab::CurrentSettings).to receive(:receive_max_input_size) { nil } + + expect(subject[:GitConfigOptions]).to be_empty + end + end end describe '.set_key_and_notify' do diff --git a/spec/migrations/import_common_metrics_spec.rb b/spec/migrations/import_common_metrics_spec.rb new file mode 100644 index 00000000000..1001629007c --- /dev/null +++ b/spec/migrations/import_common_metrics_spec.rb @@ -0,0 +1,16 @@ +# frozen_string_literal: true + +require 'spec_helper' +require Rails.root.join('db', 'migrate', '20180831164910_import_common_metrics.rb') + +describe ImportCommonMetrics, :migration do + describe '#up' do + it "imports all prometheus metrics" do + expect(PrometheusMetric.common).to be_empty + + migrate! + + expect(PrometheusMetric.common).not_to be_empty + end + end +end diff --git a/spec/migrations/remove_orphaned_label_links_spec.rb b/spec/migrations/remove_orphaned_label_links_spec.rb new file mode 100644 index 00000000000..13b8919343e --- /dev/null +++ b/spec/migrations/remove_orphaned_label_links_spec.rb @@ -0,0 +1,40 @@ +# frozen_string_literal: true + +require 'spec_helper' +require Rails.root.join('db', 'post_migrate', '20180906051323_remove_orphaned_label_links.rb') + +describe RemoveOrphanedLabelLinks, :migration do + let(:label_links) { table(:label_links) } + let(:labels) { table(:labels) } + + let(:project) { create(:project) } # rubocop:disable RSpec/FactoriesInMigrationSpecs + let(:label) { create_label } + + context 'add foreign key on label_id' do + let!(:label_link_with_label) { create_label_link(label_id: label.id) } + let!(:label_link_without_label) { create_label_link(label_id: nil) } + + it 'removes orphaned labels without corresponding label' do + expect { migrate! }.to change { LabelLink.count }.from(2).to(1) + end + + it 'does not remove entries with valid label_id' do + expect { migrate! }.not_to change { label_link_with_label.reload } + end + end + + def create_label(**opts) + labels.create!( + project_id: project.id, + **opts + ) + end + + def create_label_link(**opts) + label_links.create!( + target_id: 1, + target_type: 'Issue', + **opts + ) + end +end diff --git a/spec/models/label_note_spec.rb b/spec/models/label_note_spec.rb new file mode 100644 index 00000000000..f69874d94aa --- /dev/null +++ b/spec/models/label_note_spec.rb @@ -0,0 +1,23 @@ +# frozen_string_literal: true + +require 'spec_helper' + +describe LabelNote do + set(:project) { create(:project, :repository) } + set(:user) { create(:user) } + set(:label) { create(:label, project: project) } + set(:label2) { create(:label, project: project) } + let(:resource_parent) { project } + + context 'when resource is issue' do + set(:resource) { create(:issue, project: project) } + + it_behaves_like 'label note created from events' + end + + context 'when resource is merge request' do + set(:resource) { create(:merge_request, source_project: project, target_project: project) } + + it_behaves_like 'label note created from events' + end +end diff --git a/spec/models/namespace_spec.rb b/spec/models/namespace_spec.rb index 9b7f932ec3a..3649990670b 100644 --- a/spec/models/namespace_spec.rb +++ b/spec/models/namespace_spec.rb @@ -394,12 +394,6 @@ describe Namespace do child.destroy end end - - it 'removes the exports folder' do - expect(namespace).to receive(:remove_exports!) - - namespace.destroy - end end context 'hashed storage' do @@ -414,12 +408,6 @@ describe Namespace do expect(File.exist?(deleted_path_in_dir)).to be(false) end - - it 'removes the exports folder' do - expect(namespace).to receive(:remove_exports!) - - namespace.destroy - end end end @@ -706,26 +694,6 @@ describe Namespace do end end - describe '#remove_exports' do - let(:legacy_project) { create(:project, :with_export, :legacy_storage, namespace: namespace) } - let(:hashed_project) { create(:project, :with_export, namespace: namespace) } - let(:export_path) { Dir.mktmpdir('namespace_remove_exports_spec') } - let(:legacy_export) { legacy_project.export_project_path } - let(:hashed_export) { hashed_project.export_project_path } - - it 'removes exports for legacy and hashed projects' do - allow(Gitlab::ImportExport).to receive(:storage_path) { export_path } - - expect(File.exist?(legacy_export)).to be_truthy - expect(File.exist?(hashed_export)).to be_truthy - - namespace.remove_exports! - - expect(File.exist?(legacy_export)).to be_falsy - expect(File.exist?(hashed_export)).to be_falsy - end - end - describe '#full_path_was' do context 'when the group has no parent' do it 'should return the path was' do diff --git a/spec/models/project_spec.rb b/spec/models/project_spec.rb index 264632dba4b..cb844cd2102 100644 --- a/spec/models/project_spec.rb +++ b/spec/models/project_spec.rb @@ -2854,73 +2854,12 @@ describe Project do end describe '#remove_export' do - let(:legacy_project) { create(:project, :legacy_storage, :with_export) } let(:project) { create(:project, :with_export) } - before do - stub_feature_flags(import_export_object_storage: false) - end - - it 'removes the exports directory for the project' do - expect(File.exist?(project.export_path)).to be_truthy - - allow(FileUtils).to receive(:rm_rf).and_call_original - expect(FileUtils).to receive(:rm_rf).with(project.export_path).and_call_original + it 'removes the export' do project.remove_exports - expect(File.exist?(project.export_path)).to be_falsy - end - - it 'is a no-op on legacy projects when there is no namespace' do - export_path = legacy_project.export_path - - legacy_project.namespace.delete - legacy_project.reload - - expect(FileUtils).not_to receive(:rm_rf).with(export_path) - - legacy_project.remove_exports - - expect(File.exist?(export_path)).to be_truthy - end - - it 'runs on hashed storage projects when there is no namespace' do - export_path = project.export_path - - project.namespace.delete - legacy_project.reload - - allow(FileUtils).to receive(:rm_rf).and_call_original - expect(FileUtils).to receive(:rm_rf).with(export_path).and_call_original - - project.remove_exports - - expect(File.exist?(export_path)).to be_falsy - end - - it 'is run when the project is destroyed' do - expect(project).to receive(:remove_exports).and_call_original - - project.destroy - end - end - - describe '#remove_exported_project_file' do - let(:project) { create(:project, :with_export) } - - it 'removes the exported project file' do - stub_feature_flags(import_export_object_storage: false) - - exported_file = project.export_project_path - - expect(File.exist?(exported_file)).to be_truthy - - allow(FileUtils).to receive(:rm_rf).and_call_original - expect(FileUtils).to receive(:rm_rf).with(exported_file).and_call_original - - project.remove_exported_project_file - - expect(File.exist?(exported_file)).to be_falsy + expect(project.export_file_exists?).to be_falsey end end diff --git a/spec/models/prometheus_metric_spec.rb b/spec/models/prometheus_metric_spec.rb new file mode 100644 index 00000000000..a83a31ae88c --- /dev/null +++ b/spec/models/prometheus_metric_spec.rb @@ -0,0 +1,98 @@ +# frozen_string_literal: true + +require 'spec_helper' + +describe PrometheusMetric do + subject { build(:prometheus_metric) } + let(:other_project) { build(:project) } + + it { is_expected.to belong_to(:project) } + it { is_expected.to validate_presence_of(:title) } + it { is_expected.to validate_presence_of(:query) } + it { is_expected.to validate_presence_of(:group) } + + describe 'common metrics' do + using RSpec::Parameterized::TableSyntax + + where(:common, :project, :result) do + false | other_project | true + false | nil | false + true | other_project | false + true | nil | true + end + + with_them do + before do + subject.common = common + subject.project = project + end + + it { expect(subject.valid?).to eq(result) } + end + end + + describe '#query_series' do + using RSpec::Parameterized::TableSyntax + + where(:legend, :type) do + 'Some other legend' | NilClass + 'Status Code' | Array + end + + with_them do + before do + subject.legend = legend + end + + it { expect(subject.query_series).to be_a(type) } + end + end + + describe '#group_title' do + shared_examples 'group_title' do |group, title| + subject { build(:prometheus_metric, group: group).group_title } + + it "returns text #{title} for group #{group}" do + expect(subject).to eq(title) + end + end + + it_behaves_like 'group_title', :business, 'Business metrics (Custom)' + it_behaves_like 'group_title', :response, 'Response metrics (Custom)' + it_behaves_like 'group_title', :system, 'System metrics (Custom)' + end + + describe '#to_query_metric' do + it 'converts to queryable metric object' do + expect(subject.to_query_metric).to be_instance_of(Gitlab::Prometheus::Metric) + end + + it 'queryable metric object has title' do + expect(subject.to_query_metric.title).to eq(subject.title) + end + + it 'queryable metric object has y_label' do + expect(subject.to_query_metric.y_label).to eq(subject.y_label) + end + + it 'queryable metric has no required_metric' do + expect(subject.to_query_metric.required_metrics).to eq([]) + end + + it 'queryable metric has weight 0' do + expect(subject.to_query_metric.weight).to eq(0) + end + + it 'queryable metrics has query description' do + queries = [ + { + query_range: subject.query, + unit: subject.unit, + label: subject.legend + } + ] + + expect(subject.to_query_metric.queries).to eq(queries) + end + end +end diff --git a/spec/models/resource_label_event_spec.rb b/spec/models/resource_label_event_spec.rb index 4756caa1b97..da6e1b5610d 100644 --- a/spec/models/resource_label_event_spec.rb +++ b/spec/models/resource_label_event_spec.rb @@ -3,7 +3,7 @@ require 'rails_helper' RSpec.describe ResourceLabelEvent, type: :model do - subject { build(:resource_label_event) } + subject { build(:resource_label_event, issue: issue) } let(:issue) { create(:issue) } let(:merge_request) { create(:merge_request) } @@ -16,8 +16,6 @@ RSpec.describe ResourceLabelEvent, type: :model do describe 'validations' do it { is_expected.to be_valid } - it { is_expected.to validate_presence_of(:label) } - it { is_expected.to validate_presence_of(:user) } describe 'Issuable validation' do it 'is invalid if issue_id and merge_request_id are missing' do @@ -45,4 +43,52 @@ RSpec.describe ResourceLabelEvent, type: :model do end end end + + describe '#expire_etag_cache' do + def expect_expiration(issue) + expect_any_instance_of(Gitlab::EtagCaching::Store) + .to receive(:touch) + .with("/#{issue.project.namespace.to_param}/#{issue.project.to_param}/noteable/issue/#{issue.id}/notes") + end + + it 'expires resource note etag cache on event save' do + expect_expiration(subject.issuable) + + subject.save! + end + + it 'expires resource note etag cache on event destroy' do + subject.save! + + expect_expiration(subject.issuable) + + subject.destroy! + end + end + + describe '#outdated_markdown?' do + it 'returns true if label is missing and reference is not empty' do + subject.attributes = { reference: 'ref', label_id: nil } + + expect(subject.outdated_markdown?).to be true + end + + it 'returns true if reference is not set yet' do + subject.attributes = { reference: nil } + + expect(subject.outdated_markdown?).to be true + end + + it 'returns true markdown is outdated' do + subject.attributes = { cached_markdown_version: 0 } + + expect(subject.outdated_markdown?).to be true + end + + it 'returns false if label and reference are set' do + subject.attributes = { reference: 'whatever', cached_markdown_version: CacheMarkdownField::CACHE_COMMONMARK_VERSION } + + expect(subject.outdated_markdown?).to be false + end + end end diff --git a/spec/models/user_spec.rb b/spec/models/user_spec.rb index 2a7aff39240..bee4a3d24a7 100644 --- a/spec/models/user_spec.rb +++ b/spec/models/user_spec.rb @@ -2957,6 +2957,48 @@ describe User do end end + describe '#requires_usage_stats_consent?' do + let(:user) { create(:user, created_at: 8.days.ago) } + + before do + allow(user).to receive(:has_current_license?).and_return false + end + + context 'in single-user environment' do + it 'requires user consent after one week' do + create(:user, ghost: true) + + expect(user.requires_usage_stats_consent?).to be true + end + + it 'requires user consent after one week if there is another ghost user' do + expect(user.requires_usage_stats_consent?).to be true + end + + it 'does not require consent in the first week' do + user.created_at = 6.days.ago + + expect(user.requires_usage_stats_consent?).to be false + end + + it 'does not require consent if usage stats were set by this user' do + allow(Gitlab::CurrentSettings).to receive(:usage_stats_set_by_user_id).and_return(user.id) + + expect(user.requires_usage_stats_consent?).to be false + end + end + + context 'in multi-user environment' do + before do + create(:user) + end + + it 'does not require consent' do + expect(user.requires_usage_stats_consent?).to be false + end + end + end + context 'with uploads' do it_behaves_like 'model with mounted uploader', false do let(:model_object) { create(:user, :with_avatar) } diff --git a/spec/requests/api/internal_spec.rb b/spec/requests/api/internal_spec.rb index 6890f46c724..e0b5b34f9c4 100644 --- a/spec/requests/api/internal_spec.rb +++ b/spec/requests/api/internal_spec.rb @@ -369,6 +369,26 @@ describe API::Internal do expect(user.reload.last_activity_on).to be_nil end end + + context 'when receive_max_input_size has been updated' do + it 'returns custom git config' do + allow(Gitlab::CurrentSettings).to receive(:receive_max_input_size) { 1 } + + push(key, project) + + expect(json_response["git_config_options"]).to be_present + end + end + + context 'when receive_max_input_size is empty' do + it 'returns an empty git config' do + allow(Gitlab::CurrentSettings).to receive(:receive_max_input_size) { nil } + + push(key, project) + + expect(json_response["git_config_options"]).to be_empty + end + end end end diff --git a/spec/requests/api/project_export_spec.rb b/spec/requests/api/project_export_spec.rb index 45e4e35d773..0586025956f 100644 --- a/spec/requests/api/project_export_spec.rb +++ b/spec/requests/api/project_export_spec.rb @@ -4,8 +4,8 @@ describe API::ProjectExport do set(:project) { create(:project) } set(:project_none) { create(:project) } set(:project_started) { create(:project) } - set(:project_finished) { create(:project) } - set(:project_after_export) { create(:project) } + let(:project_finished) { create(:project, :with_export) } + let(:project_after_export) { create(:project, :with_export) } set(:user) { create(:user) } set(:admin) { create(:admin) } @@ -29,13 +29,7 @@ describe API::ProjectExport do # simulate exporting work directory FileUtils.mkdir_p File.join(project_started.export_path, 'securerandom-hex') - # simulate exported - FileUtils.mkdir_p project_finished.export_path - FileUtils.touch File.join(project_finished.export_path, '_export.tar.gz') - # simulate in after export action - FileUtils.mkdir_p project_after_export.export_path - FileUtils.touch File.join(project_after_export.export_path, '_export.tar.gz') FileUtils.touch Gitlab::ImportExport::AfterExportStrategies::BaseAfterExportStrategy.lock_file_path(project_after_export) end @@ -191,14 +185,11 @@ describe API::ProjectExport do context 'when upload complete' do before do - FileUtils.rm_rf(project_after_export.export_path) - - if project_after_export.export_project_object_exists? - upload = project_after_export.import_export_upload + project_after_export.remove_exports + end - upload.remove_export_file! - upload.save - end + it 'has removed the export' do + expect(project_after_export.export_file_exists?).to be_falsey end it_behaves_like '404 response' do @@ -273,13 +264,13 @@ describe API::ProjectExport do before do stub_uploads_object_storage(ImportExportUploader) - [project, project_finished, project_after_export].each do |p| - p.add_maintainer(user) + project.add_maintainer(user) + project_finished.add_maintainer(user) + project_after_export.add_maintainer(user) - upload = ImportExportUpload.new(project: p) - upload.export_file = fixture_file_upload('spec/fixtures/project_export.tar.gz', "`/tar.gz") - upload.save! - end + upload = ImportExportUpload.new(project: project) + upload.export_file = fixture_file_upload('spec/fixtures/project_export.tar.gz', "`/tar.gz") + upload.save! end it_behaves_like 'get project download by strategy' diff --git a/spec/requests/api/project_import_spec.rb b/spec/requests/api/project_import_spec.rb index bc06f3c3732..c8fa4754810 100644 --- a/spec/requests/api/project_import_spec.rb +++ b/spec/requests/api/project_import_spec.rb @@ -7,7 +7,6 @@ describe API::ProjectImport do let(:namespace) { create(:group) } before do allow_any_instance_of(Gitlab::ImportExport).to receive(:storage_path).and_return(export_path) - stub_feature_flags(import_export_object_storage: true) stub_uploads_object_storage(FileUploader) namespace.add_owner(user) diff --git a/spec/requests/api/resource_label_events_spec.rb b/spec/requests/api/resource_label_events_spec.rb new file mode 100644 index 00000000000..b7d4a5152cc --- /dev/null +++ b/spec/requests/api/resource_label_events_spec.rb @@ -0,0 +1,75 @@ +# frozen_string_literal: true + +require 'spec_helper' + +describe API::ResourceLabelEvents do + set(:user) { create(:user) } + set(:project) { create(:project, :public, :repository, namespace: user.namespace) } + set(:private_user) { create(:user) } + + before do + project.add_developer(user) + end + + shared_examples 'resource_label_events API' do |parent_type, eventable_type, id_name| + describe "GET /#{parent_type}/:id/#{eventable_type}/:noteable_id/resource_label_events" do + it "returns an array of resource label events" do + get api("/#{parent_type}/#{parent.id}/#{eventable_type}/#{eventable[id_name]}/resource_label_events", user) + + expect(response).to have_gitlab_http_status(200) + expect(response).to include_pagination_headers + expect(json_response).to be_an Array + expect(json_response.first['id']).to eq(event.id) + end + + it "returns a 404 error when eventable id not found" do + get api("/#{parent_type}/#{parent.id}/#{eventable_type}/12345/resource_label_events", user) + + expect(response).to have_gitlab_http_status(404) + end + + it "returns 404 when not authorized" do + parent.update!(visibility_level: Gitlab::VisibilityLevel::PRIVATE) + + get api("/#{parent_type}/#{parent.id}/#{eventable_type}/#{eventable[id_name]}/resource_label_events", private_user) + + expect(response).to have_gitlab_http_status(404) + end + end + + describe "GET /#{parent_type}/:id/#{eventable_type}/:noteable_id/resource_label_events/:event_id" do + it "returns a resource label event by id" do + get api("/#{parent_type}/#{parent.id}/#{eventable_type}/#{eventable[id_name]}/resource_label_events/#{event.id}", user) + + expect(response).to have_gitlab_http_status(200) + expect(json_response['id']).to eq(event.id) + end + + it "returns a 404 error if resource label event not found" do + get api("/#{parent_type}/#{parent.id}/#{eventable_type}/#{eventable[id_name]}/resource_label_events/12345", user) + + expect(response).to have_gitlab_http_status(404) + end + end + end + + context 'when eventable is an Issue' do + let(:issue) { create(:issue, project: project, author: user) } + + it_behaves_like 'resource_label_events API', 'projects', 'issues', 'iid' do + let(:parent) { project } + let(:eventable) { issue } + let!(:event) { create(:resource_label_event, issue: issue) } + end + end + + context 'when eventable is a Merge Request' do + let(:merge_request) { create(:merge_request, source_project: project, target_project: project, author: user) } + + it_behaves_like 'resource_label_events API', 'projects', 'merge_requests', 'iid' do + let(:parent) { project } + let(:eventable) { merge_request } + let!(:event) { create(:resource_label_event, merge_request: merge_request) } + end + end +end diff --git a/spec/services/issuable/common_system_notes_service_spec.rb b/spec/services/issuable/common_system_notes_service_spec.rb index dcf4503ef9c..fa1a421d528 100644 --- a/spec/services/issuable/common_system_notes_service_spec.rb +++ b/spec/services/issuable/common_system_notes_service_spec.rb @@ -12,12 +12,21 @@ describe Issuable::CommonSystemNotesService do it_behaves_like 'system note creation', { time_estimate: 5 }, 'changed time estimate' context 'when new label is added' do + let(:label) { create(:label, project: project) } + before do - label = create(:label, project: project) issuable.labels << label + issuable.save end - it_behaves_like 'system note creation', {}, /added ~\w+ label/ + it 'creates a resource label event' do + described_class.new(project, user).execute(issuable, []) + event = issuable.reload.resource_label_events.last + + expect(event).not_to be_nil + expect(event.label_id).to eq label.id + expect(event.user_id).to eq user.id + end end context 'when new milestone is assigned' do diff --git a/spec/services/issues/move_service_spec.rb b/spec/services/issues/move_service_spec.rb index 609eef76d2c..b5767583952 100644 --- a/spec/services/issues/move_service_spec.rb +++ b/spec/services/issues/move_service_spec.rb @@ -122,6 +122,17 @@ describe Issues::MoveService do end end + context 'issue with resource label events' do + it 'assigns resource label events to new issue' do + old_issue.resource_label_events = create_list(:resource_label_event, 2, issue: old_issue) + + new_issue = move_service.execute(old_issue, new_project) + + expected = old_issue.resource_label_events.map(&:label_id) + expect(new_issue.resource_label_events.map(&:label_id)).to match_array(expected) + end + end + context 'generic issue' do include_context 'issue move executed' diff --git a/spec/services/issues/update_service_spec.rb b/spec/services/issues/update_service_spec.rb index 5bcfef46b75..07aa8449a66 100644 --- a/spec/services/issues/update_service_spec.rb +++ b/spec/services/issues/update_service_spec.rb @@ -189,11 +189,12 @@ describe Issues::UpdateService, :mailer do expect(note.note).to include "assigned to #{user2.to_reference}" end - it 'creates system note about issue label edit' do - note = find_note('added ~') + it 'creates a resource label event' do + event = issue.resource_label_events.last - expect(note).not_to be_nil - expect(note.note).to include "added #{label.to_reference} label" + expect(event).not_to be_nil + expect(event.label_id).to eq label.id + expect(event.user_id).to eq user.id end it 'creates system note about title change' do diff --git a/spec/services/merge_requests/reload_diffs_service_spec.rb b/spec/services/merge_requests/reload_diffs_service_spec.rb index a0a27d247fc..21f369a3818 100644 --- a/spec/services/merge_requests/reload_diffs_service_spec.rb +++ b/spec/services/merge_requests/reload_diffs_service_spec.rb @@ -57,6 +57,7 @@ describe MergeRequests::ReloadDiffsService, :use_clean_rails_memory_store_cachin expect(Rails.cache).to receive(:delete).with(old_cache_key).and_call_original expect(Rails.cache).to receive(:read).with(new_cache_key).and_call_original expect(Rails.cache).to receive(:write).with(new_cache_key, anything, anything).and_call_original + subject.execute end end diff --git a/spec/services/merge_requests/update_service_spec.rb b/spec/services/merge_requests/update_service_spec.rb index f0029af83cc..55dfab81c26 100644 --- a/spec/services/merge_requests/update_service_spec.rb +++ b/spec/services/merge_requests/update_service_spec.rb @@ -109,11 +109,12 @@ describe MergeRequests::UpdateService, :mailer do expect(note.note).to include "assigned to #{user2.to_reference}" end - it 'creates system note about merge_request label edit' do - note = find_note('added ~') + it 'creates a resource label event' do + event = merge_request.resource_label_events.last - expect(note).not_to be_nil - expect(note.note).to include "added #{label.to_reference} label" + expect(event).not_to be_nil + expect(event.label_id).to eq label.id + expect(event.user_id).to eq user.id end it 'creates system note about title change' do diff --git a/spec/services/projects/container_repository/destroy_service_spec.rb b/spec/services/projects/container_repository/destroy_service_spec.rb new file mode 100644 index 00000000000..307ccc88865 --- /dev/null +++ b/spec/services/projects/container_repository/destroy_service_spec.rb @@ -0,0 +1,40 @@ +# frozen_string_literal: true + +require 'spec_helper' + +describe Projects::ContainerRepository::DestroyService do + set(:user) { create(:user) } + set(:project) { create(:project, :private) } + + subject { described_class.new(project, user) } + + before do + stub_container_registry_config(enabled: true) + end + + context 'when user does not have access to registry' do + let!(:repository) { create(:container_repository, :root, project: project) } + + it 'does not delete a repository' do + expect { subject.execute(repository) }.not_to change { ContainerRepository.all.count } + end + end + + context 'when user has access to registry' do + before do + project.add_developer(user) + end + + context 'when root container repository exists' do + let!(:repository) { create(:container_repository, :root, project: project) } + + before do + stub_container_registry_tags(repository: :any, tags: []) + end + + it 'deletes the repository' do + expect { described_class.new(project, user).execute(repository) }.to change { ContainerRepository.all.count }.by(-1) + end + end + end +end diff --git a/spec/services/resource_events/change_labels_service_spec.rb b/spec/services/resource_events/change_labels_service_spec.rb index 41b0fb3eea3..4c9138fb1ef 100644 --- a/spec/services/resource_events/change_labels_service_spec.rb +++ b/spec/services/resource_events/change_labels_service_spec.rb @@ -18,6 +18,14 @@ describe ResourceEvents::ChangeLabelsService do expect(event.action).to eq(action) end + it 'expires resource note etag cache' do + expect_any_instance_of(Gitlab::EtagCaching::Store) + .to receive(:touch) + .with("/#{resource.project.namespace.to_param}/#{resource.project.to_param}/noteable/issue/#{resource.id}/notes") + + described_class.new(resource, author).execute(added_labels: [labels[0]]) + end + context 'when adding a label' do let(:added) { [labels[0]] } let(:removed) { [] } diff --git a/spec/services/resource_events/merge_into_notes_service_spec.rb b/spec/services/resource_events/merge_into_notes_service_spec.rb new file mode 100644 index 00000000000..0d333d541c9 --- /dev/null +++ b/spec/services/resource_events/merge_into_notes_service_spec.rb @@ -0,0 +1,70 @@ +# frozen_string_literal: true + +require 'spec_helper' + +describe ResourceEvents::MergeIntoNotesService do + def create_event(params) + event_params = { action: :add, label: label, issue: resource, + user: user } + + create(:resource_label_event, event_params.merge(params)) + end + + def create_note(params) + opts = { noteable: resource, project: project } + + create(:note_on_issue, opts.merge(params)) + end + + set(:project) { create(:project) } + set(:user) { create(:user) } + set(:resource) { create(:issue, project: project) } + set(:label) { create(:label, project: project) } + set(:label2) { create(:label, project: project) } + let(:time) { Time.now } + + describe '#execute' do + it 'merges label events into notes in order of created_at' do + note1 = create_note(created_at: 4.days.ago) + note2 = create_note(created_at: 2.days.ago) + event1 = create_event(created_at: 3.days.ago) + event2 = create_event(created_at: 1.day.ago) + + notes = described_class.new(resource, user).execute([note1, note2]) + + expected = [note1, event1, note2, event2].map(&:discussion_id) + expect(notes.map(&:discussion_id)).to eq expected + end + + it 'squashes events with same time and author into single note' do + user2 = create(:user) + + create_event(created_at: time) + create_event(created_at: time, label: label2, action: :remove) + create_event(created_at: time, user: user2) + create_event(created_at: 1.day.ago, label: label2) + + notes = described_class.new(resource, user).execute() + + expected = [ + "added #{label.to_reference} label and removed #{label2.to_reference} label", + "added #{label.to_reference} label", + "added #{label2.to_reference} label" + ] + + expect(notes.count).to eq 3 + expect(notes.map(&:note)).to match_array expected + end + + it 'fetches only notes created after last_fetched_at' do + create_event(created_at: 4.days.ago) + event = create_event(created_at: 1.day.ago) + + notes = described_class.new(resource, user, + last_fetched_at: 2.days.ago.to_i).execute() + + expect(notes.count).to eq 1 + expect(notes.first.discussion_id).to eq event.discussion_id + end + end +end diff --git a/spec/services/system_note_service_spec.rb b/spec/services/system_note_service_spec.rb index 48aad8ebdbe..d5d750e182b 100644 --- a/spec/services/system_note_service_spec.rb +++ b/spec/services/system_note_service_spec.rb @@ -197,45 +197,6 @@ describe SystemNoteService do end end - describe '.change_label' do - subject { described_class.change_label(noteable, project, author, added, removed) } - - let(:labels) { create_list(:label, 2, project: project) } - let(:added) { [] } - let(:removed) { [] } - - it_behaves_like 'a system note' do - let(:action) { 'label' } - end - - context 'with added labels' do - let(:added) { labels } - let(:removed) { [] } - - it 'sets the note text' do - expect(subject.note).to eq "added ~#{labels[0].id} ~#{labels[1].id} labels" - end - end - - context 'with removed labels' do - let(:added) { [] } - let(:removed) { labels } - - it 'sets the note text' do - expect(subject.note).to eq "removed ~#{labels[0].id} ~#{labels[1].id} labels" - end - end - - context 'with added and removed labels' do - let(:added) { [labels[0]] } - let(:removed) { [labels[1]] } - - it 'sets the note text' do - expect(subject.note).to eq "added ~#{labels[0].id} and removed ~#{labels[1].id} labels" - end - end - end - describe '.change_milestone' do context 'for a project milestone' do subject { described_class.change_milestone(noteable, project, author, milestone) } diff --git a/spec/services/wikis/create_attachment_service_spec.rb b/spec/services/wikis/create_attachment_service_spec.rb index 3f4da873ce4..f5899f292c8 100644 --- a/spec/services/wikis/create_attachment_service_spec.rb +++ b/spec/services/wikis/create_attachment_service_spec.rb @@ -88,8 +88,30 @@ describe Wikis::CreateAttachmentService do end end - describe 'validations' do + describe '#parse_file_name' do context 'when file_name' do + context 'has white spaces' do + let(:file_name) { 'file with spaces' } + + it "replaces all of them with '_'" do + result = service.execute + + expect(result[:status]).to eq :success + expect(result[:result][:file_name]).to eq 'file_with_spaces' + end + end + + context 'has other invalid characters' do + let(:file_name) { "file\twith\tinvalid chars" } + + it "replaces all of them with '_'" do + result = service.execute + + expect(result[:status]).to eq :success + expect(result[:result][:file_name]).to eq 'file_with_invalid_chars' + end + end + context 'is not present' do let(:file_name) { nil } diff --git a/spec/support/import_export/export_file_helper.rb b/spec/support/import_export/export_file_helper.rb index 4d925ac77f4..d9ed405baf4 100644 --- a/spec/support/import_export/export_file_helper.rb +++ b/spec/support/import_export/export_file_helper.rb @@ -52,7 +52,7 @@ module ExportFileHelper # Expands the compressed file for an exported project into +tmpdir+ def in_directory_with_expanded_export(project) Dir.mktmpdir do |tmpdir| - export_file = project.export_project_path + export_file = project.export_file.path _output, exit_status = Gitlab::Popen.popen(%W{tar -zxf #{export_file} -C #{tmpdir}}) yield(exit_status, tmpdir) diff --git a/spec/support/shared_examples/models/label_note_shared_examples.rb b/spec/support/shared_examples/models/label_note_shared_examples.rb new file mode 100644 index 00000000000..406385c13bd --- /dev/null +++ b/spec/support/shared_examples/models/label_note_shared_examples.rb @@ -0,0 +1,109 @@ +# frozen_string_literal: true + +shared_examples 'label note created from events' do + def create_event(params = {}) + event_params = { action: :add, label: label, user: user } + resource_key = resource.class.name.underscore.to_s + event_params[resource_key] = resource + + build(:resource_label_event, event_params.merge(params)) + end + + def label_refs(events) + labels = events.map(&:label).compact + + labels.map { |l| l.to_reference}.sort.join(' ') + end + + let(:time) { Time.now } + let(:local_label_ids) { [label.id, label2.id] } + + describe '.from_events' do + it 'returns system note with expected attributes' do + event = create_event + + note = described_class.from_events([event, create_event]) + + expect(note.system).to be true + expect(note.author_id).to eq event.user_id + expect(note.discussion_id).to eq event.discussion_id + expect(note.noteable).to eq event.issuable + expect(note.note).to be_present + expect(note.note_html).to be_present + end + + it 'updates markdown cache if reference is not set yet' do + event = create_event(reference: nil) + + described_class.from_events([event]) + + expect(event.reference).not_to be_nil + end + + it 'updates markdown cache if label was deleted' do + event = create_event(reference: 'some_ref', label: nil) + + described_class.from_events([event]) + + expect(event.reference).to eq '' + end + + it 'returns html note' do + events = [create_event(created_at: time)] + + note = described_class.from_events(events) + + expect(note.note_html).to include label.title + end + + it 'returns text note for added labels' do + events = [create_event(created_at: time), + create_event(created_at: time, label: label2), + create_event(created_at: time, label: nil)] + + note = described_class.from_events(events) + + expect(note.note).to eq "added #{label_refs(events)} + 1 deleted label" + end + + it 'returns text note for removed labels' do + events = [create_event(action: :remove, created_at: time), + create_event(action: :remove, created_at: time, label: label2), + create_event(action: :remove, created_at: time, label: nil)] + + note = described_class.from_events(events) + + expect(note.note).to eq "removed #{label_refs(events)} + 1 deleted label" + end + + it 'returns text note for added and removed labels' do + add_events = [create_event(created_at: time), + create_event(created_at: time, label: nil)] + + remove_events = [create_event(action: :remove, created_at: time), + create_event(action: :remove, created_at: time, label: nil)] + + note = described_class.from_events(add_events + remove_events) + + expect(note.note).to eq "added #{label_refs(add_events)} + 1 deleted label and removed #{label_refs(remove_events)} + 1 deleted label" + end + + it 'returns text note for cross-project label' do + other_label = create(:label) + event = create_event(label: other_label) + + note = described_class.from_events([event]) + + expect(note.note).to eq "added #{other_label.to_reference(resource_parent)} label" + end + + it 'returns text note for cross-group label' do + other_label = create(:group_label) + event = create_event(label: other_label) + + note = described_class.from_events([event]) + + expect(note.note).to eq "added #{other_label.to_reference(other_label.group, target_project: project, full: true)} label" + end + end +end diff --git a/spec/tasks/gitlab/cleanup_rake_spec.rb b/spec/tasks/gitlab/cleanup_rake_spec.rb index cc2cca10f58..19794227d9f 100644 --- a/spec/tasks/gitlab/cleanup_rake_spec.rb +++ b/spec/tasks/gitlab/cleanup_rake_spec.rb @@ -6,6 +6,8 @@ describe 'gitlab:cleanup rake tasks' do end describe 'cleanup namespaces and repos' do + let(:gitlab_shell) { Gitlab::Shell.new } + let(:storage) { storages.keys.first } let(:storages) do { 'default' => Gitlab::GitalyClient::StorageSettings.new(@default_storage_hash.merge('path' => 'tmp/tests/default_storage')) @@ -17,53 +19,56 @@ describe 'gitlab:cleanup rake tasks' do end before do - FileUtils.mkdir(Settings.absolute('tmp/tests/default_storage')) allow(Gitlab.config.repositories).to receive(:storages).and_return(storages) end after do - FileUtils.rm_rf(Settings.absolute('tmp/tests/default_storage')) + Gitlab::GitalyClient::StorageService.new(storage).delete_all_repositories end describe 'cleanup:repos' do before do - FileUtils.mkdir_p(Settings.absolute('tmp/tests/default_storage/broken/project.git')) - FileUtils.mkdir_p(Settings.absolute('tmp/tests/default_storage/@hashed/12/34/5678.git')) + gitlab_shell.add_namespace(storage, 'broken/project.git') + gitlab_shell.add_namespace(storage, '@hashed/12/34/5678.git') end it 'moves it to an orphaned path' do - run_rake_task('gitlab:cleanup:repos') - repo_list = Dir['tmp/tests/default_storage/broken/*'] + now = Time.now + + Timecop.freeze(now) do + run_rake_task('gitlab:cleanup:repos') + repo_list = Gitlab::GitalyClient::StorageService.new(storage).list_directories(depth: 0) - expect(repo_list.first).to include('+orphaned+') + expect(repo_list.last).to include("broken+orphaned+#{now.to_i}") + end end it 'ignores @hashed repos' do run_rake_task('gitlab:cleanup:repos') - expect(Dir.exist?(Settings.absolute('tmp/tests/default_storage/@hashed/12/34/5678.git'))).to be_truthy + expect(gitlab_shell.exists?(storage, '@hashed/12/34/5678.git')).to be(true) end end describe 'cleanup:dirs' do it 'removes missing namespaces' do - FileUtils.mkdir_p(Settings.absolute("tmp/tests/default_storage/namespace_1/project.git")) - FileUtils.mkdir_p(Settings.absolute("tmp/tests/default_storage/namespace_2/project.git")) - allow(Namespace).to receive(:pluck).and_return('namespace_1') + gitlab_shell.add_namespace(storage, "namespace_1/project.git") + gitlab_shell.add_namespace(storage, "namespace_2/project.git") + allow(Namespace).to receive(:pluck).and_return(['namespace_1']) stub_env('REMOVE', 'true') run_rake_task('gitlab:cleanup:dirs') - expect(Dir.exist?(Settings.absolute('tmp/tests/default_storage/namespace_1'))).to be_truthy - expect(Dir.exist?(Settings.absolute('tmp/tests/default_storage/namespace_2'))).to be_falsey + expect(gitlab_shell.exists?(storage, 'namespace_1')).to be(true) + expect(gitlab_shell.exists?(storage, 'namespace_2')).to be(false) end it 'ignores @hashed directory' do - FileUtils.mkdir_p(Settings.absolute('tmp/tests/default_storage/@hashed/12/34/5678.git')) + gitlab_shell.add_namespace(storage, '@hashed/12/34/5678.git') run_rake_task('gitlab:cleanup:dirs') - expect(Dir.exist?(Settings.absolute('tmp/tests/default_storage/@hashed/12/34/5678.git'))).to be_truthy + expect(gitlab_shell.exists?(storage, '@hashed/12/34/5678.git')).to be(true) end end end diff --git a/spec/uploaders/namespace_file_uploader_spec.rb b/spec/uploaders/namespace_file_uploader_spec.rb index 71fe2c353c0..eafbea07e10 100644 --- a/spec/uploaders/namespace_file_uploader_spec.rb +++ b/spec/uploaders/namespace_file_uploader_spec.rb @@ -26,6 +26,20 @@ describe NamespaceFileUploader do upload_path: IDENTIFIER end + context '.base_dir' do + it 'returns local storage base_dir without store param' do + expect(described_class.base_dir(group)).to eq("uploads/-/system/namespace/#{group.id}") + end + + it 'returns local storage base_dir when store param is Store::LOCAL' do + expect(described_class.base_dir(group, ObjectStorage::Store::LOCAL)).to eq("uploads/-/system/namespace/#{group.id}") + end + + it 'returns remote base_dir when store param is Store::REMOTE' do + expect(described_class.base_dir(group, ObjectStorage::Store::REMOTE)).to eq("namespace/#{group.id}") + end + end + describe "#migrate!" do before do uploader.store!(fixture_file_upload(File.join('spec/fixtures/doc_sample.txt'))) diff --git a/spec/workers/delete_container_repository_worker_spec.rb b/spec/workers/delete_container_repository_worker_spec.rb new file mode 100644 index 00000000000..8c40611a959 --- /dev/null +++ b/spec/workers/delete_container_repository_worker_spec.rb @@ -0,0 +1,33 @@ +# frozen_string_literal: true + +require 'spec_helper' + +describe DeleteContainerRepositoryWorker do + let(:registry) { create(:container_repository) } + let(:project) { registry.project } + let(:user) { project.owner } + + subject { described_class.new } + + describe '#perform' do + it 'executes the destroy service' do + service = instance_double(Projects::ContainerRepository::DestroyService) + expect(service).to receive(:execute) + expect(Projects::ContainerRepository::DestroyService).to receive(:new).with(project, user).and_return(service) + + subject.perform(user.id, registry.id) + end + + it 'does not raise error when user could not be found' do + expect do + subject.perform(-1, registry.id) + end.not_to raise_error + end + + it 'does not raise error when registry could not be found' do + expect do + subject.perform(user.id, -1) + end.not_to raise_error + end + end +end diff --git a/yarn.lock b/yarn.lock index 6f96e5ff228..0778c5214aa 100644 --- a/yarn.lock +++ b/yarn.lock @@ -78,13 +78,9 @@ lodash "^4.2.0" to-fast-properties "^2.0.0" -"@gitlab-org/gitlab-svgs@^1.23.0": - version "1.27.0" - resolved "https://registry.yarnpkg.com/@gitlab-org/gitlab-svgs/-/gitlab-svgs-1.27.0.tgz#638e70399ebd59e503732177316bb9a18bf7a13f" - -"@gitlab-org/gitlab-svgs@^1.28.0": - version "1.28.0" - resolved "https://registry.yarnpkg.com/@gitlab-org/gitlab-svgs/-/gitlab-svgs-1.28.0.tgz#f689dfd46504df0a75027d6dd4ea01a71cd46f88" +"@gitlab-org/gitlab-svgs@^1.23.0", "@gitlab-org/gitlab-svgs@^1.29.0": + version "1.29.0" + resolved "https://registry.yarnpkg.com/@gitlab-org/gitlab-svgs/-/gitlab-svgs-1.29.0.tgz#03b65b513f9099bbda6ecf94d673a2952f8c6c70" "@gitlab-org/gitlab-ui@1.0.5": version "1.0.5" @@ -307,11 +303,7 @@ acorn@^3.0.4: version "3.3.0" resolved "https://registry.yarnpkg.com/acorn/-/acorn-3.3.0.tgz#45e37fb39e8da3f25baee3ff5369e2bb5f22017a" -acorn@^5.0.0, acorn@^5.3.0, acorn@^5.5.0: - version "5.6.2" - resolved "https://registry.yarnpkg.com/acorn/-/acorn-5.6.2.tgz#b1da1d7be2ac1b4a327fb9eab851702c5045b4e7" - -acorn@^5.6.2: +acorn@^5.0.0, acorn@^5.3.0, acorn@^5.5.0, acorn@^5.6.2: version "5.7.1" resolved "https://registry.yarnpkg.com/acorn/-/acorn-5.7.1.tgz#f095829297706a7c9776958c0afc8930a9b9d9d8" @@ -553,13 +545,7 @@ async@1.x, async@^1.4.0, async@^1.5.2: version "1.5.2" resolved "https://registry.yarnpkg.com/async/-/async-1.5.2.tgz#ec6a61ae56480c0c3cb241c95618e20892f9672a" -async@^2.0.0, async@^2.1.4: - version "2.6.0" - resolved "https://registry.yarnpkg.com/async/-/async-2.6.0.tgz#61a29abb6fcc026fea77e56d1c6ec53a795951f4" - dependencies: - lodash "^4.14.0" - -async@~2.6.0: +async@^2.0.0, async@^2.1.4, async@~2.6.0: version "2.6.1" resolved "https://registry.yarnpkg.com/async/-/async-2.6.1.tgz#b245a23ca71930044ec53fa46aa00a3e87c6a610" dependencies: @@ -1380,14 +1366,10 @@ bootstrap-vue@^2.0.0-rc.11: popper.js "^1.12.9" vue-functional-data-merge "^2.0.5" -bootstrap@^4.1.1: +bootstrap@^4.1.1, bootstrap@~4.1.1: version "4.1.2" resolved "https://registry.yarnpkg.com/bootstrap/-/bootstrap-4.1.2.tgz#aee2a93472e61c471fc79fb475531dcbc87de326" -bootstrap@~4.1.1: - version "4.1.1" - resolved "https://registry.yarnpkg.com/bootstrap/-/bootstrap-4.1.1.tgz#3aec85000fa619085da8d2e4983dfd67cf2114cb" - boxen@^1.2.1: version "1.3.0" resolved "https://registry.yarnpkg.com/boxen/-/boxen-1.3.0.tgz#55c6c39a8ba58d9c61ad22cd877532deb665a20b" @@ -1681,25 +1663,7 @@ check-types@^7.3.0: version "7.3.0" resolved "https://registry.yarnpkg.com/check-types/-/check-types-7.3.0.tgz#468f571a4435c24248f5fd0cb0e8d87c3c341e7d" -chokidar@^2.0.0, chokidar@^2.0.2: - version "2.0.2" - resolved "https://registry.yarnpkg.com/chokidar/-/chokidar-2.0.2.tgz#4dc65139eeb2714977735b6a35d06e97b494dfd7" - dependencies: - anymatch "^2.0.0" - async-each "^1.0.0" - braces "^2.3.0" - glob-parent "^3.1.0" - inherits "^2.0.1" - is-binary-path "^1.0.0" - is-glob "^4.0.0" - normalize-path "^2.1.1" - path-is-absolute "^1.0.0" - readdirp "^2.0.0" - upath "^1.0.0" - optionalDependencies: - fsevents "^1.0.0" - -chokidar@^2.0.3: +chokidar@^2.0.0, chokidar@^2.0.2, chokidar@^2.0.3: version "2.0.4" resolved "https://registry.yarnpkg.com/chokidar/-/chokidar-2.0.4.tgz#356ff4e2b0e8e43e322d18a372460bbcf3accd26" dependencies: @@ -2784,15 +2748,7 @@ engine.io@~3.1.0: optionalDependencies: uws "~9.14.0" -enhanced-resolve@^4.0.0: - version "4.0.0" - resolved "https://registry.yarnpkg.com/enhanced-resolve/-/enhanced-resolve-4.0.0.tgz#e34a6eaa790f62fccd71d93959f56b2b432db10a" - dependencies: - graceful-fs "^4.1.2" - memory-fs "^0.4.0" - tapable "^1.0.0" - -enhanced-resolve@^4.1.0: +enhanced-resolve@^4.0.0, enhanced-resolve@^4.1.0: version "4.1.0" resolved "https://registry.yarnpkg.com/enhanced-resolve/-/enhanced-resolve-4.1.0.tgz#41c7e0bfdfe74ac1ffe1e57ad6a5c6c9f3742a7f" dependencies: @@ -2816,13 +2772,7 @@ entities@^1.1.1, entities@~1.1.1: version "1.1.1" resolved "https://registry.yarnpkg.com/entities/-/entities-1.1.1.tgz#6e5c2d0a5621b5dadaecef80b90edfb5cd7772f0" -errno@^0.1.3: - version "0.1.4" - resolved "https://registry.yarnpkg.com/errno/-/errno-0.1.4.tgz#b896e23a9e5e8ba33871fc996abd3635fc9a1c7d" - dependencies: - prr "~0.0.0" - -errno@^0.1.4: +errno@^0.1.3, errno@^0.1.4: version "0.1.7" resolved "https://registry.yarnpkg.com/errno/-/errno-0.1.7.tgz#4684d71779ad39af177e3f007996f7c67c852618" dependencies: @@ -3470,7 +3420,7 @@ fs.realpath@^1.0.0: version "1.0.0" resolved "https://registry.yarnpkg.com/fs.realpath/-/fs.realpath-1.0.0.tgz#1504ad2523158caa40db4a2787cb01411994ea4f" -fsevents@^1.0.0, fsevents@^1.2.2: +fsevents@^1.2.2: version "1.2.4" resolved "https://registry.yarnpkg.com/fsevents/-/fsevents-1.2.4.tgz#f41dcb1af2582af3692da36fc55cbd8e1041c426" dependencies: @@ -4490,11 +4440,7 @@ istanbul-api@^1.1.14: mkdirp "^0.5.1" once "^1.4.0" -istanbul-lib-coverage@^1.1.1: - version "1.1.1" - resolved "https://registry.yarnpkg.com/istanbul-lib-coverage/-/istanbul-lib-coverage-1.1.1.tgz#73bfb998885299415c93d38a3e9adf784a77a9da" - -istanbul-lib-coverage@^1.2.0: +istanbul-lib-coverage@^1.1.1, istanbul-lib-coverage@^1.2.0: version "1.2.0" resolved "https://registry.yarnpkg.com/istanbul-lib-coverage/-/istanbul-lib-coverage-1.2.0.tgz#f7d8f2e42b97e37fe796114cb0f9d68b5e3a4341" @@ -4945,7 +4891,7 @@ lodash@4.17.4: version "4.17.4" resolved "https://registry.yarnpkg.com/lodash/-/lodash-4.17.4.tgz#78203a4d1c328ae1d86dca6460e369b57f4055ae" -lodash@^4.0.0, lodash@^4.11.1, lodash@^4.14.0, lodash@^4.15.0, lodash@^4.17.10, lodash@^4.17.4, lodash@^4.17.5, lodash@^4.2.0, lodash@^4.3.0, lodash@^4.5.0: +lodash@^4.0.0, lodash@^4.11.1, lodash@^4.15.0, lodash@^4.17.10, lodash@^4.17.4, lodash@^4.17.5, lodash@^4.2.0, lodash@^4.3.0, lodash@^4.5.0: version "4.17.10" resolved "https://registry.yarnpkg.com/lodash/-/lodash-4.17.10.tgz#1b7793cf7259ea38fb3661d4d38b3260af8ae4e7" @@ -6003,15 +5949,7 @@ postcss-value-parser@^3.3.0: version "3.3.0" resolved "https://registry.yarnpkg.com/postcss-value-parser/-/postcss-value-parser-3.3.0.tgz#87f38f9f18f774a4ab4c8a232f5c5ce8872a9d15" -postcss@^6.0.1, postcss@^6.0.14, postcss@^6.0.20: - version "6.0.22" - resolved "https://registry.yarnpkg.com/postcss/-/postcss-6.0.22.tgz#e23b78314905c3b90cbd61702121e7a78848f2a3" - dependencies: - chalk "^2.4.1" - source-map "^0.6.1" - supports-color "^5.4.0" - -postcss@^6.0.23: +postcss@^6.0.1, postcss@^6.0.14, postcss@^6.0.20, postcss@^6.0.23: version "6.0.23" resolved "https://registry.yarnpkg.com/postcss/-/postcss-6.0.23.tgz#61c82cc328ac60e677645f979054eb98bc0e3324" dependencies: @@ -6095,10 +6033,6 @@ proxy-from-env@^1.0.0: version "1.0.0" resolved "https://registry.yarnpkg.com/proxy-from-env/-/proxy-from-env-1.0.0.tgz#33c50398f70ea7eb96d21f7b817630a55791c7ee" -prr@~0.0.0: - version "0.0.0" - resolved "https://registry.yarnpkg.com/prr/-/prr-0.0.0.tgz#1a84b85908325501411853d0081ee3fa86e2926a" - prr@~1.0.1: version "1.0.1" resolved "https://registry.yarnpkg.com/prr/-/prr-1.0.1.tgz#d3fc114ba06995a45ec6893f484ceb1d78f5f476" @@ -6276,16 +6210,16 @@ read-pkg@^2.0.0: normalize-package-data "^2.3.2" path-type "^2.0.0" -"readable-stream@1 || 2", readable-stream@2, readable-stream@^2.0.0, readable-stream@^2.0.1, readable-stream@^2.0.2, readable-stream@^2.0.4, readable-stream@^2.0.6, readable-stream@^2.1.5, readable-stream@^2.2.2, readable-stream@^2.2.9, readable-stream@^2.3.0, readable-stream@^2.3.3: - version "2.3.4" - resolved "https://registry.yarnpkg.com/readable-stream/-/readable-stream-2.3.4.tgz#c946c3f47fa7d8eabc0b6150f4a12f69a4574071" +"readable-stream@1 || 2", readable-stream@2, readable-stream@^2.0.0, readable-stream@^2.0.1, readable-stream@^2.0.2, readable-stream@^2.0.4, readable-stream@^2.0.6, readable-stream@^2.1.5, readable-stream@^2.2.2, readable-stream@^2.2.9, readable-stream@^2.3.0, readable-stream@^2.3.3, readable-stream@^2.3.6: + version "2.3.6" + resolved "https://registry.yarnpkg.com/readable-stream/-/readable-stream-2.3.6.tgz#b11c27d88b8ff1fbe070643cf94b0c79ae1b0aaf" dependencies: core-util-is "~1.0.0" inherits "~2.0.3" isarray "~1.0.0" process-nextick-args "~2.0.0" safe-buffer "~5.1.1" - string_decoder "~1.0.3" + string_decoder "~1.1.1" util-deprecate "~1.0.1" readable-stream@1.1.x, "readable-stream@1.x >=1.1.9": @@ -6297,18 +6231,6 @@ readable-stream@1.1.x, "readable-stream@1.x >=1.1.9": isarray "0.0.1" string_decoder "~0.10.x" -readable-stream@^2.3.6: - version "2.3.6" - resolved "https://registry.yarnpkg.com/readable-stream/-/readable-stream-2.3.6.tgz#b11c27d88b8ff1fbe070643cf94b0c79ae1b0aaf" - dependencies: - core-util-is "~1.0.0" - inherits "~2.0.3" - isarray "~1.0.0" - process-nextick-args "~2.0.0" - safe-buffer "~5.1.1" - string_decoder "~1.1.1" - util-deprecate "~1.0.1" - readable-stream@~2.0.5, readable-stream@~2.0.6: version "2.0.6" resolved "https://registry.yarnpkg.com/readable-stream/-/readable-stream-2.0.6.tgz#8f90341e68a53ccc928788dacfcd11b36eb9b78e" @@ -7021,14 +6943,10 @@ source-map@^0.4.4: dependencies: amdefine ">=0.0.4" -source-map@^0.5.0, source-map@^0.5.7, source-map@~0.5.6: +source-map@^0.5.0, source-map@^0.5.3, source-map@^0.5.6, source-map@^0.5.7, source-map@~0.5.1, source-map@~0.5.6: version "0.5.7" resolved "https://registry.yarnpkg.com/source-map/-/source-map-0.5.7.tgz#8a039d2d1021d22d1ea14c80d8ea468ba2ef3fcc" -source-map@^0.5.3, source-map@^0.5.6, source-map@~0.5.1: - version "0.5.6" - resolved "https://registry.yarnpkg.com/source-map/-/source-map-0.5.6.tgz#75ce38f52bf0733c5a7f0c118d81334a2bb5f412" - source-map@^0.6.1, source-map@~0.6.1: version "0.6.1" resolved "https://registry.yarnpkg.com/source-map/-/source-map-0.6.1.tgz#74722af32e9614e9c287a8d0bbde48b5e2f1a263" @@ -7130,11 +7048,7 @@ static-extend@^0.1.1: define-property "^0.2.5" object-copy "^0.1.0" -"statuses@>= 1.3.1 < 2": - version "1.4.0" - resolved "https://registry.yarnpkg.com/statuses/-/statuses-1.4.0.tgz#bb73d446da2796106efcc1b601a253d6c46bd087" - -"statuses@>= 1.4.0 < 2": +"statuses@>= 1.3.1 < 2", "statuses@>= 1.4.0 < 2": version "1.5.0" resolved "https://registry.yarnpkg.com/statuses/-/statuses-1.5.0.tgz#161c7dac177659fd9811f43771fa99381478628c" @@ -7218,12 +7132,6 @@ string_decoder@~0.10.x: version "0.10.31" resolved "https://registry.yarnpkg.com/string_decoder/-/string_decoder-0.10.31.tgz#62e203bc41766c6c28c9fc84301dab1c5310fa94" -string_decoder@~1.0.3: - version "1.0.3" - resolved "https://registry.yarnpkg.com/string_decoder/-/string_decoder-1.0.3.tgz#0fc67d7c141825de94282dd536bec6b9bce860ab" - dependencies: - safe-buffer "~5.1.0" - stringstream@~0.0.4, stringstream@~0.0.5: version "0.0.5" resolved "https://registry.yarnpkg.com/stringstream/-/stringstream-0.0.5.tgz#4e484cd4de5a0bbbee18e46307710a8a81621878" @@ -7621,10 +7529,6 @@ unzip-response@^2.0.1: version "2.0.1" resolved "https://registry.yarnpkg.com/unzip-response/-/unzip-response-2.0.1.tgz#d2f0f737d16b0615e72a6935ed04214572d56f97" -upath@^1.0.0: - version "1.0.5" - resolved "https://registry.yarnpkg.com/upath/-/upath-1.0.5.tgz#02cab9ecebe95bbec6d5fc2566325725ab6d1a73" - upath@^1.0.5: version "1.1.0" resolved "https://registry.yarnpkg.com/upath/-/upath-1.1.0.tgz#35256597e46a581db4793d0ce47fa9aebfc9fabd" @@ -7744,11 +7648,7 @@ validate-npm-package-license@^3.0.1: spdx-correct "~1.0.0" spdx-expression-parse "~1.0.0" -vary@~1.1.1: - version "1.1.1" - resolved "https://registry.yarnpkg.com/vary/-/vary-1.1.1.tgz#67535ebb694c1d52257457984665323f587e8d37" - -vary@~1.1.2: +vary@~1.1.1, vary@~1.1.2: version "1.1.2" resolved "https://registry.yarnpkg.com/vary/-/vary-1.1.2.tgz#2299f02c6ded30d4a5961b0b9f74524a18f634fc" |