summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--app/assets/javascripts/clusters.js5
-rw-r--r--app/assets/javascripts/issue_show/components/app.vue16
-rw-r--r--app/assets/javascripts/issue_show/components/title.vue56
-rw-r--r--app/assets/javascripts/registry/stores/mutations.js2
-rw-r--r--app/assets/javascripts/repo/components/repo.vue4
-rw-r--r--app/assets/javascripts/repo/components/repo_commit_section.vue4
-rw-r--r--app/assets/javascripts/repo/components/repo_edit_button.vue4
-rw-r--r--app/assets/javascripts/repo/components/repo_editor.vue6
-rw-r--r--app/assets/javascripts/repo/components/repo_file.vue161
-rw-r--r--app/assets/javascripts/repo/components/repo_file_buttons.vue4
-rw-r--r--app/assets/javascripts/repo/components/repo_file_options.vue25
-rw-r--r--app/assets/javascripts/repo/components/repo_loading_file.vue87
-rw-r--r--app/assets/javascripts/repo/components/repo_prev_directory.vue58
-rw-r--r--app/assets/javascripts/repo/components/repo_preview.vue4
-rw-r--r--app/assets/javascripts/repo/components/repo_sidebar.vue80
-rw-r--r--app/assets/javascripts/repo/components/repo_tab.vue51
-rw-r--r--app/assets/javascripts/repo/components/repo_tabs.vue51
-rw-r--r--app/assets/javascripts/repo/event_hub.js3
-rw-r--r--app/assets/javascripts/repo/helpers/repo_helper.js182
-rw-r--r--app/assets/javascripts/repo/index.js3
-rw-r--r--app/assets/javascripts/repo/stores/repo_store.js46
-rw-r--r--app/assets/stylesheets/framework/animations.scss7
-rw-r--r--app/assets/stylesheets/framework/new-sidebar.scss2
-rw-r--r--app/assets/stylesheets/pages/clusters.scss2
-rw-r--r--app/assets/stylesheets/pages/issuable.scss10
-rw-r--r--app/assets/stylesheets/pages/notes.scss4
-rw-r--r--app/assets/stylesheets/pages/repo.scss82
-rw-r--r--app/controllers/projects/tree_controller.rb1
-rw-r--r--app/controllers/projects_controller.rb2
-rw-r--r--app/models/application_setting.rb2
-rw-r--r--app/models/project.rb2
-rw-r--r--app/serializers/container_tag_entity.rb2
-rw-r--r--app/services/projects/unlink_fork_service.rb2
-rw-r--r--app/views/projects/clusters/_advanced_settings.html.haml14
-rw-r--r--app/views/projects/clusters/show.html.haml90
-rw-r--r--app/views/shared/repo/_repo.html.haml3
-rw-r--r--changelogs/unreleased/bvl-do-not-use-redis-keys.yml5
-rw-r--r--changelogs/unreleased/bvl-fix-deleting-forked-projects.yml5
-rw-r--r--changelogs/unreleased/kt-bug-fix-revision-and-size-for-container-registry.yml5
-rw-r--r--changelogs/unreleased/prevent-creating-multiple-application-settings.yml5
-rw-r--r--config/database.yml.mysql21
-rw-r--r--config/database.yml.postgresql21
-rw-r--r--doc/user/project/issues/automatic_issue_closing.md8
-rw-r--r--lib/gitlab/background_migration/deserialize_merge_request_diffs_and_commits.rb1
-rw-r--r--lib/gitlab/git/storage/circuit_breaker.rb2
-rw-r--r--lib/gitlab/git/storage/health.rb20
-rw-r--r--scripts/prepare_build.sh6
-rw-r--r--spec/features/projects_spec.rb2
-rw-r--r--spec/fixtures/api/schemas/registry/tag.json5
-rw-r--r--spec/javascripts/issue_show/components/app_spec.js11
-rw-r--r--spec/javascripts/issue_show/components/title_spec.js39
-rw-r--r--spec/javascripts/lib/utils/common_utils_spec.js28
-rw-r--r--spec/javascripts/registry/mock_data.js8
-rw-r--r--spec/javascripts/repo/components/repo_commit_section_spec.js1
-rw-r--r--spec/javascripts/repo/components/repo_edit_button_spec.js12
-rw-r--r--spec/javascripts/repo/components/repo_editor_spec.js5
-rw-r--r--spec/javascripts/repo/components/repo_file_buttons_spec.js4
-rw-r--r--spec/javascripts/repo/components/repo_file_options_spec.js33
-rw-r--r--spec/javascripts/repo/components/repo_file_spec.js103
-rw-r--r--spec/javascripts/repo/components/repo_loading_file_spec.js29
-rw-r--r--spec/javascripts/repo/components/repo_prev_directory_spec.js16
-rw-r--r--spec/javascripts/repo/components/repo_sidebar_spec.js117
-rw-r--r--spec/javascripts/repo/components/repo_tab_spec.js40
-rw-r--r--spec/javascripts/repo/components/repo_tabs_spec.js18
-rw-r--r--spec/javascripts/repo/mock_data.js13
-rw-r--r--spec/javascripts/vue_shared/components/user_avatar/user_avatar_image_spec.js (renamed from spec/javascripts/vue_shared/components/user_avatar_image_spec.js)0
-rw-r--r--spec/javascripts/vue_shared/components/user_avatar/user_avatar_link_spec.js (renamed from spec/javascripts/vue_shared/components/user_avatar_link_spec.js)0
-rw-r--r--spec/javascripts/vue_shared/components/user_avatar/user_avatar_svg_spec.js (renamed from spec/javascripts/vue_shared/components/user_avatar_svg_spec.js)0
-rw-r--r--spec/lib/gitlab/background_migration/deserialize_merge_request_diffs_and_commits_spec.rb21
-rw-r--r--spec/lib/gitlab/git/storage/circuit_breaker_spec.rb4
-rw-r--r--spec/lib/gitlab/git/storage/health_spec.rb30
-rw-r--r--spec/models/application_setting_spec.rb10
-rw-r--r--spec/serializers/container_tag_entity_spec.rb2
-rw-r--r--spec/services/projects/destroy_service_spec.rb17
-rw-r--r--spec/support/redis_without_keys.rb8
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