diff options
75 files changed, 875 insertions, 877 deletions
diff --git a/app/assets/javascripts/clusters.js b/app/assets/javascripts/clusters.js index 50dbeb06362..180aa30e98c 100644 --- a/app/assets/javascripts/clusters.js +++ b/app/assets/javascripts/clusters.js @@ -3,7 +3,8 @@ import Visibility from 'visibilityjs'; import axios from 'axios'; import Poll from './lib/utils/poll'; import { s__ } from './locale'; -import './flash'; +import initSettingsPanels from './settings_panels'; +import Flash from './flash'; /** * Cluster page has 2 separate parts: @@ -24,6 +25,8 @@ class ClusterService { export default class Clusters { constructor() { + initSettingsPanels(); + const dataset = document.querySelector('.js-edit-cluster-form').dataset; this.state = { diff --git a/app/assets/javascripts/issue_show/components/app.vue b/app/assets/javascripts/issue_show/components/app.vue index eecb56cb185..d1aa83ea57f 100644 --- a/app/assets/javascripts/issue_show/components/app.vue +++ b/app/assets/javascripts/issue_show/components/app.vue @@ -24,6 +24,11 @@ export default { required: true, type: Boolean, }, + showInlineEditButton: { + type: Boolean, + required: false, + default: false, + }, issuableRef: { type: String, required: true, @@ -222,20 +227,25 @@ export default { <div v-else> <title-component :issuable-ref="issuableRef" + :can-update="canUpdate" :title-html="state.titleHtml" - :title-text="state.titleText" /> + :title-text="state.titleText" + :show-inline-edit-button="showInlineEditButton" + /> <description-component v-if="state.descriptionHtml" :can-update="canUpdate" :description-html="state.descriptionHtml" :description-text="state.descriptionText" :updated-at="state.updatedAt" - :task-status="state.taskStatus" /> + :task-status="state.taskStatus" + /> <edited-component v-if="hasUpdated" :updated-at="state.updatedAt" :updated-by-name="state.updatedByName" - :updated-by-path="state.updatedByPath" /> + :updated-by-path="state.updatedByPath" + /> </div> </div> </template> diff --git a/app/assets/javascripts/issue_show/components/title.vue b/app/assets/javascripts/issue_show/components/title.vue index a9dabd4cff1..00002709ac6 100644 --- a/app/assets/javascripts/issue_show/components/title.vue +++ b/app/assets/javascripts/issue_show/components/title.vue @@ -1,5 +1,8 @@ <script> import animateMixin from '../mixins/animate'; + import eventHub from '../event_hub'; + import tooltip from '../../vue_shared/directives/tooltip'; + import { spriteIcon } from '../../lib/utils/common_utils'; export default { mixins: [animateMixin], @@ -15,6 +18,11 @@ type: String, required: true, }, + canUpdate: { + required: false, + type: Boolean, + default: false, + }, titleHtml: { type: String, required: true, @@ -23,6 +31,14 @@ type: String, required: true, }, + showInlineEditButton: { + type: Boolean, + required: false, + default: false, + }, + }, + directives: { + tooltip, }, watch: { titleHtml() { @@ -30,24 +46,46 @@ this.animateChange(); }, }, + computed: { + pencilIcon() { + return spriteIcon('pencil', 'link-highlight'); + }, + }, methods: { setPageTitle() { const currentPageTitleScope = this.titleEl.innerText.split('·'); currentPageTitleScope[0] = `${this.titleText} (${this.issuableRef}) `; this.titleEl.textContent = currentPageTitleScope.join('·'); }, + edit() { + eventHub.$emit('open.form'); + }, }, }; </script> <template> - <h2 - class="title" - :class="{ - 'issue-realtime-pre-pulse': preAnimation, - 'issue-realtime-trigger-pulse': pulseAnimation - }" - v-html="titleHtml" - > - </h2> + <div class="title-container"> + <h2 + class="title" + :class="{ + 'issue-realtime-pre-pulse': preAnimation, + 'issue-realtime-trigger-pulse': pulseAnimation + }" + v-html="titleHtml" + > + </h2> + <button + v-tooltip + v-if="showInlineEditButton && canUpdate" + type="button" + class="btn-blank btn-edit note-action-button" + v-html="pencilIcon" + title="Edit title and description" + data-placement="bottom" + data-container="body" + @click="edit" + > + </button> + </div> </template> diff --git a/app/assets/javascripts/registry/stores/mutations.js b/app/assets/javascripts/registry/stores/mutations.js index e40382e7afc..208c3c39866 100644 --- a/app/assets/javascripts/registry/stores/mutations.js +++ b/app/assets/javascripts/registry/stores/mutations.js @@ -38,7 +38,7 @@ export default { tag: element.name, revision: element.revision, shortRevision: element.short_revision, - size: element.size, + size: element.total_size, layers: element.layers, location: element.location, createdAt: element.created_at, diff --git a/app/assets/javascripts/repo/components/repo.vue b/app/assets/javascripts/repo/components/repo.vue index cc60aa5939c..0a89a9f16cb 100644 --- a/app/assets/javascripts/repo/components/repo.vue +++ b/app/assets/javascripts/repo/components/repo.vue @@ -11,7 +11,9 @@ import Helper from '../helpers/repo_helper'; import MonacoLoaderHelper from '../helpers/monaco_loader_helper'; export default { - data: () => Store, + data() { + return Store; + }, mixins: [RepoMixin], components: { RepoSidebar, diff --git a/app/assets/javascripts/repo/components/repo_commit_section.vue b/app/assets/javascripts/repo/components/repo_commit_section.vue index c0dc4c8cd8b..185cd90ac06 100644 --- a/app/assets/javascripts/repo/components/repo_commit_section.vue +++ b/app/assets/javascripts/repo/components/repo_commit_section.vue @@ -9,7 +9,9 @@ import { visitUrl } from '../../lib/utils/url_utility'; export default { mixins: [RepoMixin], - data: () => Store, + data() { + return Store; + }, components: { PopupDialog, diff --git a/app/assets/javascripts/repo/components/repo_edit_button.vue b/app/assets/javascripts/repo/components/repo_edit_button.vue index 353142edeb7..e6e8b2e5205 100644 --- a/app/assets/javascripts/repo/components/repo_edit_button.vue +++ b/app/assets/javascripts/repo/components/repo_edit_button.vue @@ -3,7 +3,9 @@ import Store from '../stores/repo_store'; import RepoMixin from '../mixins/repo_mixin'; export default { - data: () => Store, + data() { + return Store; + }, mixins: [RepoMixin], computed: { buttonLabel() { diff --git a/app/assets/javascripts/repo/components/repo_editor.vue b/app/assets/javascripts/repo/components/repo_editor.vue index 2c597a3cd65..4639bee6d66 100644 --- a/app/assets/javascripts/repo/components/repo_editor.vue +++ b/app/assets/javascripts/repo/components/repo_editor.vue @@ -5,7 +5,9 @@ import Service from '../services/repo_service'; import Helper from '../helpers/repo_helper'; const RepoEditor = { - data: () => Store, + data() { + return Store; + }, destroyed() { if (Helper.monacoInstance) { @@ -93,7 +95,7 @@ const RepoEditor = { }, blobRaw() { - if (Helper.monacoInstance && !this.isTree) { + if (Helper.monacoInstance) { this.setupEditor(); } }, diff --git a/app/assets/javascripts/repo/components/repo_file.vue b/app/assets/javascripts/repo/components/repo_file.vue index 8b9cbd23456..c7e69340f17 100644 --- a/app/assets/javascripts/repo/components/repo_file.vue +++ b/app/assets/javascripts/repo/components/repo_file.vue @@ -1,107 +1,78 @@ <script> -import TimeAgoMixin from '../../vue_shared/mixins/timeago'; + import timeAgoMixin from '../../vue_shared/mixins/timeago'; + import eventHub from '../event_hub'; + import repoMixin from '../mixins/repo_mixin'; -const RepoFile = { - mixins: [TimeAgoMixin], - props: { - file: { - type: Object, - required: true, + export default { + mixins: [ + repoMixin, + timeAgoMixin, + ], + props: { + file: { + type: Object, + required: true, + }, }, - isMini: { - type: Boolean, - required: false, - default: false, + computed: { + fileIcon() { + const classObj = { + 'fa-spinner fa-spin': this.file.loading, + [this.file.icon]: !this.file.loading, + 'fa-folder-open': !this.file.loading && this.file.opened, + }; + return classObj; + }, + levelIndentation() { + return { + marginLeft: `${this.file.level * 16}px`, + }; + }, }, - loading: { - type: Object, - required: false, - default() { return { tree: false }; }, + methods: { + linkClicked(file) { + eventHub.$emit('fileNameClicked', file); + }, }, - hasFiles: { - type: Boolean, - required: false, - default: false, - }, - activeFile: { - type: Object, - required: true, - }, - }, - - computed: { - canShowFile() { - return !this.loading.tree || this.hasFiles; - }, - - fileIcon() { - const classObj = { - 'fa-spinner fa-spin': this.file.loading, - [this.file.icon]: !this.file.loading, - }; - return classObj; - }, - - fileIndentation() { - return { - 'margin-left': `${this.file.level * 10}px`, - }; - }, - - activeFileClass() { - return { - active: this.activeFile.url === this.file.url, - }; - }, - }, - - methods: { - linkClicked(file) { - this.$emit('linkclicked', file); - }, - }, -}; - -export default RepoFile; + }; </script> <template> -<tr - v-if="canShowFile" - class="file" - :class="activeFileClass" - @click.prevent="linkClicked(file)"> - <td> - <i - class="fa fa-fw file-icon" - :class="fileIcon" - :style="fileIndentation" - aria-label="file icon"> - </i> - <a - :href="file.url" - class="repo-file-name" - :title="file.url"> - {{file.name}} - </a> - </td> + <tr + class="file" + @click.prevent="linkClicked(file)"> + <td> + <i + class="fa fa-fw file-icon" + :class="fileIcon" + :style="levelIndentation" + aria-hidden="true" + > + </i> + <a + :href="file.url" + class="repo-file-name" + > + {{ file.name }} + </a> + </td> - <template v-if="!isMini"> - <td class="hidden-sm hidden-xs"> - <div class="commit-message"> - <a @click.stop :href="file.lastCommitUrl"> - {{file.lastCommitMessage}} + <template v-if="!isMini"> + <td class="hidden-sm hidden-xs"> + <a + @click.stop + :href="file.lastCommit.url" + class="commit-message" + > + {{ file.lastCommit.message }} </a> - </div> - </td> + </td> - <td class="hidden-xs text-right"> - <span - class="commit-update" - :title="tooltipTitle(file.lastCommitUpdate)"> - {{timeFormated(file.lastCommitUpdate)}} - </span> - </td> - </template> -</tr> + <td class="commit-update hidden-xs text-right"> + <span :title="tooltipTitle(file.lastCommit.updatedAt)"> + {{ timeFormated(file.lastCommit.updatedAt) }} + </span> + </td> + </template> + </tr> </template> diff --git a/app/assets/javascripts/repo/components/repo_file_buttons.vue b/app/assets/javascripts/repo/components/repo_file_buttons.vue index e43ef366f47..03cd219e718 100644 --- a/app/assets/javascripts/repo/components/repo_file_buttons.vue +++ b/app/assets/javascripts/repo/components/repo_file_buttons.vue @@ -4,7 +4,9 @@ import Helper from '../helpers/repo_helper'; import RepoMixin from '../mixins/repo_mixin'; const RepoFileButtons = { - data: () => Store, + data() { + return Store; + }, mixins: [RepoMixin], diff --git a/app/assets/javascripts/repo/components/repo_file_options.vue b/app/assets/javascripts/repo/components/repo_file_options.vue deleted file mode 100644 index 6a15755f029..00000000000 --- a/app/assets/javascripts/repo/components/repo_file_options.vue +++ /dev/null @@ -1,25 +0,0 @@ -<script> -const RepoFileOptions = { - props: { - isMini: { - type: Boolean, - required: false, - default: false, - }, - projectName: { - type: String, - required: true, - }, - }, -}; - -export default RepoFileOptions; -</script> - -<template> - <tr v-if="isMini" class="repo-file-options"> - <td> - <span class="title">{{projectName}}</span> - </td> - </tr> -</template> diff --git a/app/assets/javascripts/repo/components/repo_loading_file.vue b/app/assets/javascripts/repo/components/repo_loading_file.vue index bc8c64c8362..832b45b2b29 100644 --- a/app/assets/javascripts/repo/components/repo_loading_file.vue +++ b/app/assets/javascripts/repo/components/repo_loading_file.vue @@ -1,43 +1,23 @@ <script> -const RepoLoadingFile = { - props: { - loading: { - type: Object, - required: false, - default: {}, - }, - hasFiles: { - type: Boolean, - required: false, - default: false, - }, - isMini: { - type: Boolean, - required: false, - default: false, - }, - }, - - computed: { - showGhostLines() { - return this.loading.tree && !this.hasFiles; - }, - }, + import repoMixin from '../mixins/repo_mixin'; - methods: { - lineOfCode(n) { - return `skeleton-line-${n}`; + export default { + mixins: [ + repoMixin, + ], + methods: { + lineOfCode(n) { + return `skeleton-line-${n}`; + }, }, - }, -}; - -export default RepoLoadingFile; + }; </script> <template> <tr - v-if="showGhostLines" - class="loading-file"> + class="loading-file" + aria-label="Loading files" + > <td> <div class="animation-container animation-container-small"> @@ -48,29 +28,28 @@ export default RepoLoadingFile; </div> </div> </td> - - <td - v-if="!isMini" - class="hidden-sm hidden-xs"> - <div class="animation-container"> - <div - v-for="n in 6" - :key="n" - :class="lineOfCode(n)"> + <template v-if="!isMini"> + <td + class="hidden-sm hidden-xs"> + <div class="animation-container"> + <div + v-for="n in 6" + :key="n" + :class="lineOfCode(n)"> + </div> </div> - </div> - </td> + </td> - <td - v-if="!isMini" - class="hidden-xs"> - <div class="animation-container animation-container-small"> - <div - v-for="n in 6" - :key="n" - :class="lineOfCode(n)"> + <td + class="hidden-xs"> + <div class="animation-container animation-container-small animation-container-right"> + <div + v-for="n in 6" + :key="n" + :class="lineOfCode(n)"> + </div> </div> - </div> - </td> + </td> + </template> </tr> </template> diff --git a/app/assets/javascripts/repo/components/repo_prev_directory.vue b/app/assets/javascripts/repo/components/repo_prev_directory.vue index bbdbdc61e38..c4bf6dcdec2 100644 --- a/app/assets/javascripts/repo/components/repo_prev_directory.vue +++ b/app/assets/javascripts/repo/components/repo_prev_directory.vue @@ -1,38 +1,38 @@ <script> -import RepoMixin from '../mixins/repo_mixin'; + import eventHub from '../event_hub'; + import repoMixin from '../mixins/repo_mixin'; -const RepoPreviousDirectory = { - props: { - prevUrl: { - type: String, - required: true, + export default { + mixins: [ + repoMixin, + ], + props: { + prevUrl: { + type: String, + required: true, + }, }, - }, - - mixins: [RepoMixin], - - computed: { - colSpanCondition() { - return this.isMini ? undefined : 3; + computed: { + colSpanCondition() { + return this.isMini ? undefined : 3; + }, }, - }, - - methods: { - linkClicked(file) { - this.$emit('linkclicked', file); + methods: { + linkClicked(file) { + eventHub.$emit('goToPreviousDirectoryClicked', file); + }, }, - }, -}; - -export default RepoPreviousDirectory; + }; </script> <template> -<tr class="prev-directory"> - <td - :colspan="colSpanCondition" - @click.prevent="linkClicked(prevUrl)"> - <a :href="prevUrl">..</a> - </td> -</tr> + <tr class="file prev-directory"> + <td + :colspan="colSpanCondition" + class="table-cell" + @click.prevent="linkClicked(prevUrl)" + > + <a :href="prevUrl">...</a> + </td> + </tr> </template> diff --git a/app/assets/javascripts/repo/components/repo_preview.vue b/app/assets/javascripts/repo/components/repo_preview.vue index a87bef6084a..b5be771d539 100644 --- a/app/assets/javascripts/repo/components/repo_preview.vue +++ b/app/assets/javascripts/repo/components/repo_preview.vue @@ -4,7 +4,9 @@ import Store from '../stores/repo_store'; export default { - data: () => Store, + data() { + return Store; + }, computed: { html() { return this.activeFile.html; diff --git a/app/assets/javascripts/repo/components/repo_sidebar.vue b/app/assets/javascripts/repo/components/repo_sidebar.vue index e0f3c33003a..5832e603907 100644 --- a/app/assets/javascripts/repo/components/repo_sidebar.vue +++ b/app/assets/javascripts/repo/components/repo_sidebar.vue @@ -1,9 +1,10 @@ <script> +import _ from 'underscore'; import Service from '../services/repo_service'; import Helper from '../helpers/repo_helper'; import Store from '../stores/repo_store'; +import eventHub from '../event_hub'; import RepoPreviousDirectory from './repo_prev_directory.vue'; -import RepoFileOptions from './repo_file_options.vue'; import RepoFile from './repo_file.vue'; import RepoLoadingFile from './repo_loading_file.vue'; import RepoMixin from '../mixins/repo_mixin'; @@ -11,21 +12,35 @@ import RepoMixin from '../mixins/repo_mixin'; export default { mixins: [RepoMixin], components: { - 'repo-file-options': RepoFileOptions, 'repo-previous-directory': RepoPreviousDirectory, 'repo-file': RepoFile, 'repo-loading-file': RepoLoadingFile, }, - created() { window.addEventListener('popstate', this.checkHistory); }, destroyed() { + eventHub.$off('fileNameClicked', this.fileClicked); + eventHub.$off('goToPreviousDirectoryClicked', this.goToPreviousDirectoryClicked); window.removeEventListener('popstate', this.checkHistory); }, + mounted() { + eventHub.$on('fileNameClicked', this.fileClicked); + eventHub.$on('goToPreviousDirectoryClicked', this.goToPreviousDirectoryClicked); + }, + data() { + return Store; + }, + computed: { + flattendFiles() { + const mapFiles = arr => (!arr.files.length ? [] : _.map(arr.files, a => [a, mapFiles(a)])); - data: () => Store, - + return _.chain(this.files) + .map(arr => [arr, mapFiles(arr)]) + .flatten() + .value(); + }, + }, methods: { checkHistory() { let selectedFile = this.files.find(file => location.pathname.indexOf(file.url) > -1); @@ -52,21 +67,21 @@ export default { }, fileClicked(clickedFile, lineNumber) { - let file = clickedFile; + const file = clickedFile; + if (file.loading) return; - file.loading = true; if (file.type === 'tree' && file.opened) { - file = Store.removeChildFilesOfTree(file); - file.loading = false; + Helper.setDirectoryToClosed(file); Store.setActiveLine(lineNumber); } else { const openFile = Helper.getFileFromPath(file.url); + if (openFile) { - file.loading = false; Store.setActiveFiles(openFile); Store.setActiveLine(lineNumber); } else { + file.loading = true; Service.url = file.url; Helper.getContent(file) .then(() => { @@ -81,7 +96,7 @@ export default { goToPreviousDirectoryClicked(prevURL) { Service.url = prevURL; - Helper.getContent(null) + Helper.getContent(null, true) .then(() => Helper.scrollTabsRight()) .catch(Helper.loadingError); }, @@ -92,38 +107,43 @@ export default { <template> <div id="sidebar" :class="{'sidebar-mini' : isMini}"> <table class="table"> - <thead v-if="!isMini"> + <thead> <tr> - <th class="name">Name</th> - <th class="hidden-sm hidden-xs last-commit">Last commit</th> - <th class="hidden-xs last-update text-right">Last update</th> + <th + v-if="isMini" + class="repo-file-options title" + > + <strong class="clgray"> + {{ projectName }} + </strong> + </th> + <template v-else> + <th class="name"> + Name + </th> + <th class="hidden-sm hidden-xs last-commit"> + Last commit + </th> + <th class="hidden-xs last-update text-right"> + Last update + </th> + </template> </tr> </thead> <tbody> - <repo-file-options - :is-mini="isMini" - :project-name="projectName" - /> <repo-previous-directory - v-if="isRoot" + v-if="!isRoot && !loading.tree" :prev-url="prevURL" - @linkclicked="goToPreviousDirectoryClicked(prevURL)"/> + /> <repo-loading-file + v-if="!flattendFiles.length && loading.tree" v-for="n in 5" :key="n" - :loading="loading" - :has-files="!!files.length" - :is-mini="isMini" /> <repo-file - v-for="file in files" + v-for="file in flattendFiles" :key="file.id" :file="file" - :is-mini="isMini" - @linkclicked="fileClicked(file)" - :is-tree="isTree" - :has-files="!!files.length" - :active-file="activeFile" /> </tbody> </table> diff --git a/app/assets/javascripts/repo/components/repo_tab.vue b/app/assets/javascripts/repo/components/repo_tab.vue index 0d0c34ec741..098715915b0 100644 --- a/app/assets/javascripts/repo/components/repo_tab.vue +++ b/app/assets/javascripts/repo/components/repo_tab.vue @@ -26,11 +26,13 @@ const RepoTab = { }, methods: { - tabClicked: Store.setActiveFiles, - + tabClicked(file) { + Store.setActiveFiles(file); + }, closeTab(file) { if (file.changed) return; - this.$emit('tabclosed', file); + + Store.removeFromOpenedFiles(file); }, }, }; @@ -39,25 +41,28 @@ export default RepoTab; </script> <template> -<li @click="tabClicked(tab)"> - <a - href="#0" - class="close" - @click.stop.prevent="closeTab(tab)" - :aria-label="closeLabel"> - <i - class="fa" - :class="changedClass" - aria-hidden="true"> - </i> - </a> + <li + :class="{ active : tab.active }" + @click="tabClicked(tab)" + > + <button + type="button" + class="close-btn" + @click.stop.prevent="closeTab(tab)" + :aria-label="closeLabel"> + <i + class="fa" + :class="changedClass" + aria-hidden="true"> + </i> + </button> - <a - href="#" - class="repo-tab" - :title="tab.url" - @click.prevent="tabClicked(tab)"> - {{tab.name}} - </a> -</li> + <a + href="#" + class="repo-tab" + :title="tab.url" + @click.prevent="tabClicked(tab)"> + {{tab.name}} + </a> + </li> </template> diff --git a/app/assets/javascripts/repo/components/repo_tabs.vue b/app/assets/javascripts/repo/components/repo_tabs.vue index 9c5bfc5d0cf..b57cd0960de 100644 --- a/app/assets/javascripts/repo/components/repo_tabs.vue +++ b/app/assets/javascripts/repo/components/repo_tabs.vue @@ -1,36 +1,29 @@ <script> -import Store from '../stores/repo_store'; -import RepoTab from './repo_tab.vue'; -import RepoMixin from '../mixins/repo_mixin'; + import Store from '../stores/repo_store'; + import RepoTab from './repo_tab.vue'; + import RepoMixin from '../mixins/repo_mixin'; -const RepoTabs = { - mixins: [RepoMixin], - - components: { - 'repo-tab': RepoTab, - }, - - data: () => Store, - - methods: { - tabClosed(file) { - Store.removeFromOpenedFiles(file); + export default { + mixins: [RepoMixin], + components: { + 'repo-tab': RepoTab, }, - }, -}; - -export default RepoTabs; + data() { + return Store; + }, + }; </script> <template> -<ul id="tabs"> - <repo-tab - v-for="tab in openedFiles" - :key="tab.id" - :tab="tab" - :class="{'active' : tab.active}" - @tabclosed="tabClosed" - /> - <li class="tabs-divider" /> -</ul> + <ul + id="tabs" + class="list-unstyled" + > + <repo-tab + v-for="tab in openedFiles" + :key="tab.id" + :tab="tab" + /> + <li class="tabs-divider" /> + </ul> </template> diff --git a/app/assets/javascripts/repo/event_hub.js b/app/assets/javascripts/repo/event_hub.js new file mode 100644 index 00000000000..0948c2e5352 --- /dev/null +++ b/app/assets/javascripts/repo/event_hub.js @@ -0,0 +1,3 @@ +import Vue from 'vue'; + +export default new Vue(); diff --git a/app/assets/javascripts/repo/helpers/repo_helper.js b/app/assets/javascripts/repo/helpers/repo_helper.js index 46204598e1d..dfaf9caaee7 100644 --- a/app/assets/javascripts/repo/helpers/repo_helper.js +++ b/app/assets/javascripts/repo/helpers/repo_helper.js @@ -1,3 +1,4 @@ +import { convertPermissionToBoolean } from '../../lib/utils/common_utils'; import Service from '../services/repo_service'; import Store from '../stores/repo_store'; import Flash from '../../flash'; @@ -25,10 +26,6 @@ const RepoHelper = { key: '', - isTree(data) { - return Object.hasOwnProperty.call(data, 'blobs'); - }, - Time: window.performance && window.performance.now ? window.performance @@ -58,13 +55,20 @@ const RepoHelper = { }, setDirectoryOpen(tree, title) { - const file = tree; - if (!file) return undefined; + if (!tree) return; + + Object.assign(tree, { + opened: true, + }); + + RepoHelper.updateHistoryEntry(tree.url, title); + }, - file.opened = true; - file.icon = 'fa-folder-open'; - RepoHelper.updateHistoryEntry(file.url, title); - return file; + setDirectoryToClosed(entry) { + Object.assign(entry, { + opened: false, + files: [], + }); }, isRenderable() { @@ -81,63 +85,23 @@ const RepoHelper = { .catch(RepoHelper.loadingError); }, - // when you open a directory you need to put the directory files under - // the directory... This will merge the list of the current directory and the new list. - getNewMergedList(inDirectory, currentList, newList) { - const newListSorted = newList.sort(this.compareFilesCaseInsensitive); - if (!inDirectory) return newListSorted; - const indexOfFile = currentList.findIndex(file => file.url === inDirectory.url); - if (!indexOfFile) return newListSorted; - return RepoHelper.mergeNewListToOldList(newListSorted, currentList, inDirectory, indexOfFile); - }, - - // within the get new merged list this does the merging of the current list of files - // and the new list of files. The files are never "in" another directory they just - // appear like they are because of the margin. - mergeNewListToOldList(newList, oldList, inDirectory, indexOfFile) { - newList.reverse().forEach((newFile) => { - const fileIndex = indexOfFile + 1; - const file = newFile; - file.level = inDirectory.level + 1; - oldList.splice(fileIndex, 0, file); - }); - - return oldList; - }, - - compareFilesCaseInsensitive(a, b) { - const aName = a.name.toLowerCase(); - const bName = b.name.toLowerCase(); - if (a.level > 0) return 0; - if (aName < bName) { return -1; } - if (aName > bName) { return 1; } - return 0; - }, + getContent(treeOrFile, emptyFiles = false) { + let file = treeOrFile; - isRoot(url) { - // the url we are requesting -> split by the project URL. Grab the right side. - const isRoot = !!url.split(Store.projectUrl)[1] - // remove the first "/" - .slice(1) - // split this by "/" - .split('/') - // remove the first two items of the array... usually /tree/master. - .slice(2) - // we want to know the length of the array. - // If greater than 0 not root. - .length; - return isRoot; - }, + if (!Store.files.length) { + Store.loading.tree = true; + } - getContent(treeOrFile) { - let file = treeOrFile; return Service.getContent() .then((response) => { const data = response.data; if (response.headers && response.headers['page-title']) data.pageTitle = response.headers['page-title']; + if (response.headers && response.headers['is-root'] && !Store.isInitialRoot) { + Store.isRoot = convertPermissionToBoolean(response.headers['is-root']); + Store.isInitialRoot = Store.isRoot; + } - Store.isTree = RepoHelper.isTree(data); - if (!Store.isTree) { + if (file && file.type === 'blob') { if (!file) file = data; Store.binary = data.binary; @@ -145,38 +109,40 @@ const RepoHelper = { // file might be undefined RepoHelper.setBinaryDataAsBase64(data); Store.setViewToPreview(); - } else if (!Store.isPreviewView()) { - if (!data.render_error) { - Service.getRaw(data.raw_path) - .then((rawResponse) => { - Store.blobRaw = rawResponse.data; - data.plain = rawResponse.data; - RepoHelper.setFile(data, file); - }).catch(RepoHelper.loadingError); - } + } else if (!Store.isPreviewView() && !data.render_error) { + Service.getRaw(data.raw_path) + .then((rawResponse) => { + Store.blobRaw = rawResponse.data; + data.plain = rawResponse.data; + RepoHelper.setFile(data, file); + }).catch(RepoHelper.loadingError); } if (Store.isPreviewView()) { RepoHelper.setFile(data, file); } + } else { + Store.loading.tree = false; + RepoHelper.setDirectoryOpen(file, data.pageTitle || data.name); - // if the file tree is empty - if (Store.files.length === 0) { - const parentURL = Service.blobURLtoParentTree(Service.url); - Service.url = parentURL; - RepoHelper.getContent(); + if (emptyFiles) { + Store.files = []; } - } else { - // it's a tree - if (!file) Store.isRoot = RepoHelper.isRoot(Service.url); - file = RepoHelper.setDirectoryOpen(file, data.pageTitle || data.name); - const newDirectory = RepoHelper.dataToListOfFiles(data); - Store.addFilesToDirectory(file, Store.files, newDirectory); + + this.addToDirectory(file, data); + Store.prevURL = Service.blobURLtoParentTree(Service.url); } }).catch(RepoHelper.loadingError); }, + addToDirectory(file, data) { + const tree = file || Store; + const files = tree.files.concat(this.dataToListOfFiles(data, file ? file.level + 1 : 0)); + + tree.files = files; + }, + setFile(data, file) { const newFile = data; newFile.url = file.url || Service.url; // Grab the URL from service, happens on page refresh. @@ -190,57 +156,39 @@ const RepoHelper = { Store.setActiveFiles(newFile); }, - serializeBlob(blob) { - const simpleBlob = RepoHelper.serializeRepoEntity('blob', blob); - simpleBlob.lastCommitMessage = blob.last_commit.message; - simpleBlob.lastCommitUpdate = blob.last_commit.committed_date; - simpleBlob.loading = false; - - return simpleBlob; - }, - - serializeTree(tree) { - return RepoHelper.serializeRepoEntity('tree', tree); - }, - - serializeSubmodule(submodule) { - return RepoHelper.serializeRepoEntity('submodule', submodule); - }, - - serializeRepoEntity(type, entity) { + serializeRepoEntity(type, entity, level = 0) { const { url, name, icon, last_commit } = entity; - const returnObj = { + + return { type, name, url, + level, icon: `fa-${icon}`, - level: 0, + files: [], loading: false, + opened: false, + // eslint-disable-next-line camelcase + lastCommit: last_commit ? { + url: `${Store.projectUrl}/commit/${last_commit.id}`, + message: last_commit.message, + updatedAt: last_commit.committed_date, + } : {}, }; - - if (entity.last_commit) { - returnObj.lastCommitUrl = `${Store.projectUrl}/commit/${last_commit.id}`; - } else { - returnObj.lastCommitUrl = ''; - } - return returnObj; }, scrollTabsRight() { - // wait for the transition. 0.1 seconds. - setTimeout(() => { - const tabs = document.getElementById('tabs'); - if (!tabs) return; - tabs.scrollLeft = tabs.scrollWidth; - }, 200); + const tabs = document.getElementById('tabs'); + if (!tabs) return; + tabs.scrollLeft = tabs.scrollWidth; }, - dataToListOfFiles(data) { + dataToListOfFiles(data, level) { const { blobs, trees, submodules } = data; return [ - ...blobs.map(blob => RepoHelper.serializeBlob(blob)), - ...trees.map(tree => RepoHelper.serializeTree(tree)), - ...submodules.map(submodule => RepoHelper.serializeSubmodule(submodule)), + ...trees.map(tree => RepoHelper.serializeRepoEntity('tree', tree, level)), + ...submodules.map(submodule => RepoHelper.serializeRepoEntity('submodule', submodule, level)), + ...blobs.map(blob => RepoHelper.serializeRepoEntity('blob', blob, level)), ]; }, diff --git a/app/assets/javascripts/repo/index.js b/app/assets/javascripts/repo/index.js index 1a09f411b22..65dee7d5fd1 100644 --- a/app/assets/javascripts/repo/index.js +++ b/app/assets/javascripts/repo/index.js @@ -1,5 +1,6 @@ import $ from 'jquery'; import Vue from 'vue'; +import { convertPermissionToBoolean } from '../lib/utils/common_utils'; import Service from './services/repo_service'; import Store from './stores/repo_store'; import Repo from './components/repo.vue'; @@ -33,6 +34,8 @@ function setInitialStore(data) { Store.onTopOfBranch = data.onTopOfBranch; Store.newMrTemplateUrl = decodeURIComponent(data.newMrTemplateUrl); Store.customBranchURL = decodeURIComponent(data.blobUrl); + Store.isRoot = convertPermissionToBoolean(data.root); + Store.isInitialRoot = convertPermissionToBoolean(data.root); Store.currentBranch = $('button.dropdown-menu-toggle').attr('data-ref'); Store.checkIsCommitable(); Store.setBranchHash(); diff --git a/app/assets/javascripts/repo/stores/repo_store.js b/app/assets/javascripts/repo/stores/repo_store.js index f8d29af7ffe..49d7317a17e 100644 --- a/app/assets/javascripts/repo/stores/repo_store.js +++ b/app/assets/javascripts/repo/stores/repo_store.js @@ -2,14 +2,13 @@ import Helper from '../helpers/repo_helper'; import Service from '../services/repo_service'; const RepoStore = { - monaco: {}, monacoLoading: false, service: '', canCommit: false, onTopOfBranch: false, editMode: false, - isTree: false, - isRoot: false, + isRoot: null, + isInitialRoot: null, prevURL: '', projectId: '', projectName: '', @@ -39,23 +38,11 @@ const RepoStore = { newMrTemplateUrl: '', branchChanged: false, commitMessage: '', - binaryTypes: { - png: false, - md: false, - svg: false, - unknown: false, - }, loading: { tree: false, blob: false, }, - resetBinaryTypes() { - Object.keys(RepoStore.binaryTypes).forEach((key) => { - RepoStore.binaryTypes[key] = false; - }); - }, - setBranchHash() { return Service.getBranch() .then((data) => { @@ -72,10 +59,6 @@ const RepoStore = { RepoStore.isCommitable = RepoStore.onTopOfBranch && RepoStore.canCommit; }, - addFilesToDirectory(inDirectory, currentList, newList) { - RepoStore.files = Helper.getNewMergedList(inDirectory, currentList, newList); - }, - toggleRawPreview() { RepoStore.activeFile.raw = !RepoStore.activeFile.raw; RepoStore.activeFileLabel = RepoStore.activeFile.raw ? 'Display rendered file' : 'Display source'; @@ -129,30 +112,6 @@ const RepoStore = { RepoStore.activeFileLabel = 'Display source'; }, - removeChildFilesOfTree(tree) { - let foundTree = false; - const treeToClose = tree; - let canStopSearching = false; - RepoStore.files = RepoStore.files.filter((file) => { - const isItTheTreeWeWant = file.url === treeToClose.url; - // if it's the next tree - if (foundTree && file.type === 'tree' && !isItTheTreeWeWant && file.level === treeToClose.level) { - canStopSearching = true; - return true; - } - if (canStopSearching) return true; - - if (isItTheTreeWeWant) foundTree = true; - - if (foundTree) return file.level <= treeToClose.level; - return true; - }); - - treeToClose.opened = false; - treeToClose.icon = 'fa-folder'; - return treeToClose; - }, - removeFromOpenedFiles(file) { if (file.type === 'tree') return; let foundIndex; @@ -186,6 +145,7 @@ const RepoStore = { if (openedFilesAlreadyExists) return; openFile.changed = false; + openFile.active = true; RepoStore.openedFiles.push(openFile); }, diff --git a/app/assets/stylesheets/framework/animations.scss b/app/assets/stylesheets/framework/animations.scss index f0e6b23757f..374988bb590 100644 --- a/app/assets/stylesheets/framework/animations.scss +++ b/app/assets/stylesheets/framework/animations.scss @@ -198,6 +198,13 @@ a { height: 12px; } + &.animation-container-right { + .skeleton-line-2 { + left: 0; + right: 150px; + } + } + &::before { animation-duration: 1s; animation-fill-mode: forwards; diff --git a/app/assets/stylesheets/framework/new-sidebar.scss b/app/assets/stylesheets/framework/new-sidebar.scss index 78972717932..17fa31c450d 100644 --- a/app/assets/stylesheets/framework/new-sidebar.scss +++ b/app/assets/stylesheets/framework/new-sidebar.scss @@ -466,7 +466,7 @@ $new-sidebar-collapsed-width: 50px; @media (max-width: $screen-xs-max) { + .breadcrumbs-links { - padding-left: 17px; + padding-left: $gl-padding; border-left: 1px solid $gl-text-color-quaternary; } } diff --git a/app/assets/stylesheets/pages/clusters.scss b/app/assets/stylesheets/pages/clusters.scss index 5538e46a6c4..8d6f30e3b84 100644 --- a/app/assets/stylesheets/pages/clusters.scss +++ b/app/assets/stylesheets/pages/clusters.scss @@ -4,6 +4,6 @@ } .alert-block { - margin-bottom: 20px; + margin-bottom: 10px; } } diff --git a/app/assets/stylesheets/pages/issuable.scss b/app/assets/stylesheets/pages/issuable.scss index c93c4e93af5..48532503263 100644 --- a/app/assets/stylesheets/pages/issuable.scss +++ b/app/assets/stylesheets/pages/issuable.scss @@ -72,12 +72,22 @@ } } + .title-container { + display: flex; + } + .title { padding: 0; margin-bottom: 16px; border-bottom: none; } + .btn-edit { + margin-left: auto; + // Set height to match title height + height: 2em; + } + // Border around images in issue and MR descriptions. .description img:not(.emoji) { border: 1px solid $white-normal; diff --git a/app/assets/stylesheets/pages/notes.scss b/app/assets/stylesheets/pages/notes.scss index 96b7db3b85d..ebad429c2ba 100644 --- a/app/assets/stylesheets/pages/notes.scss +++ b/app/assets/stylesheets/pages/notes.scss @@ -531,14 +531,13 @@ ul.notes { padding: 0; min-width: 16px; color: $gray-darkest; + fill: $gray-darkest; .fa { position: relative; font-size: 16px; } - - svg { height: 16px; width: 16px; @@ -566,6 +565,7 @@ ul.notes { .link-highlight { color: $gl-link-color; + fill: $gl-link-color; svg { fill: $gl-link-color; diff --git a/app/assets/stylesheets/pages/repo.scss b/app/assets/stylesheets/pages/repo.scss index c36fe25f74d..ea37ccf5e3d 100644 --- a/app/assets/stylesheets/pages/repo.scss +++ b/app/assets/stylesheets/pages/repo.scss @@ -153,28 +153,13 @@ overflow-x: auto; li { - animation: swipeRightAppear ease-in 0.1s; - animation-iteration-count: 1; - transform-origin: 0% 50%; - list-style-type: none; + position: relative; background: $gray-normal; - display: inline-block; padding: #{$gl-padding / 2} $gl-padding; border-right: 1px solid $white-dark; border-bottom: 1px solid $white-dark; - white-space: nowrap; cursor: pointer; - &.remove { - animation: swipeRightDissapear ease-in 0.1s; - animation-iteration-count: 1; - transform-origin: 0% 50%; - - a { - width: 0; - } - } - &.active { background: $white-light; border-bottom: none; @@ -182,17 +167,21 @@ a { @include str-truncated(100px); - color: $black; + color: $gl-text-color; vertical-align: middle; text-decoration: none; margin-right: 12px; + } - &.close { - width: auto; - font-size: 15px; - opacity: 1; - margin-right: -6px; - } + .close-btn { + position: absolute; + right: 8px; + top: 50%; + padding: 0; + background: none; + border: 0; + font-size: $gl-font-size; + transform: translateY(-50%); } .close-icon:hover { @@ -201,9 +190,6 @@ .close-icon, .unsaved-icon { - float: right; - margin-top: 3px; - margin-left: 15px; color: $gray-darkest; } @@ -222,9 +208,7 @@ #repo-file-buttons { background-color: $white-light; - border-bottom: 1px solid $white-normal; padding: 5px 10px; - position: relative; border-top: 1px solid $white-normal; } @@ -287,37 +271,23 @@ overflow: auto; } - table { + .table { margin-bottom: 0; } tr { - animation: fadein 0.5s; - cursor: pointer; - - &.repo-file-options td { - padding: 0; - border-top: none; - background: $gray-light; + .repo-file-options { + padding: 2px 16px; width: 100%; - display: inline-block; - - &:first-child { - border-top-left-radius: 2px; - } + } - .title { - display: inline-block; - font-size: 10px; - text-transform: uppercase; - font-weight: $gl-font-weight-bold; - color: $gray-darkest; - white-space: nowrap; - overflow: hidden; - text-overflow: ellipsis; - vertical-align: middle; - padding: 2px 16px; - } + .title { + font-size: 10px; + text-transform: uppercase; + white-space: nowrap; + overflow: hidden; + text-overflow: ellipsis; + vertical-align: middle; } .file-icon { @@ -329,11 +299,13 @@ } } + .file { + cursor: pointer; + } + a { @include str-truncated(250px); color: $almost-black; - display: inline-block; - vertical-align: middle; } } } diff --git a/app/controllers/projects/tree_controller.rb b/app/controllers/projects/tree_controller.rb index f3719059f88..756f7e5df8c 100644 --- a/app/controllers/projects/tree_controller.rb +++ b/app/controllers/projects/tree_controller.rb @@ -36,6 +36,7 @@ class Projects::TreeController < Projects::ApplicationController format.json do page_title @path.presence || _("Files"), @ref, @project.name_with_namespace + response.header['is-root'] = @path.empty? # n+1: https://gitlab.com/gitlab-org/gitlab-ce/issues/38261 Gitlab::GitalyClient.allow_n_plus_1_calls do diff --git a/app/controllers/projects_controller.rb b/app/controllers/projects_controller.rb index e90b75672ae..c45f31f9e7e 100644 --- a/app/controllers/projects_controller.rb +++ b/app/controllers/projects_controller.rb @@ -125,7 +125,7 @@ class ProjectsController < Projects::ApplicationController return access_denied! unless can?(current_user, :remove_project, @project) ::Projects::DestroyService.new(@project, current_user, {}).async_execute - flash[:alert] = _("Project '%{project_name}' will be deleted.") % { project_name: @project.name_with_namespace } + flash[:notice] = _("Project '%{project_name}' is in the process of being deleted.") % { project_name: @project.name_with_namespace } redirect_to dashboard_projects_path, status: 302 rescue Projects::DestroyService::DestroyError => ex diff --git a/app/models/application_setting.rb b/app/models/application_setting.rb index a93b777a9bc..d3b8debb0fd 100644 --- a/app/models/application_setting.rb +++ b/app/models/application_setting.rb @@ -33,6 +33,8 @@ class ApplicationSetting < ActiveRecord::Base attr_accessor :domain_whitelist_raw, :domain_blacklist_raw + default_value_for :id, 1 + validates :uuid, presence: true validates :session_expire_delay, diff --git a/app/models/project.rb b/app/models/project.rb index 6e1cf9e31ee..4689b588906 100644 --- a/app/models/project.rb +++ b/app/models/project.rb @@ -1272,7 +1272,7 @@ class Project < ActiveRecord::Base # self.forked_from_project will be nil before the project is saved, so # we need to go through the relation - original_project = forked_project_link.forked_from_project + original_project = forked_project_link&.forked_from_project return true unless original_project level <= original_project.visibility_level diff --git a/app/serializers/container_tag_entity.rb b/app/serializers/container_tag_entity.rb index ec1fc349586..26a68c43807 100644 --- a/app/serializers/container_tag_entity.rb +++ b/app/serializers/container_tag_entity.rb @@ -1,7 +1,7 @@ class ContainerTagEntity < Grape::Entity include RequestAwareEntity - expose :name, :location, :revision, :total_size, :created_at + expose :name, :location, :revision, :short_revision, :total_size, :created_at expose :destroy_path, if: -> (*) { can_destroy? } do |tag| project_registry_repository_tag_path(project, tag.repository, tag.name, format: :json) diff --git a/app/services/projects/unlink_fork_service.rb b/app/services/projects/unlink_fork_service.rb index abe414d0c05..2b82e5732e4 100644 --- a/app/services/projects/unlink_fork_service.rb +++ b/app/services/projects/unlink_fork_service.rb @@ -15,8 +15,8 @@ module Projects refresh_forks_count(@project.forked_from_project) - @project.forked_project_link.destroy @project.fork_network_member.destroy + @project.forked_project_link.destroy end def refresh_forks_count(project) diff --git a/app/views/projects/clusters/_advanced_settings.html.haml b/app/views/projects/clusters/_advanced_settings.html.haml new file mode 100644 index 00000000000..6c162481dd8 --- /dev/null +++ b/app/views/projects/clusters/_advanced_settings.html.haml @@ -0,0 +1,14 @@ +- if can?(current_user, :admin_cluster, @cluster) + .append-bottom-20 + %label.append-bottom-10 + = s_('ClusterIntegration|Google Container Engine') + %p + - link_gke = link_to(s_('ClusterIntegration|Google Container Engine'), @cluster.gke_cluster_url, target: '_blank', rel: 'noopener noreferrer') + = s_('ClusterIntegration|Manage your cluster by visiting %{link_gke}').html_safe % { link_gke: link_gke } + + .well.form-group + %label.text-danger + = s_('ClusterIntegration|Remove cluster integration') + %p + = s_('ClusterIntegration|Removing cluster integration will remove the cluster configuration you have added to this project. It will not delete your project.') + = link_to(s_('ClusterIntegration|Remove integration'), namespace_project_cluster_path(@project.namespace, @project, @cluster.id), method: :delete, class: 'btn btn-danger', data: { confirm: "Are you sure you want to remove cluster integration from this project? This will not delete your cluster on Google Container Engine"}) diff --git a/app/views/projects/clusters/show.html.haml b/app/views/projects/clusters/show.html.haml index aee6f904a62..ff76abc3553 100644 --- a/app/views/projects/clusters/show.html.haml +++ b/app/views/projects/clusters/show.html.haml @@ -1,24 +1,37 @@ +- @content_class = "limit-container-width" unless fluid_layout - breadcrumb_title "Cluster" - page_title _("Cluster") +- expanded = Rails.env.test? + - status_path = status_namespace_project_cluster_path(@cluster.project.namespace, @cluster.project, @cluster.id, format: :json) if can?(current_user, :admin_cluster, @cluster) && @cluster.on_creation? -.row.prepend-top-default.edit-cluster-form.js-edit-cluster-form{ data: { status_path: status_path, +.edit-cluster-form.js-edit-cluster-form{ data: { status_path: status_path, toggle_status: @cluster.enabled? ? 'true': 'false', cluster_status: @cluster.status_name, cluster_status_reason: @cluster.status_reason } } - .col-sm-4 - = render 'sidebar' - .col-sm-8 - %label.append-bottom-10{ for: 'enable-cluster-integration' } - = s_('ClusterIntegration|Enable cluster integration') - %p - - if @cluster.enabled? - - if can?(current_user, :update_cluster, @cluster) - = s_('ClusterIntegration|Cluster integration is enabled for this project. Disabling this integration will not affect your cluster, it will only temporarily turn off GitLab\'s connection to it.') + + %section.settings + %h4= s_('ClusterIntegration|Enable cluster integration') + .settings-content.expanded + + .hidden.js-cluster-error.alert.alert-danger.alert-block.append-bottom-10{ role: 'alert' } + = s_('ClusterIntegration|Something went wrong while creating your cluster on Google Container Engine') + %p.js-error-reason + + .hidden.js-cluster-creating.alert.alert-info.alert-block.append-bottom-10{ role: 'alert' } + = s_('ClusterIntegration|Cluster is being created on Google Container Engine...') + + .hidden.js-cluster-success.alert.alert-success.alert-block.append-bottom-10{ role: 'alert' } + = s_('ClusterIntegration|Cluster was successfully created on Google Container Engine') + + %p + - if @cluster.enabled? + - if can?(current_user, :update_cluster, @cluster) + = s_('ClusterIntegration|Cluster integration is enabled for this project. Disabling this integration will not affect your cluster, it will only temporarily turn off GitLab\'s connection to it.') + - else + = s_('ClusterIntegration|Cluster integration is enabled for this project.') - else - = s_('ClusterIntegration|Cluster integration is enabled for this project.') - - else - = s_('ClusterIntegration|Cluster integration is disabled for this project.') + = s_('ClusterIntegration|Cluster integration is disabled for this project.') = form_for [@project.namespace.becomes(Namespace), @project, @cluster] do |field| = form_errors(@cluster) @@ -36,35 +49,28 @@ .form-group = field.submit s_('ClusterIntegration|Save'), class: 'btn btn-success' - - if can?(current_user, :admin_cluster, @cluster) - %label.append-bottom-10{ for: 'google-container-engine' } - = s_('ClusterIntegration|Google Container Engine') - %p - - link_gke = link_to(s_('ClusterIntegration|Google Container Engine'), @cluster.gke_cluster_url, target: '_blank', rel: 'noopener noreferrer') - = s_('ClusterIntegration|Manage your cluster by visiting %{link_gke}').html_safe % { link_gke: link_gke } + %section.settings#js-cluster-details + .settings-header + %h4= s_('ClusterIntegration|Cluster details') + %button.btn.js-settings-toggle + = expanded ? 'Collapse' : 'Expand' + %p= s_('ClusterIntegration|See and edit the details for your cluster') - .hidden.js-cluster-error.alert.alert-danger.alert-block{ role: 'alert' } - = s_('ClusterIntegration|Something went wrong while creating your cluster on Google Container Engine') - %p.js-error-reason - - .hidden.js-cluster-creating.alert.alert-info.alert-block{ role: 'alert' } - = s_('ClusterIntegration|Cluster is being created on Google Container Engine...') - - .hidden.js-cluster-success.alert.alert-success.alert-block{ role: 'alert' } - = s_('ClusterIntegration|Cluster was successfully created on Google Container Engine') + .settings-content.no-animate{ class: ('expanded' if expanded) } - .form_group.append-bottom-20 - %label.append-bottom-10{ for: 'cluter-name' } - = s_('ClusterIntegration|Cluster name') - .input-group - %input.form-control.cluster-name{ value: @cluster.gcp_cluster_name, disabled: true } - %span.input-group-addon.clipboard-addon - = clipboard_button(text: @cluster.gcp_cluster_name, title: s_('ClusterIntegration|Copy cluster name')) + .form_group.append-bottom-20 + %label.append-bottom-10{ for: 'cluter-name' } + = s_('ClusterIntegration|Cluster name') + .input-group + %input.form-control.cluster-name{ value: @cluster.gcp_cluster_name, disabled: true } + %span.input-group-addon.clipboard-addon + = clipboard_button(text: @cluster.gcp_cluster_name, title: s_('ClusterIntegration|Copy cluster name')) - - if can?(current_user, :admin_cluster, @cluster) - .well.form_group - %label.text-danger - = s_('ClusterIntegration|Remove cluster integration') - %p - = s_('ClusterIntegration|Removing cluster integration will remove the cluster configuration you have added to this project. It will not delete your project.') - = link_to(s_('ClusterIntegration|Remove integration'), namespace_project_cluster_path(@project.namespace, @project, @cluster.id), method: :delete, class: 'btn btn-danger', data: { confirm: "Are you sure you want to remove cluster integration from this project? This will not delete your cluster on Google Container Engine"}) + %section.settings#js-cluster-advanced-settings + .settings-header + %h4= s_('ClusterIntegration|Advanced settings') + %button.btn.js-settings-toggle + = expanded ? 'Collapse' : 'Expand' + %p= s_('ClusterIntegration|Manage Cluster integration on your GitLab project') + .settings-content.no-animate{ class: ('expanded' if expanded) } + = render 'advanced_settings' diff --git a/app/views/shared/repo/_repo.html.haml b/app/views/shared/repo/_repo.html.haml index 919f19f2c23..7185f5bcc5b 100644 --- a/app/views/shared/repo/_repo.html.haml +++ b/app/views/shared/repo/_repo.html.haml @@ -1,4 +1,5 @@ -#repo{ data: { url: content_url, +#repo{ data: { root: @path.empty?.to_s, + url: content_url, project_name: project.name, refs_url: refs_project_path(project, format: :json), project_url: project_path(project), diff --git a/changelogs/unreleased/bvl-do-not-use-redis-keys.yml b/changelogs/unreleased/bvl-do-not-use-redis-keys.yml new file mode 100644 index 00000000000..f703aad2065 --- /dev/null +++ b/changelogs/unreleased/bvl-do-not-use-redis-keys.yml @@ -0,0 +1,5 @@ +--- +title: Forbid the usage of `Redis#keys` +merge_request: 14889 +author: +type: fixed diff --git a/changelogs/unreleased/bvl-fix-deleting-forked-projects.yml b/changelogs/unreleased/bvl-fix-deleting-forked-projects.yml new file mode 100644 index 00000000000..95f56facc4b --- /dev/null +++ b/changelogs/unreleased/bvl-fix-deleting-forked-projects.yml @@ -0,0 +1,5 @@ +--- +title: Fix error when updating a forked project with deleted `ForkedProjectLink` +merge_request: 14916 +author: +type: fixed diff --git a/changelogs/unreleased/kt-bug-fix-revision-and-size-for-container-registry.yml b/changelogs/unreleased/kt-bug-fix-revision-and-size-for-container-registry.yml new file mode 100644 index 00000000000..acbb24d16fc --- /dev/null +++ b/changelogs/unreleased/kt-bug-fix-revision-and-size-for-container-registry.yml @@ -0,0 +1,5 @@ +--- +title: Fix revision and total size missing for Container Registry +merge_request: +author: +type: fixed diff --git a/changelogs/unreleased/prevent-creating-multiple-application-settings.yml b/changelogs/unreleased/prevent-creating-multiple-application-settings.yml new file mode 100644 index 00000000000..fd49028b9e9 --- /dev/null +++ b/changelogs/unreleased/prevent-creating-multiple-application-settings.yml @@ -0,0 +1,5 @@ +--- +title: Prevent creating multiple ApplicationSetting instances +merge_request: +author: +type: fixed diff --git a/config/database.yml.mysql b/config/database.yml.mysql index eb71d3f5fe1..98c2abe9f5e 100644 --- a/config/database.yml.mysql +++ b/config/database.yml.mysql @@ -10,7 +10,7 @@ production: pool: 10 username: git password: "secure password" - # host: localhost + host: localhost # socket: /tmp/mysql.sock # @@ -25,7 +25,22 @@ development: pool: 5 username: root password: "secure password" - # host: localhost + host: localhost + # socket: /tmp/mysql.sock + +# +# Staging specific +# +staging: + adapter: mysql2 + encoding: utf8 + collation: utf8_general_ci + reconnect: false + database: gitlabhq_staging + pool: 10 + username: git + password: "secure password" + host: localhost # socket: /tmp/mysql.sock # Warning: The database defined as "test" will be erased and @@ -40,6 +55,6 @@ test: &test pool: 5 username: root password: - # host: localhost + host: localhost # socket: /tmp/mysql.sock prepared_statements: false diff --git a/config/database.yml.postgresql b/config/database.yml.postgresql index 4b30982fe82..baded682e46 100644 --- a/config/database.yml.postgresql +++ b/config/database.yml.postgresql @@ -6,10 +6,9 @@ production: encoding: unicode database: gitlabhq_production pool: 10 - # username: git - # password: - # host: localhost - # port: 5432 + username: git + password: "secure password" + host: localhost # # Development specific @@ -20,8 +19,8 @@ development: database: gitlabhq_development pool: 5 username: postgres - password: - # host: localhost + password: "secure password" + host: localhost # # Staging specific @@ -30,10 +29,10 @@ staging: adapter: postgresql encoding: unicode database: gitlabhq_staging - pool: 5 - username: postgres - password: - # host: localhost + pool: 10 + username: git + password: "secure password" + host: localhost # Warning: The database defined as "test" will be erased and # re-generated from your development database when you run "rake". @@ -45,5 +44,5 @@ test: &test pool: 5 username: postgres password: - # host: localhost + host: localhost prepared_statements: false diff --git a/doc/user/project/issues/automatic_issue_closing.md b/doc/user/project/issues/automatic_issue_closing.md index 10dede255ec..402a2a3c727 100644 --- a/doc/user/project/issues/automatic_issue_closing.md +++ b/doc/user/project/issues/automatic_issue_closing.md @@ -1,8 +1,10 @@ # Automatic issue closing ->**Note:** -This is the user docs. In order to change the default issue closing pattern, -follow the steps in the [administration docs]. +>**Notes:** +> - This is the user docs. In order to change the default issue closing pattern, +> follow the steps in the [administration docs]. +> - For performance reasons, automatic issue closing is disabled for the very +> first push from an existing repository. When a commit or merge request resolves one or more issues, it is possible to automatically have these issues closed when the commit or merge request lands diff --git a/lib/gitlab/background_migration/deserialize_merge_request_diffs_and_commits.rb b/lib/gitlab/background_migration/deserialize_merge_request_diffs_and_commits.rb index 8e5c95f2287..380802258f5 100644 --- a/lib/gitlab/background_migration/deserialize_merge_request_diffs_and_commits.rb +++ b/lib/gitlab/background_migration/deserialize_merge_request_diffs_and_commits.rb @@ -81,6 +81,7 @@ module Gitlab def single_diff_rows(merge_request_diff) sha_attribute = Gitlab::Database::ShaAttribute.new commits = YAML.load(merge_request_diff.st_commits) rescue [] + commits ||= [] commit_rows = commits.map.with_index do |commit, index| commit_hash = commit.to_hash.with_indifferent_access.except(:parent_ids) diff --git a/lib/gitlab/git/storage/circuit_breaker.rb b/lib/gitlab/git/storage/circuit_breaker.rb index 4e2d261f461..0456ad9a1f3 100644 --- a/lib/gitlab/git/storage/circuit_breaker.rb +++ b/lib/gitlab/git/storage/circuit_breaker.rb @@ -16,7 +16,7 @@ module Gitlab pattern = "#{Gitlab::Git::Storage::REDIS_KEY_PREFIX}*" Gitlab::Git::Storage.redis.with do |redis| - all_storage_keys = redis.keys(pattern) + all_storage_keys = redis.scan_each(match: pattern).to_a redis.del(*all_storage_keys) unless all_storage_keys.empty? end diff --git a/lib/gitlab/git/storage/health.rb b/lib/gitlab/git/storage/health.rb index 1564e94b7f7..7049772fe3b 100644 --- a/lib/gitlab/git/storage/health.rb +++ b/lib/gitlab/git/storage/health.rb @@ -23,26 +23,36 @@ module Gitlab end end - def self.all_keys_for_storages(storage_names, redis) + private_class_method def self.all_keys_for_storages(storage_names, redis) keys_per_storage = {} redis.pipelined do storage_names.each do |storage_name| pattern = pattern_for_storage(storage_name) + matched_keys = redis.scan_each(match: pattern) - keys_per_storage[storage_name] = redis.keys(pattern) + keys_per_storage[storage_name] = matched_keys end end - keys_per_storage + # We need to make sure each lazy-loaded `Enumerator` for matched keys + # is loaded into an array. + # + # Otherwise it would be loaded in the second `Redis#pipelined` block + # within `.load_for_keys`. In this pipelined call, the active + # Redis-client changes again, so the values would not be available + # until the end of that pipelined-block. + keys_per_storage.each do |storage_name, key_future| + keys_per_storage[storage_name] = key_future.to_a + end end - def self.load_for_keys(keys_per_storage, redis) + private_class_method def self.load_for_keys(keys_per_storage, redis) info_for_keys = {} redis.pipelined do keys_per_storage.each do |storage_name, keys_future| - info_for_storage = keys_future.value.map do |key| + info_for_storage = keys_future.map do |key| { name: key, failure_count: redis.hget(key, :failure_count) } end diff --git a/scripts/prepare_build.sh b/scripts/prepare_build.sh index 39806901274..7abadef5e89 100644 --- a/scripts/prepare_build.sh +++ b/scripts/prepare_build.sh @@ -27,11 +27,9 @@ fi cp config/database.yml.$GITLAB_DATABASE config/database.yml if [ "$GITLAB_DATABASE" = 'postgresql' ]; then - sed -i 's/# host:.*/host: postgres/g' config/database.yml + sed -i 's/localhost/postgres/g' config/database.yml else # Assume it's mysql - sed -i 's/username:.*/username: root/g' config/database.yml - sed -i 's/password:.*/password:/g' config/database.yml - sed -i 's/# host:.*/host: mysql/g' config/database.yml + sed -i 's/localhost/mysql/g' config/database.yml fi cp config/resque.yml.example config/resque.yml diff --git a/spec/features/projects_spec.rb b/spec/features/projects_spec.rb index 3bc7ec3123f..3b01ed442bf 100644 --- a/spec/features/projects_spec.rb +++ b/spec/features/projects_spec.rb @@ -139,7 +139,7 @@ feature 'Project' do it 'removes a project' do expect { remove_with_confirm('Remove project', project.path) }.to change {Project.count}.by(-1) - expect(page).to have_content "Project 'test / project1' will be deleted." + expect(page).to have_content "Project 'test / project1' is in the process of being deleted." expect(Project.all.count).to be_zero expect(project.issues).to be_empty expect(project.merge_requests).to be_empty diff --git a/spec/fixtures/api/schemas/registry/tag.json b/spec/fixtures/api/schemas/registry/tag.json index 5bc307e0e64..3a2c88791e1 100644 --- a/spec/fixtures/api/schemas/registry/tag.json +++ b/spec/fixtures/api/schemas/registry/tag.json @@ -14,6 +14,11 @@ "revision": { "type": "string" }, + "short_revision": { + "type": "string", + "minLength": 9, + "maxLength": 9 + }, "total_size": { "type": "integer" }, diff --git a/spec/javascripts/issue_show/components/app_spec.js b/spec/javascripts/issue_show/components/app_spec.js index 583a3a74d77..2ea290108a4 100644 --- a/spec/javascripts/issue_show/components/app_spec.js +++ b/spec/javascripts/issue_show/components/app_spec.js @@ -332,4 +332,15 @@ describe('Issuable output', () => { .catch(done.fail); }); }); + + describe('show inline edit button', () => { + it('should not render by default', () => { + expect(vm.$el.querySelector('.title-container .note-action-button')).toBeDefined(); + }); + + it('should render if showInlineEditButton', () => { + vm.showInlineEditButton = true; + expect(vm.$el.querySelector('.title-container .note-action-button')).toBeDefined(); + }); + }); }); diff --git a/spec/javascripts/issue_show/components/title_spec.js b/spec/javascripts/issue_show/components/title_spec.js index a2d90a9b9f5..c1edc785d0f 100644 --- a/spec/javascripts/issue_show/components/title_spec.js +++ b/spec/javascripts/issue_show/components/title_spec.js @@ -1,6 +1,7 @@ import Vue from 'vue'; import Store from '~/issue_show/stores'; import titleComponent from '~/issue_show/components/title.vue'; +import eventHub from '~/issue_show/event_hub'; describe('Title component', () => { let vm; @@ -25,7 +26,7 @@ describe('Title component', () => { it('renders title HTML', () => { expect( - vm.$el.innerHTML.trim(), + vm.$el.querySelector('.title').innerHTML.trim(), ).toBe('Testing <img>'); }); @@ -47,12 +48,12 @@ describe('Title component', () => { Vue.nextTick(() => { expect( - vm.$el.classList.contains('issue-realtime-pre-pulse'), + vm.$el.querySelector('.title').classList.contains('issue-realtime-pre-pulse'), ).toBeTruthy(); setTimeout(() => { expect( - vm.$el.classList.contains('issue-realtime-trigger-pulse'), + vm.$el.querySelector('.title').classList.contains('issue-realtime-trigger-pulse'), ).toBeTruthy(); done(); @@ -72,4 +73,36 @@ describe('Title component', () => { done(); }); }); + + describe('inline edit button', () => { + beforeEach(() => { + spyOn(eventHub, '$emit'); + }); + + it('should not show by default', () => { + expect(vm.$el.querySelector('.note-action-button')).toBeNull(); + }); + + it('should not show if canUpdate is false', () => { + vm.showInlineEditButton = true; + vm.canUpdate = false; + expect(vm.$el.querySelector('.note-action-button')).toBeNull(); + }); + + it('should show if showInlineEditButton and canUpdate', () => { + vm.showInlineEditButton = true; + vm.canUpdate = true; + expect(vm.$el.querySelector('.note-action-button')).toBeDefined(); + }); + + it('should trigger open.form event when clicked', () => { + vm.showInlineEditButton = true; + vm.canUpdate = true; + + Vue.nextTick(() => { + vm.$el.querySelector('.note-action-button').click(); + expect(eventHub.$emit).toHaveBeenCalledWith('open.form'); + }); + }); + }); }); diff --git a/spec/javascripts/lib/utils/common_utils_spec.js b/spec/javascripts/lib/utils/common_utils_spec.js index 6613b7dee6b..a5298be5669 100644 --- a/spec/javascripts/lib/utils/common_utils_spec.js +++ b/spec/javascripts/lib/utils/common_utils_spec.js @@ -467,19 +467,27 @@ describe('common_utils', () => { commonUtils.ajaxPost(requestURL, data); expect(ajaxSpy.calls.allArgs()[0][0].type).toEqual('POST'); }); + }); - describe('gl.utils.spriteIcon', () => { - beforeEach(() => { - window.gon.sprite_icons = 'icons.svg'; - }); + describe('spriteIcon', () => { + let beforeGon; - it('should return the svg for a linked icon', () => { - expect(gl.utils.spriteIcon('test')).toEqual('<svg ><use xlink:href="icons.svg#test" /></svg>'); - }); + beforeEach(() => { + window.gon = window.gon || {}; + beforeGon = Object.assign({}, window.gon); + window.gon.sprite_icons = 'icons.svg'; + }); - it('should set svg className when passed', () => { - expect(gl.utils.spriteIcon('test', 'fa fa-test')).toEqual('<svg class="fa fa-test"><use xlink:href="icons.svg#test" /></svg>'); - }); + afterEach(() => { + window.gon = beforeGon; + }); + + it('should return the svg for a linked icon', () => { + expect(commonUtils.spriteIcon('test')).toEqual('<svg ><use xlink:href="icons.svg#test" /></svg>'); + }); + + it('should set svg className when passed', () => { + expect(commonUtils.spriteIcon('test', 'fa fa-test')).toEqual('<svg class="fa fa-test"><use xlink:href="icons.svg#test" /></svg>'); }); }); }); diff --git a/spec/javascripts/registry/mock_data.js b/spec/javascripts/registry/mock_data.js index 18600d00bff..6bffb47be55 100644 --- a/spec/javascripts/registry/mock_data.js +++ b/spec/javascripts/registry/mock_data.js @@ -26,7 +26,7 @@ export const registryServerResponse = [ name: 'centos7', short_revision: 'b118ab5b0', revision: 'b118ab5b0e90b7cb5127db31d5321ac14961d097516a8e0e72084b6cdc783b43', - size: 679, + total_size: 679, layers: 19, location: 'location', created_at: 1505828744434, @@ -36,7 +36,7 @@ export const registryServerResponse = [ name: 'centos6', short_revision: 'b118ab5b0', revision: 'b118ab5b0e90b7cb5127db31d5321ac14961d097516a8e0e72084b6cdc783b43', - size: 679, + total_size: 679, layers: 19, location: 'location', created_at: 1505828744434, @@ -70,7 +70,7 @@ export const parsedRegistryServerResponse = [ tag: registryServerResponse[0].name, revision: registryServerResponse[0].revision, shortRevision: registryServerResponse[0].short_revision, - size: registryServerResponse[0].size, + size: registryServerResponse[0].total_size, layers: registryServerResponse[0].layers, location: registryServerResponse[0].location, createdAt: registryServerResponse[0].created_at, @@ -81,7 +81,7 @@ export const parsedRegistryServerResponse = [ tag: registryServerResponse[1].name, revision: registryServerResponse[1].revision, shortRevision: registryServerResponse[1].short_revision, - size: registryServerResponse[1].size, + size: registryServerResponse[1].total_size, layers: registryServerResponse[1].layers, location: registryServerResponse[1].location, createdAt: registryServerResponse[1].created_at, diff --git a/spec/javascripts/repo/components/repo_commit_section_spec.js b/spec/javascripts/repo/components/repo_commit_section_spec.js index 0635de4b30b..e09d593f04c 100644 --- a/spec/javascripts/repo/components/repo_commit_section_spec.js +++ b/spec/javascripts/repo/components/repo_commit_section_spec.js @@ -134,6 +134,7 @@ describe('RepoCommitSection', () => { afterEach(() => { vm.$destroy(); el.remove(); + RepoStore.openedFiles = []; }); it('shows commit message', () => { diff --git a/spec/javascripts/repo/components/repo_edit_button_spec.js b/spec/javascripts/repo/components/repo_edit_button_spec.js index 411514009dc..dff2fac191d 100644 --- a/spec/javascripts/repo/components/repo_edit_button_spec.js +++ b/spec/javascripts/repo/components/repo_edit_button_spec.js @@ -9,6 +9,10 @@ describe('RepoEditButton', () => { return new RepoEditButton().$mount(); } + afterEach(() => { + RepoStore.openedFiles = []; + }); + it('renders an edit button that toggles the view state', (done) => { RepoStore.isCommitable = true; RepoStore.changedFiles = []; @@ -38,12 +42,4 @@ describe('RepoEditButton', () => { expect(vm.$el.innerHTML).toBeUndefined(); }); - - describe('methods', () => { - describe('editCancelClicked', () => { - it('sets dialog to open when there are changedFiles'); - - it('toggles editMode and calls toggleBlobView'); - }); - }); }); diff --git a/spec/javascripts/repo/components/repo_editor_spec.js b/spec/javascripts/repo/components/repo_editor_spec.js index 85d55d171f9..a25a600b3be 100644 --- a/spec/javascripts/repo/components/repo_editor_spec.js +++ b/spec/javascripts/repo/components/repo_editor_spec.js @@ -1,4 +1,5 @@ import Vue from 'vue'; +import RepoStore from '~/repo/stores/repo_store'; import repoEditor from '~/repo/components/repo_editor.vue'; describe('RepoEditor', () => { @@ -8,6 +9,10 @@ describe('RepoEditor', () => { this.vm = new RepoEditor().$mount(); }); + afterEach(() => { + RepoStore.openedFiles = []; + }); + it('renders an ide container', (done) => { this.vm.openedFiles = ['idiidid']; this.vm.binary = false; diff --git a/spec/javascripts/repo/components/repo_file_buttons_spec.js b/spec/javascripts/repo/components/repo_file_buttons_spec.js index dfab51710c3..701c260224f 100644 --- a/spec/javascripts/repo/components/repo_file_buttons_spec.js +++ b/spec/javascripts/repo/components/repo_file_buttons_spec.js @@ -9,6 +9,10 @@ describe('RepoFileButtons', () => { return new RepoFileButtons().$mount(); } + afterEach(() => { + RepoStore.openedFiles = []; + }); + it('renders Raw, Blame, History, Permalink and Preview toggle', () => { const activeFile = { extension: 'md', diff --git a/spec/javascripts/repo/components/repo_file_options_spec.js b/spec/javascripts/repo/components/repo_file_options_spec.js deleted file mode 100644 index 9759b4bf12d..00000000000 --- a/spec/javascripts/repo/components/repo_file_options_spec.js +++ /dev/null @@ -1,33 +0,0 @@ -import Vue from 'vue'; -import repoFileOptions from '~/repo/components/repo_file_options.vue'; - -describe('RepoFileOptions', () => { - const projectName = 'projectName'; - - function createComponent(propsData) { - const RepoFileOptions = Vue.extend(repoFileOptions); - - return new RepoFileOptions({ - propsData, - }).$mount(); - } - - it('renders the title and new file/folder buttons if isMini is true', () => { - const vm = createComponent({ - isMini: true, - projectName, - }); - - expect(vm.$el.classList.contains('repo-file-options')).toBeTruthy(); - expect(vm.$el.querySelector('.title').textContent).toEqual(projectName); - }); - - it('does not render if isMini is false', () => { - const vm = createComponent({ - isMini: false, - projectName, - }); - - expect(vm.$el.innerHTML).toBeFalsy(); - }); -}); diff --git a/spec/javascripts/repo/components/repo_file_spec.js b/spec/javascripts/repo/components/repo_file_spec.js index 620b604f404..334bf0997ca 100644 --- a/spec/javascripts/repo/components/repo_file_spec.js +++ b/spec/javascripts/repo/components/repo_file_spec.js @@ -1,21 +1,11 @@ import Vue from 'vue'; import repoFile from '~/repo/components/repo_file.vue'; import RepoStore from '~/repo/stores/repo_store'; +import eventHub from '~/repo/event_hub'; +import { file } from '../mock_data'; describe('RepoFile', () => { const updated = 'updated'; - const file = { - icon: 'icon', - url: 'url', - name: 'name', - lastCommitMessage: 'message', - lastCommitUpdate: Date.now(), - level: 10, - }; - const activeFile = { - pageTitle: 'pageTitle', - url: 'url', - }; const otherFile = { html: '<p class="file-content">html</p>', pageTitle: 'otherpageTitle', @@ -29,12 +19,15 @@ describe('RepoFile', () => { }).$mount(); } + beforeEach(() => { + RepoStore.openedFiles = []; + }); + it('renders link, icon, name and last commit details', () => { const RepoFile = Vue.extend(repoFile); const vm = new RepoFile({ propsData: { - file, - activeFile, + file: file(), }, }); spyOn(vm, 'timeFormated').and.returnValue(updated); @@ -43,28 +36,20 @@ describe('RepoFile', () => { const name = vm.$el.querySelector('.repo-file-name'); const fileIcon = vm.$el.querySelector('.file-icon'); - expect(vm.$el.classList.contains('active')).toBeTruthy(); - expect(vm.$el.querySelector(`.${file.icon}`).style.marginLeft).toEqual('100px'); - expect(name.title).toEqual(file.url); - expect(name.href).toMatch(`/${file.url}`); - expect(name.textContent.trim()).toEqual(file.name); - expect(vm.$el.querySelector('.commit-message').textContent.trim()).toBe(file.lastCommitMessage); + expect(vm.$el.querySelector(`.${vm.file.icon}`).style.marginLeft).toEqual('0px'); + expect(name.href).toMatch(`/${vm.file.url}`); + expect(name.textContent.trim()).toEqual(vm.file.name); + expect(vm.$el.querySelector('.commit-message').textContent.trim()).toBe(vm.file.lastCommit.message); expect(vm.$el.querySelector('.commit-update').textContent.trim()).toBe(updated); - expect(fileIcon.classList.contains(file.icon)).toBeTruthy(); - expect(fileIcon.style.marginLeft).toEqual(`${file.level * 10}px`); + expect(fileIcon.classList.contains(vm.file.icon)).toBeTruthy(); + expect(fileIcon.style.marginLeft).toEqual(`${vm.file.level * 10}px`); }); it('does render if hasFiles is true and is loading tree', () => { const vm = createComponent({ - file, - activeFile, - loading: { - tree: true, - }, - hasFiles: true, + file: file(), }); - expect(vm.$el.innerHTML).toBeTruthy(); expect(vm.$el.querySelector('.fa-spin.fa-spinner')).toBeFalsy(); }); @@ -75,75 +60,51 @@ describe('RepoFile', () => { }); it('renders a spinner if the file is loading', () => { - file.loading = true; - const vm = createComponent({ - file, - activeFile, - loading: { - tree: true, - }, - hasFiles: true, - }); - - expect(vm.$el.innerHTML).toBeTruthy(); - expect(vm.$el.querySelector('.fa-spin.fa-spinner').style.marginLeft).toEqual(`${file.level * 10}px`); - }); - - it('does not render if loading tree', () => { + const f = file(); + f.loading = true; const vm = createComponent({ - file, - activeFile, - loading: { - tree: true, - }, + file: f, }); - expect(vm.$el.innerHTML).toBeFalsy(); + expect(vm.$el.querySelector('.fa-spin.fa-spinner')).not.toBeNull(); + expect(vm.$el.querySelector('.fa-spin.fa-spinner').style.marginLeft).toEqual(`${vm.file.level * 16}px`); }); it('does not render commit message and datetime if mini', () => { + RepoStore.openedFiles.push(file()); + const vm = createComponent({ - file, - activeFile, - isMini: true, + file: file(), }); expect(vm.$el.querySelector('.commit-message')).toBeFalsy(); expect(vm.$el.querySelector('.commit-update')).toBeFalsy(); }); - it('does not set active class if file is active file', () => { - const vm = createComponent({ - file, - activeFile: {}, - }); - - expect(vm.$el.classList.contains('active')).toBeFalsy(); - }); - it('fires linkClicked when the link is clicked', () => { const vm = createComponent({ - file, - activeFile, + file: file(), }); spyOn(vm, 'linkClicked'); - vm.$el.querySelector('.repo-file-name').click(); + vm.$el.click(); - expect(vm.linkClicked).toHaveBeenCalledWith(file); + expect(vm.linkClicked).toHaveBeenCalledWith(vm.file); }); describe('methods', () => { describe('linkClicked', () => { - const vm = jasmine.createSpyObj('vm', ['$emit']); + it('$emits fileNameClicked with file obj', () => { + spyOn(eventHub, '$emit'); - it('$emits linkclicked with file obj', () => { - const theFile = {}; + const vm = createComponent({ + file: file(), + }); - repoFile.methods.linkClicked.call(vm, theFile); + vm.linkClicked(vm.file); - expect(vm.$emit).toHaveBeenCalledWith('linkclicked', theFile); + expect(eventHub.$emit).toHaveBeenCalledWith('fileNameClicked', vm.file); }); }); }); diff --git a/spec/javascripts/repo/components/repo_loading_file_spec.js b/spec/javascripts/repo/components/repo_loading_file_spec.js index a030314d749..e9f95a02028 100644 --- a/spec/javascripts/repo/components/repo_loading_file_spec.js +++ b/spec/javascripts/repo/components/repo_loading_file_spec.js @@ -1,4 +1,5 @@ import Vue from 'vue'; +import RepoStore from '~/repo/stores/repo_store'; import repoLoadingFile from '~/repo/components/repo_loading_file.vue'; describe('RepoLoadingFile', () => { @@ -28,6 +29,10 @@ describe('RepoLoadingFile', () => { }); } + afterEach(() => { + RepoStore.openedFiles = []; + }); + it('renders 3 columns of animated LoC', () => { const vm = createComponent({ loading: { @@ -42,38 +47,16 @@ describe('RepoLoadingFile', () => { }); it('renders 1 column of animated LoC if isMini', () => { + RepoStore.openedFiles = new Array(1); const vm = createComponent({ loading: { tree: true, }, hasFiles: false, - isMini: true, }); const columns = [...vm.$el.querySelectorAll('td')]; expect(columns.length).toEqual(1); assertColumns(columns); }); - - it('does not render if tree is not loading', () => { - const vm = createComponent({ - loading: { - tree: false, - }, - hasFiles: false, - }); - - expect(vm.$el.innerHTML).toBeFalsy(); - }); - - it('does not render if hasFiles is true', () => { - const vm = createComponent({ - loading: { - tree: true, - }, - hasFiles: true, - }); - - expect(vm.$el.innerHTML).toBeFalsy(); - }); }); diff --git a/spec/javascripts/repo/components/repo_prev_directory_spec.js b/spec/javascripts/repo/components/repo_prev_directory_spec.js index 34dde545e6a..4c064f21084 100644 --- a/spec/javascripts/repo/components/repo_prev_directory_spec.js +++ b/spec/javascripts/repo/components/repo_prev_directory_spec.js @@ -1,5 +1,6 @@ import Vue from 'vue'; import repoPrevDirectory from '~/repo/components/repo_prev_directory.vue'; +import eventHub from '~/repo/event_hub'; describe('RepoPrevDirectory', () => { function createComponent(propsData) { @@ -20,7 +21,7 @@ describe('RepoPrevDirectory', () => { spyOn(vm, 'linkClicked'); expect(link.href).toMatch(`/${prevUrl}`); - expect(link.textContent).toEqual('..'); + expect(link.textContent).toEqual('...'); link.click(); @@ -29,14 +30,17 @@ describe('RepoPrevDirectory', () => { describe('methods', () => { describe('linkClicked', () => { - const vm = jasmine.createSpyObj('vm', ['$emit']); + it('$emits linkclicked with prevUrl', () => { + const prevUrl = 'prevUrl'; + const vm = createComponent({ + prevUrl, + }); - it('$emits linkclicked with file obj', () => { - const file = {}; + spyOn(eventHub, '$emit'); - repoPrevDirectory.methods.linkClicked.call(vm, file); + vm.linkClicked(prevUrl); - expect(vm.$emit).toHaveBeenCalledWith('linkclicked', file); + expect(eventHub.$emit).toHaveBeenCalledWith('goToPreviousDirectoryClicked', prevUrl); }); }); }); diff --git a/spec/javascripts/repo/components/repo_sidebar_spec.js b/spec/javascripts/repo/components/repo_sidebar_spec.js index 35d2b37ac2a..61283da8257 100644 --- a/spec/javascripts/repo/components/repo_sidebar_spec.js +++ b/spec/javascripts/repo/components/repo_sidebar_spec.js @@ -3,6 +3,7 @@ import Helper from '~/repo/helpers/repo_helper'; import RepoService from '~/repo/services/repo_service'; import RepoStore from '~/repo/stores/repo_store'; import repoSidebar from '~/repo/components/repo_sidebar.vue'; +import { file } from '../mock_data'; describe('RepoSidebar', () => { let vm; @@ -15,14 +16,15 @@ describe('RepoSidebar', () => { afterEach(() => { vm.$destroy(); + + RepoStore.files = []; + RepoStore.openedFiles = []; }); it('renders a sidebar', () => { - RepoStore.files = [{ - id: 0, - }]; + RepoStore.files = [file()]; RepoStore.openedFiles = []; - RepoStore.isRoot = false; + RepoStore.isRoot = true; vm = createComponent(); const thead = vm.$el.querySelector('thead'); @@ -30,9 +32,9 @@ describe('RepoSidebar', () => { expect(vm.$el.id).toEqual('sidebar'); expect(vm.$el.classList.contains('sidebar-mini')).toBeFalsy(); - expect(thead.querySelector('.name').textContent).toEqual('Name'); - expect(thead.querySelector('.last-commit').textContent).toEqual('Last commit'); - expect(thead.querySelector('.last-update').textContent).toEqual('Last update'); + expect(thead.querySelector('.name').textContent.trim()).toEqual('Name'); + expect(thead.querySelector('.last-commit').textContent.trim()).toEqual('Last commit'); + expect(thead.querySelector('.last-update').textContent.trim()).toEqual('Last update'); expect(tbody.querySelector('.repo-file-options')).toBeFalsy(); expect(tbody.querySelector('.prev-directory')).toBeFalsy(); expect(tbody.querySelector('.loading-file')).toBeFalsy(); @@ -46,76 +48,74 @@ describe('RepoSidebar', () => { vm = createComponent(); expect(vm.$el.classList.contains('sidebar-mini')).toBeTruthy(); - expect(vm.$el.querySelector('thead')).toBeFalsy(); - expect(vm.$el.querySelector('tbody .repo-file-options')).toBeTruthy(); + expect(vm.$el.querySelector('thead')).toBeTruthy(); + expect(vm.$el.querySelector('thead .repo-file-options')).toBeTruthy(); }); it('renders 5 loading files if tree is loading and not hasFiles', () => { - RepoStore.loading = { - tree: true, - }; + RepoStore.loading.tree = true; RepoStore.files = []; vm = createComponent(); expect(vm.$el.querySelectorAll('tbody .loading-file').length).toEqual(5); }); - it('renders a prev directory if isRoot', () => { - RepoStore.files = [{ - id: 0, - }]; - RepoStore.isRoot = true; + it('renders a prev directory if is not root', () => { + RepoStore.files = [file()]; + RepoStore.isRoot = false; + RepoStore.loading.tree = false; vm = createComponent(); expect(vm.$el.querySelector('tbody .prev-directory')).toBeTruthy(); }); + describe('flattendFiles', () => { + it('returns a flattend array of files', () => { + const f = file(); + f.files.push(file('testing 123')); + const files = [f, file()]; + vm = createComponent(); + vm.files = files; + + expect(vm.flattendFiles.length).toBe(3); + expect(vm.flattendFiles[1].name).toBe('testing 123'); + }); + }); + describe('methods', () => { describe('fileClicked', () => { it('should fetch data for new file', () => { spyOn(Helper, 'getContent').and.callThrough(); - const file1 = { - id: 0, - url: '', - }; - RepoStore.files = [file1]; + RepoStore.files = [file()]; RepoStore.isRoot = true; vm = createComponent(); - vm.fileClicked(file1); + vm.fileClicked(RepoStore.files[0]); - expect(Helper.getContent).toHaveBeenCalledWith(file1); + expect(Helper.getContent).toHaveBeenCalledWith(RepoStore.files[0]); }); it('should not fetch data for already opened files', () => { - const file = { - id: 42, - url: 'foo', - }; - - spyOn(Helper, 'getFileFromPath').and.returnValue(file); + const f = file(); + spyOn(Helper, 'getFileFromPath').and.returnValue(f); spyOn(RepoStore, 'setActiveFiles'); vm = createComponent(); - vm.fileClicked(file); + vm.fileClicked(f); - expect(RepoStore.setActiveFiles).toHaveBeenCalledWith(file); + expect(RepoStore.setActiveFiles).toHaveBeenCalledWith(f); }); it('should hide files in directory if already open', () => { - spyOn(RepoStore, 'removeChildFilesOfTree').and.callThrough(); - const file1 = { - id: 0, - type: 'tree', - url: '', - opened: true, - }; - RepoStore.files = [file1]; - RepoStore.isRoot = true; + spyOn(Helper, 'setDirectoryToClosed').and.callThrough(); + const f = file(); + f.opened = true; + f.type = 'tree'; + RepoStore.files = [f]; vm = createComponent(); - vm.fileClicked(file1); + vm.fileClicked(RepoStore.files[0]); - expect(RepoStore.removeChildFilesOfTree).toHaveBeenCalledWith(file1); + expect(Helper.setDirectoryToClosed).toHaveBeenCalledWith(RepoStore.files[0]); }); }); @@ -131,36 +131,31 @@ describe('RepoSidebar', () => { }); describe('back button', () => { - const file1 = { - id: 1, - url: 'file1', - }; - const file2 = { - id: 2, - url: 'file2', - }; - RepoStore.files = [file1, file2]; - RepoStore.openedFiles = [file1, file2]; - RepoStore.isRoot = true; + beforeEach(() => { + const f = file(); + const file2 = Object.assign({}, file()); + file2.url = 'test'; + RepoStore.files = [f, file2]; + RepoStore.openedFiles = []; + RepoStore.isRoot = true; - vm = createComponent(); - vm.fileClicked(file1); + vm = createComponent(); + }); it('render previous file when using back button', () => { spyOn(Helper, 'getContent').and.callThrough(); - vm.fileClicked(file2); - expect(Helper.getContent).toHaveBeenCalledWith(file2); - Helper.getContent.calls.reset(); + vm.fileClicked(RepoStore.files[1]); + expect(Helper.getContent).toHaveBeenCalledWith(RepoStore.files[1]); history.pushState({ key: Math.random(), - }, '', file1.url); + }, '', RepoStore.files[1].url); const popEvent = document.createEvent('Event'); popEvent.initEvent('popstate', true, true); window.dispatchEvent(popEvent); - expect(Helper.getContent.calls.mostRecent().args[0].url).toContain(file1.url); + expect(Helper.getContent.calls.mostRecent().args[0].url).toContain(RepoStore.files[1].url); window.history.pushState({}, null, '/'); }); diff --git a/spec/javascripts/repo/components/repo_tab_spec.js b/spec/javascripts/repo/components/repo_tab_spec.js index d2a790ad73a..37e297437f0 100644 --- a/spec/javascripts/repo/components/repo_tab_spec.js +++ b/spec/javascripts/repo/components/repo_tab_spec.js @@ -1,5 +1,6 @@ import Vue from 'vue'; import repoTab from '~/repo/components/repo_tab.vue'; +import RepoStore from '~/repo/stores/repo_store'; describe('RepoTab', () => { function createComponent(propsData) { @@ -18,7 +19,7 @@ describe('RepoTab', () => { const vm = createComponent({ tab, }); - const close = vm.$el.querySelector('.close'); + const close = vm.$el.querySelector('.close-btn'); const name = vm.$el.querySelector(`a[title="${tab.url}"]`); spyOn(vm, 'closeTab'); @@ -44,26 +45,43 @@ describe('RepoTab', () => { tab, }); - expect(vm.$el.querySelector('.close .fa-circle')).toBeTruthy(); + expect(vm.$el.querySelector('.close-btn .fa-circle')).toBeTruthy(); }); describe('methods', () => { describe('closeTab', () => { - const vm = jasmine.createSpyObj('vm', ['$emit']); - it('returns undefined and does not $emit if file is changed', () => { - const file = { changed: true }; - const returnVal = repoTab.methods.closeTab.call(vm, file); + const tab = { + url: 'url', + name: 'name', + changed: true, + }; + const vm = createComponent({ + tab, + }); + + spyOn(RepoStore, 'removeFromOpenedFiles'); + + vm.$el.querySelector('.close-btn').click(); - expect(returnVal).toBeUndefined(); - expect(vm.$emit).not.toHaveBeenCalled(); + expect(RepoStore.removeFromOpenedFiles).not.toHaveBeenCalled(); }); it('$emits tabclosed event with file obj', () => { - const file = { changed: false }; - repoTab.methods.closeTab.call(vm, file); + const tab = { + url: 'url', + name: 'name', + changed: false, + }; + const vm = createComponent({ + tab, + }); + + spyOn(RepoStore, 'removeFromOpenedFiles'); + + vm.$el.querySelector('.close-btn').click(); - expect(vm.$emit).toHaveBeenCalledWith('tabclosed', file); + expect(RepoStore.removeFromOpenedFiles).toHaveBeenCalledWith(tab); }); }); }); diff --git a/spec/javascripts/repo/components/repo_tabs_spec.js b/spec/javascripts/repo/components/repo_tabs_spec.js index a02b54efafc..431129bc866 100644 --- a/spec/javascripts/repo/components/repo_tabs_spec.js +++ b/spec/javascripts/repo/components/repo_tabs_spec.js @@ -16,6 +16,10 @@ describe('RepoTabs', () => { return new RepoTabs().$mount(); } + afterEach(() => { + RepoStore.openedFiles = []; + }); + it('renders a list of tabs', () => { RepoStore.openedFiles = openedFiles; @@ -28,18 +32,4 @@ describe('RepoTabs', () => { expect(tabs[1].classList.contains('active')).toBeFalsy(); expect(tabs[2].classList.contains('tabs-divider')).toBeTruthy(); }); - - describe('methods', () => { - describe('tabClosed', () => { - it('calls removeFromOpenedFiles with file obj', () => { - const file = {}; - - spyOn(RepoStore, 'removeFromOpenedFiles'); - - repoTabs.methods.tabClosed(file); - - expect(RepoStore.removeFromOpenedFiles).toHaveBeenCalledWith(file); - }); - }); - }); }); diff --git a/spec/javascripts/repo/mock_data.js b/spec/javascripts/repo/mock_data.js new file mode 100644 index 00000000000..836b867205e --- /dev/null +++ b/spec/javascripts/repo/mock_data.js @@ -0,0 +1,13 @@ +import RepoHelper from '~/repo/helpers/repo_helper'; + +// eslint-disable-next-line import/prefer-default-export +export const file = (name = 'name') => RepoHelper.serializeRepoEntity('blob', { + icon: 'icon', + url: 'url', + name, + last_commit: { + id: '123', + message: 'test', + committed_date: '', + }, +}); diff --git a/spec/javascripts/vue_shared/components/user_avatar_image_spec.js b/spec/javascripts/vue_shared/components/user_avatar/user_avatar_image_spec.js index 8daa7610274..8daa7610274 100644 --- a/spec/javascripts/vue_shared/components/user_avatar_image_spec.js +++ b/spec/javascripts/vue_shared/components/user_avatar/user_avatar_image_spec.js diff --git a/spec/javascripts/vue_shared/components/user_avatar_link_spec.js b/spec/javascripts/vue_shared/components/user_avatar/user_avatar_link_spec.js index 52e450e9ba5..52e450e9ba5 100644 --- a/spec/javascripts/vue_shared/components/user_avatar_link_spec.js +++ b/spec/javascripts/vue_shared/components/user_avatar/user_avatar_link_spec.js diff --git a/spec/javascripts/vue_shared/components/user_avatar_svg_spec.js b/spec/javascripts/vue_shared/components/user_avatar/user_avatar_svg_spec.js index b8d639ffbec..b8d639ffbec 100644 --- a/spec/javascripts/vue_shared/components/user_avatar_svg_spec.js +++ b/spec/javascripts/vue_shared/components/user_avatar/user_avatar_svg_spec.js diff --git a/spec/lib/gitlab/background_migration/deserialize_merge_request_diffs_and_commits_spec.rb b/spec/lib/gitlab/background_migration/deserialize_merge_request_diffs_and_commits_spec.rb index d2e7243ee05..4d3fdbd9554 100644 --- a/spec/lib/gitlab/background_migration/deserialize_merge_request_diffs_and_commits_spec.rb +++ b/spec/lib/gitlab/background_migration/deserialize_merge_request_diffs_and_commits_spec.rb @@ -31,8 +31,8 @@ describe Gitlab::BackgroundMigration::DeserializeMergeRequestDiffsAndCommits, :t end it 'creates correct entries in the merge_request_diff_commits table' do - expect(updated_merge_request_diff.merge_request_diff_commits.count).to eq(commits.count) - expect(updated_merge_request_diff.commits.map(&:to_hash)).to eq(commits) + expect(updated_merge_request_diff.merge_request_diff_commits.count).to eq(expected_commits.count) + expect(updated_merge_request_diff.commits.map(&:to_hash)).to eq(expected_commits) end it 'creates correct entries in the merge_request_diff_files table' do @@ -199,6 +199,16 @@ describe Gitlab::BackgroundMigration::DeserializeMergeRequestDiffsAndCommits, :t context 'when the merge request diff has valid commits and diffs' do let(:commits) { merge_request_diff.commits.map(&:to_hash) } + let(:expected_commits) { commits } + let(:diffs) { diffs_to_hashes(merge_request_diff.merge_request_diff_files) } + let(:expected_diffs) { diffs } + + include_examples 'updated MR diff' + end + + context 'when the merge request diff has diffs but no commits' do + let(:commits) { nil } + let(:expected_commits) { [] } let(:diffs) { diffs_to_hashes(merge_request_diff.merge_request_diff_files) } let(:expected_diffs) { diffs } @@ -207,6 +217,7 @@ describe Gitlab::BackgroundMigration::DeserializeMergeRequestDiffsAndCommits, :t context 'when the merge request diffs do not have too_large set' do let(:commits) { merge_request_diff.commits.map(&:to_hash) } + let(:expected_commits) { commits } let(:expected_diffs) { diffs_to_hashes(merge_request_diff.merge_request_diff_files) } let(:diffs) do @@ -218,6 +229,7 @@ describe Gitlab::BackgroundMigration::DeserializeMergeRequestDiffsAndCommits, :t context 'when the merge request diffs do not have a_mode and b_mode set' do let(:commits) { merge_request_diff.commits.map(&:to_hash) } + let(:expected_commits) { commits } let(:expected_diffs) { diffs_to_hashes(merge_request_diff.merge_request_diff_files) } let(:diffs) do @@ -229,6 +241,7 @@ describe Gitlab::BackgroundMigration::DeserializeMergeRequestDiffsAndCommits, :t context 'when the merge request diffs have binary content' do let(:commits) { merge_request_diff.commits.map(&:to_hash) } + let(:expected_commits) { commits } let(:expected_diffs) { diffs } # The start of a PDF created by Illustrator @@ -257,6 +270,7 @@ describe Gitlab::BackgroundMigration::DeserializeMergeRequestDiffsAndCommits, :t context 'when the merge request diff has commits, but no diffs' do let(:commits) { merge_request_diff.commits.map(&:to_hash) } + let(:expected_commits) { commits } let(:diffs) { [] } let(:expected_diffs) { diffs } @@ -265,6 +279,7 @@ describe Gitlab::BackgroundMigration::DeserializeMergeRequestDiffsAndCommits, :t context 'when the merge request diffs have invalid content' do let(:commits) { merge_request_diff.commits.map(&:to_hash) } + let(:expected_commits) { commits } let(:diffs) { ['--broken-diff'] } let(:expected_diffs) { [] } @@ -274,6 +289,7 @@ describe Gitlab::BackgroundMigration::DeserializeMergeRequestDiffsAndCommits, :t context 'when the merge request diffs are Rugged::Patch instances' do let(:commits) { merge_request_diff.commits.map(&:to_hash) } let(:first_commit) { merge_request.project.repository.commit(merge_request_diff.head_commit_sha) } + let(:expected_commits) { commits } let(:diffs) { first_commit.rugged_diff_from_parent.patches } let(:expected_diffs) { [] } @@ -283,6 +299,7 @@ describe Gitlab::BackgroundMigration::DeserializeMergeRequestDiffsAndCommits, :t context 'when the merge request diffs are Rugged::Diff::Delta instances' do let(:commits) { merge_request_diff.commits.map(&:to_hash) } let(:first_commit) { merge_request.project.repository.commit(merge_request_diff.head_commit_sha) } + let(:expected_commits) { commits } let(:diffs) { first_commit.rugged_diff_from_parent.deltas } let(:expected_diffs) { [] } diff --git a/spec/lib/gitlab/git/storage/circuit_breaker_spec.rb b/spec/lib/gitlab/git/storage/circuit_breaker_spec.rb index 68eb93eb9f9..c8d532df059 100644 --- a/spec/lib/gitlab/git/storage/circuit_breaker_spec.rb +++ b/spec/lib/gitlab/git/storage/circuit_breaker_spec.rb @@ -41,6 +41,10 @@ describe Gitlab::Git::Storage::CircuitBreaker, clean_gitlab_redis_shared_state: expect(key_exists).to be_falsey end + + it 'does not break when there are no keys in redis' do + expect { described_class.reset_all! }.not_to raise_error + end end describe '.for_storage' do diff --git a/spec/lib/gitlab/git/storage/health_spec.rb b/spec/lib/gitlab/git/storage/health_spec.rb index 2d3af387971..4a14a5201d1 100644 --- a/spec/lib/gitlab/git/storage/health_spec.rb +++ b/spec/lib/gitlab/git/storage/health_spec.rb @@ -20,36 +20,6 @@ describe Gitlab::Git::Storage::Health, clean_gitlab_redis_shared_state: true, br end end - describe '.load_for_keys' do - let(:subject) do - results = Gitlab::Git::Storage.redis.with do |redis| - fake_future = double - allow(fake_future).to receive(:value).and_return([host1_key]) - described_class.load_for_keys({ 'broken' => fake_future }, redis) - end - - # Make sure the `Redis#future is loaded - results.inject({}) do |result, (name, info)| - info.each { |i| i[:failure_count] = i[:failure_count].value.to_i } - - result[name] = info - - result - end - end - - it 'loads when there is no info in redis' do - expect(subject).to eq('broken' => [{ name: host1_key, failure_count: 0 }]) - end - - it 'reads the correct values for a storage from redis' do - set_in_redis(host1_key, 5) - set_in_redis(host2_key, 7) - - expect(subject).to eq('broken' => [{ name: host1_key, failure_count: 5 }]) - end - end - describe '.for_all_storages' do it 'loads health status for all configured storages' do healths = described_class.for_all_storages diff --git a/spec/models/application_setting_spec.rb b/spec/models/application_setting_spec.rb index cf192691507..6945c90cb9b 100644 --- a/spec/models/application_setting_spec.rb +++ b/spec/models/application_setting_spec.rb @@ -222,6 +222,16 @@ describe ApplicationSetting do end end + context 'restrict creating duplicates' do + before do + described_class.create_from_defaults + end + + it 'raises an record creation violation if already created' do + expect { described_class.create_from_defaults }.to raise_error(ActiveRecord::RecordNotUnique) + end + end + context 'restricted signup domains' do it 'sets single domain' do setting.domain_whitelist_raw = 'example.com' diff --git a/spec/serializers/container_tag_entity_spec.rb b/spec/serializers/container_tag_entity_spec.rb index 6dcc5204516..4beb50c70f8 100644 --- a/spec/serializers/container_tag_entity_spec.rb +++ b/spec/serializers/container_tag_entity_spec.rb @@ -22,7 +22,7 @@ describe ContainerTagEntity do end it 'exposes required informations' do - expect(subject).to include(:name, :location, :revision, :total_size, :created_at) + expect(subject).to include(:name, :location, :revision, :short_revision, :total_size, :created_at) end context 'when user can manage repositories' do diff --git a/spec/services/projects/destroy_service_spec.rb b/spec/services/projects/destroy_service_spec.rb index c90bad46295..0bec2054f50 100644 --- a/spec/services/projects/destroy_service_spec.rb +++ b/spec/services/projects/destroy_service_spec.rb @@ -1,6 +1,8 @@ require 'spec_helper' describe Projects::DestroyService do + include ProjectForksHelper + let!(:user) { create(:user) } let!(:project) { create(:project, :repository, namespace: user.namespace) } let!(:path) { project.repository.path_to_repo } @@ -212,6 +214,21 @@ describe Projects::DestroyService do end end + context 'for a forked project with LFS objects' do + let(:forked_project) { fork_project(project, user) } + + before do + project.lfs_objects << create(:lfs_object) + forked_project.forked_project_link.destroy + forked_project.reload + end + + it 'destroys the fork' do + expect { destroy_project(forked_project, user) } + .not_to raise_error + end + end + context 'as the root of a fork network' do let!(:fork_network) { create(:fork_network, root_project: project) } diff --git a/spec/support/redis_without_keys.rb b/spec/support/redis_without_keys.rb new file mode 100644 index 00000000000..6220167dee6 --- /dev/null +++ b/spec/support/redis_without_keys.rb @@ -0,0 +1,8 @@ +class Redis + ForbiddenCommand = Class.new(StandardError) + + def keys(*args) + raise ForbiddenCommand.new("Don't use `Redis#keys` as it iterates over all "\ + "keys in redis. Use `Redis#scan_each` instead.") + end +end |