Welcome to mirror list, hosted at ThFree Co, Russian Federation.

gitlab.com/gitlab-org/gitlab-foss.git - Unnamed repository; edit this file 'description' to name the repository.
summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--app/assets/javascripts/behaviors/markdown/gfm_auto_complete.js1
-rw-r--r--app/assets/javascripts/boards/components/board_card_layout.vue24
-rw-r--r--app/assets/javascripts/boards/components/board_column.vue30
-rw-r--r--app/assets/javascripts/boards/components/board_content.vue24
-rw-r--r--app/assets/javascripts/boards/components/board_list.vue22
-rw-r--r--app/assets/javascripts/boards/components/board_list_header.vue9
-rw-r--r--app/assets/javascripts/boards/components/board_new_issue.vue23
-rw-r--r--app/assets/javascripts/boards/components/issue_card_inner.vue15
-rw-r--r--app/assets/javascripts/boards/components/modal/index.vue15
-rw-r--r--app/assets/javascripts/boards/components/modal/list.vue10
-rw-r--r--app/assets/javascripts/boards/components/project_select.vue10
-rw-r--r--app/assets/javascripts/boards/index.js12
-rw-r--r--app/assets/javascripts/gfm_auto_complete.js5
-rw-r--r--app/assets/javascripts/ide/components/ide_status_list.vue7
-rw-r--r--app/assets/javascripts/ide/components/new_dropdown/upload.vue3
-rw-r--r--app/assets/javascripts/ide/components/repo_editor.vue4
-rw-r--r--app/assets/javascripts/ide/lib/files.js11
-rw-r--r--app/assets/javascripts/ide/stores/actions.js12
-rw-r--r--app/assets/javascripts/ide/stores/utils.js3
-rw-r--r--app/assets/javascripts/ide/utils.js9
-rw-r--r--app/assets/javascripts/integrations/edit/components/integration_form.vue6
-rw-r--r--app/assets/javascripts/integrations/edit/components/override_dropdown.vue4
-rw-r--r--app/assets/javascripts/integrations/edit/index.js8
-rw-r--r--app/assets/javascripts/integrations/edit/store/getters.js4
-rw-r--r--app/assets/javascripts/integrations/edit/store/state.js6
-rw-r--r--app/assets/javascripts/integrations/integration_settings_form.js2
-rw-r--r--app/assets/javascripts/issuable_create/components/issuable_form.vue7
-rw-r--r--app/assets/javascripts/issuable_list/components/issuable_item.vue140
-rw-r--r--app/assets/javascripts/issuable_list/components/issuable_list_root.vue153
-rw-r--r--app/assets/javascripts/issuable_list/components/issuable_tabs.vue53
-rw-r--r--app/assets/javascripts/shared/milestones/form.js1
-rw-r--r--app/assets/javascripts/snippet/snippet_edit.js1
-rw-r--r--app/assets/javascripts/vue_shared/components/markdown/field.vue1
-rw-r--r--app/controllers/groups/settings/integrations_controller.rb2
-rw-r--r--app/controllers/projects/issues_controller.rb8
-rw-r--r--app/controllers/projects/services_controller.rb2
-rw-r--r--app/helpers/boards_helper.rb4
-rw-r--r--app/models/ci/daily_build_group_report_result.rb2
-rw-r--r--app/models/issue.rb5
-rw-r--r--app/models/protected_branch.rb4
-rw-r--r--app/services/projects/open_issues_count_service.rb6
-rw-r--r--app/views/projects/issues/_design_management.html.haml2
-rw-r--r--app/views/projects/packages/packages/show.html.haml2
-rw-r--r--app/views/shared/_service_settings.html.haml4
-rw-r--r--app/views/shared/boards/_show.html.haml13
-rw-r--r--app/workers/git_garbage_collect_worker.rb12
-rw-r--r--changelogs/unreleased/246655-hashed-storage-trigger-migration-to-hashed-storage-via-post-deploy.yml5
-rw-r--r--changelogs/unreleased/249910-does-not-update-repository-statistics-when-running-on-a-read-only-.yml6
-rw-r--r--changelogs/unreleased/41818.yml5
-rw-r--r--changelogs/unreleased/add_deduplicated_column_to_security_findings.yml6
-rw-r--r--changelogs/unreleased/improve-group-coverage-query-performance.yml5
-rw-r--r--changelogs/unreleased/update-conan-installation-snippet.yml5
-rw-r--r--db/migrate/20200914155135_add_deduplicated_flag_into_security_findings_table.rb9
-rw-r--r--db/migrate/20200914183227_add_index_on_deduplicated_column_of_security_findings.rb18
-rw-r--r--db/migrate/20200914184212_remove_index_on_security_findings_scan_id.rb18
-rw-r--r--db/post_migrate/20200915044225_schedule_migration_to_hashed_storage.rb17
-rw-r--r--db/schema_migrations/202009141551351
-rw-r--r--db/schema_migrations/202009141832271
-rw-r--r--db/schema_migrations/202009141842121
-rw-r--r--db/schema_migrations/202009150442251
-rw-r--r--db/structure.sql3
-rw-r--r--doc/ci/parent_child_pipelines.md38
-rw-r--r--doc/install/azure/index.md18
-rw-r--r--lib/gitlab/background_migration/migrate_to_hashed_storage.rb61
-rw-r--r--lib/gitlab/hashed_storage/migrator.rb8
-rw-r--r--locale/gitlab.pot17
-rw-r--r--qa/qa/page/project/issue/show.rb5
-rw-r--r--qa/qa/specs/features/browser_ui/2_plan/related_issues/related_issues_spec.rb11
-rw-r--r--spec/controllers/projects/issues_controller_spec.rb10
-rw-r--r--spec/features/projects/issues/design_management/user_uploads_designs_spec.rb2
-rw-r--r--spec/frontend/boards/board_list_helper.js6
-rw-r--r--spec/frontend/boards/board_list_spec.js6
-rw-r--r--spec/frontend/boards/board_new_issue_spec.js59
-rw-r--r--spec/frontend/boards/components/board_card_layout_spec.js6
-rw-r--r--spec/frontend/boards/components/board_card_spec.js6
-rw-r--r--spec/frontend/boards/components/board_column_spec.js5
-rw-r--r--spec/frontend/boards/components/board_content_spec.js4
-rw-r--r--spec/frontend/boards/components/board_list_header_spec.js6
-rw-r--r--spec/frontend/boards/issue_card_spec.js6
-rw-r--r--spec/frontend/gfm_auto_complete_spec.js3
-rw-r--r--spec/frontend/ide/components/ide_status_list_spec.js3
-rw-r--r--spec/frontend/ide/components/new_dropdown/upload_spec.js2
-rw-r--r--spec/frontend/ide/components/repo_editor_spec.js13
-rw-r--r--spec/frontend/ide/lib/files_spec.js4
-rw-r--r--spec/frontend/ide/stores/actions/file_spec.js2
-rw-r--r--spec/frontend/ide/stores/mutations/file_spec.js2
-rw-r--r--spec/frontend/ide/utils_spec.js100
-rw-r--r--spec/frontend/integrations/edit/components/integration_form_spec.js8
-rw-r--r--spec/frontend/integrations/edit/components/override_dropdown_spec.js6
-rw-r--r--spec/frontend/integrations/edit/store/getters_spec.js14
-rw-r--r--spec/frontend/integrations/edit/store/state_spec.js8
-rw-r--r--spec/frontend/issuable_create/components/issuable_form_spec.js1
-rw-r--r--spec/frontend/issuable_list/components/issuable_item_spec.js185
-rw-r--r--spec/frontend/issuable_list/components/issuable_list_root_spec.js160
-rw-r--r--spec/frontend/issuable_list/components/issuable_tabs_spec.js91
-rw-r--r--spec/frontend/issuable_list/mock_data.js135
-rw-r--r--spec/lib/gitlab/background_migration/migrate_to_hashed_storage_spec.rb43
-rw-r--r--spec/lib/gitlab/hashed_storage/migrator_spec.rb12
-rw-r--r--spec/lib/gitlab/import_export/all_models.yml1
-rw-r--r--spec/migrations/20200915044225_schedule_migration_to_hashed_storage_spec.rb14
-rw-r--r--spec/services/projects/open_issues_count_service_spec.rb8
-rw-r--r--spec/workers/git_garbage_collect_worker_spec.rb21
102 files changed, 1527 insertions, 385 deletions
diff --git a/app/assets/javascripts/behaviors/markdown/gfm_auto_complete.js b/app/assets/javascripts/behaviors/markdown/gfm_auto_complete.js
index 83f2ca0bdc2..d712c90242c 100644
--- a/app/assets/javascripts/behaviors/markdown/gfm_auto_complete.js
+++ b/app/assets/javascripts/behaviors/markdown/gfm_auto_complete.js
@@ -14,6 +14,7 @@ export default function initGFMInput($els) {
milestones: enableGFM,
mergeRequests: enableGFM,
labels: enableGFM,
+ vulnerabilities: enableGFM,
});
});
}
diff --git a/app/assets/javascripts/boards/components/board_card_layout.vue b/app/assets/javascripts/boards/components/board_card_layout.vue
index e3b57d5a7f7..072dd87861a 100644
--- a/app/assets/javascripts/boards/components/board_card_layout.vue
+++ b/app/assets/javascripts/boards/components/board_card_layout.vue
@@ -1,5 +1,4 @@
<script>
-/* eslint-disable vue/require-default-prop */
import IssueCardInner from './issue_card_inner.vue';
import boardsStore from '../stores/boards_store';
@@ -19,11 +18,6 @@ export default {
default: () => ({}),
required: false,
},
- issueLinkBase: {
- type: String,
- default: '',
- required: false,
- },
disabled: {
type: Boolean,
default: false,
@@ -34,15 +28,6 @@ export default {
default: 0,
required: false,
},
- rootPath: {
- type: String,
- default: '',
- required: false,
- },
- groupId: {
- type: Number,
- required: false,
- },
isActive: {
type: Boolean,
required: false,
@@ -103,13 +88,6 @@ export default {
@mousemove="mouseMove"
@mouseup="showIssue($event)"
>
- <issue-card-inner
- :list="list"
- :issue="issue"
- :issue-link-base="issueLinkBase"
- :group-id="groupId"
- :root-path="rootPath"
- :update-filters="true"
- />
+ <issue-card-inner :list="list" :issue="issue" :update-filters="true" />
</li>
</template>
diff --git a/app/assets/javascripts/boards/components/board_column.vue b/app/assets/javascripts/boards/components/board_column.vue
index 23e4edea40f..6d216911798 100644
--- a/app/assets/javascripts/boards/components/board_column.vue
+++ b/app/assets/javascripts/boards/components/board_column.vue
@@ -34,27 +34,15 @@ export default {
type: Boolean,
required: true,
},
- issueLinkBase: {
- type: String,
- required: true,
- },
- rootPath: {
- type: String,
- required: true,
- },
- boardId: {
- type: String,
- required: true,
- },
canAdminList: {
type: Boolean,
required: false,
default: false,
},
- groupId: {
- type: Number,
- required: false,
- default: null,
+ },
+ inject: {
+ boardId: {
+ type: String,
},
},
data() {
@@ -151,22 +139,14 @@ export default {
<div
class="board-inner gl-display-flex gl-flex-direction-column gl-relative gl-h-full gl-rounded-base"
>
- <board-list-header
- :can-admin-list="canAdminList"
- :list="list"
- :disabled="disabled"
- :board-id="boardId"
- />
+ <board-list-header :can-admin-list="canAdminList" :list="list" :disabled="disabled" />
<board-list
v-if="showBoardListAndBoardInfo"
ref="board-list"
:disabled="disabled"
- :group-id="groupId || null"
- :issue-link-base="issueLinkBase"
:issues="listIssues"
:list="list"
:loading="list.loading"
- :root-path="rootPath"
/>
<board-blank-state v-if="canAdminList && list.id === 'blank'" />
diff --git a/app/assets/javascripts/boards/components/board_content.vue b/app/assets/javascripts/boards/components/board_content.vue
index d3c0e160153..c7b3da0e672 100644
--- a/app/assets/javascripts/boards/components/board_content.vue
+++ b/app/assets/javascripts/boards/components/board_content.vue
@@ -21,27 +21,10 @@ export default {
type: Boolean,
required: true,
},
- groupId: {
- type: Number,
- required: false,
- default: null,
- },
disabled: {
type: Boolean,
required: true,
},
- issueLinkBase: {
- type: String,
- required: true,
- },
- rootPath: {
- type: String,
- required: true,
- },
- boardId: {
- type: String,
- required: true,
- },
},
computed: {
...mapState(['boardLists', 'error']),
@@ -77,12 +60,8 @@ export default {
:key="list.id"
ref="board"
:can-admin-list="canAdminList"
- :group-id="groupId"
:list="list"
:disabled="disabled"
- :issue-link-base="issueLinkBase"
- :root-path="rootPath"
- :board-id="boardId"
/>
</div>
@@ -92,9 +71,6 @@ export default {
:lists="boardLists"
:can-admin-list="canAdminList"
:disabled="disabled"
- :board-id="boardId"
- :group-id="groupId"
- :root-path="rootPath"
/>
<board-content-sidebar />
</template>
diff --git a/app/assets/javascripts/boards/components/board_list.vue b/app/assets/javascripts/boards/components/board_list.vue
index 6b19f7f3353..25f8ffca633 100644
--- a/app/assets/javascripts/boards/components/board_list.vue
+++ b/app/assets/javascripts/boards/components/board_list.vue
@@ -27,11 +27,6 @@ export default {
},
mixins: [glFeatureFlagMixin()],
props: {
- groupId: {
- type: Number,
- required: false,
- default: 0,
- },
disabled: {
type: Boolean,
required: true,
@@ -48,14 +43,6 @@ export default {
type: Boolean,
required: true,
},
- issueLinkBase: {
- type: String,
- required: true,
- },
- rootPath: {
- type: String,
- required: true,
- },
},
data() {
return {
@@ -435,11 +422,7 @@ export default {
<div v-if="loading" class="board-list-loading text-center" :aria-label="__('Loading issues')">
<gl-loading-icon />
</div>
- <board-new-issue
- v-if="list.type !== 'closed' && showIssueForm"
- :group-id="groupId"
- :list="list"
- />
+ <board-new-issue v-if="list.type !== 'closed' && showIssueForm" :list="list" />
<ul
v-show="!loading"
ref="list"
@@ -455,9 +438,6 @@ export default {
:index="index"
:list="list"
:issue="issue"
- :issue-link-base="issueLinkBase"
- :group-id="groupId"
- :root-path="rootPath"
:disabled="disabled"
/>
<li v-if="showCount" class="board-list-count text-center" data-issue-id="-1">
diff --git a/app/assets/javascripts/boards/components/board_list_header.vue b/app/assets/javascripts/boards/components/board_list_header.vue
index 0f27961cb3f..361fe252afb 100644
--- a/app/assets/javascripts/boards/components/board_list_header.vue
+++ b/app/assets/javascripts/boards/components/board_list_header.vue
@@ -45,10 +45,6 @@ export default {
type: Boolean,
required: true,
},
- boardId: {
- type: String,
- required: true,
- },
canAdminList: {
type: Boolean,
required: false,
@@ -60,6 +56,11 @@ export default {
default: false,
},
},
+ inject: {
+ boardId: {
+ type: String,
+ },
+ },
data() {
return {
weightFeatureAvailable: false,
diff --git a/app/assets/javascripts/boards/components/board_new_issue.vue b/app/assets/javascripts/boards/components/board_new_issue.vue
index 924874d4dd2..348d485ff37 100644
--- a/app/assets/javascripts/boards/components/board_new_issue.vue
+++ b/app/assets/javascripts/boards/components/board_new_issue.vue
@@ -17,16 +17,16 @@ export default {
},
mixins: [glFeatureFlagMixin()],
props: {
- groupId: {
- type: Number,
- required: false,
- default: 0,
- },
list: {
type: Object,
required: true,
},
},
+ inject: {
+ groupId: {
+ type: Number,
+ },
+ },
data() {
return {
title: '',
@@ -139,7 +139,7 @@ export default {
<project-select v-if="groupId" :group-id="groupId" :list="list" />
<div class="clearfix gl-mt-3">
<gl-button
- ref="submit-button"
+ ref="submitButton"
:disabled="disabled"
class="float-left"
variant="success"
@@ -147,9 +147,14 @@ export default {
type="submit"
>{{ __('Submit issue') }}</gl-button
>
- <gl-button class="float-right" type="button" variant="default" @click="cancel">{{
- __('Cancel')
- }}</gl-button>
+ <gl-button
+ ref="cancelButton"
+ class="float-right"
+ type="button"
+ variant="default"
+ @click="cancel"
+ >{{ __('Cancel') }}</gl-button
+ >
</div>
</form>
</div>
diff --git a/app/assets/javascripts/boards/components/issue_card_inner.vue b/app/assets/javascripts/boards/components/issue_card_inner.vue
index 3184983ff22..8658f51e5cf 100644
--- a/app/assets/javascripts/boards/components/issue_card_inner.vue
+++ b/app/assets/javascripts/boards/components/issue_card_inner.vue
@@ -30,28 +30,23 @@ export default {
type: Object,
required: true,
},
- issueLinkBase: {
- type: String,
- required: true,
- },
list: {
type: Object,
required: false,
default: () => ({}),
},
- rootPath: {
- type: String,
- required: true,
- },
updateFilters: {
type: Boolean,
required: false,
default: false,
},
+ },
+ inject: {
groupId: {
type: Number,
- required: false,
- default: null,
+ },
+ rootPath: {
+ type: String,
},
},
data() {
diff --git a/app/assets/javascripts/boards/components/modal/index.vue b/app/assets/javascripts/boards/components/modal/index.vue
index fb2d7b6dbc5..817b3bdddb0 100644
--- a/app/assets/javascripts/boards/components/modal/index.vue
+++ b/app/assets/javascripts/boards/components/modal/index.vue
@@ -26,14 +26,6 @@ export default {
type: String,
required: true,
},
- issueLinkBase: {
- type: String,
- required: true,
- },
- rootPath: {
- type: String,
- required: true,
- },
projectId: {
type: Number,
required: true,
@@ -146,12 +138,7 @@ export default {
>
<div class="add-issues-container d-flex flex-column m-auto rounded">
<modal-header :project-id="projectId" :label-path="labelPath" />
- <modal-list
- v-if="!loading && showList && !filterLoading"
- :issue-link-base="issueLinkBase"
- :root-path="rootPath"
- :empty-state-svg="emptyStateSvg"
- />
+ <modal-list v-if="!loading && showList && !filterLoading" :empty-state-svg="emptyStateSvg" />
<empty-state
v-if="showEmptyState"
:new-issue-path="newIssuePath"
diff --git a/app/assets/javascripts/boards/components/modal/list.vue b/app/assets/javascripts/boards/components/modal/list.vue
index 9b8562f2ce5..219263bd9b9 100644
--- a/app/assets/javascripts/boards/components/modal/list.vue
+++ b/app/assets/javascripts/boards/components/modal/list.vue
@@ -10,14 +10,6 @@ export default {
GlIcon,
},
props: {
- issueLinkBase: {
- type: String,
- required: true,
- },
- rootPath: {
- type: String,
- required: true,
- },
emptyStateSvg: {
type: String,
required: true,
@@ -134,7 +126,7 @@ export default {
class="board-card position-relative p-3 rounded"
@click="toggleIssue($event, issue)"
>
- <issue-card-inner :issue="issue" :issue-link-base="issueLinkBase" :root-path="rootPath" />
+ <issue-card-inner :issue="issue" />
<gl-icon
v-if="issue.selected"
:aria-label="'Issue #' + issue.id + ' selected'"
diff --git a/app/assets/javascripts/boards/components/project_select.vue b/app/assets/javascripts/boards/components/project_select.vue
index b4657ffcc35..59e7620962a 100644
--- a/app/assets/javascripts/boards/components/project_select.vue
+++ b/app/assets/javascripts/boards/components/project_select.vue
@@ -15,16 +15,16 @@ export default {
GlLoadingIcon,
},
props: {
- groupId: {
- type: Number,
- required: true,
- default: 0,
- },
list: {
type: Object,
required: true,
},
},
+ inject: {
+ groupId: {
+ type: Number,
+ },
+ },
data() {
return {
loading: true,
diff --git a/app/assets/javascripts/boards/index.js b/app/assets/javascripts/boards/index.js
index 2f3a0c7351d..1173c6d0578 100644
--- a/app/assets/javascripts/boards/index.js
+++ b/app/assets/javascripts/boards/index.js
@@ -82,12 +82,13 @@ export default () => {
BoardAddIssuesModal,
BoardSettingsSidebar: () => import('~/boards/components/board_settings_sidebar.vue'),
},
- store,
- apolloProvider,
provide: {
- // TODO: Mv all non-reactive props from data/props to here.
+ boardId: $boardApp.dataset.boardId,
+ groupId: Number($boardApp.dataset.groupId) || null,
rootPath: $boardApp.dataset.rootPath,
},
+ store,
+ apolloProvider,
data() {
return {
state: boardsStore.state,
@@ -95,10 +96,7 @@ export default () => {
boardsEndpoint: $boardApp.dataset.boardsEndpoint,
recentBoardsEndpoint: $boardApp.dataset.recentBoardsEndpoint,
listsEndpoint: $boardApp.dataset.listsEndpoint,
- boardId: $boardApp.dataset.boardId,
disabled: parseBoolean($boardApp.dataset.disabled),
- issueLinkBase: $boardApp.dataset.issueLinkBase,
- rootPath: $boardApp.dataset.rootPath,
bulkUpdatePath: $boardApp.dataset.bulkUpdatePath,
detailIssue: boardsStore.detail,
parent: $boardApp.dataset.parent,
@@ -116,7 +114,7 @@ export default () => {
recentBoardsEndpoint: this.recentBoardsEndpoint,
listsEndpoint: this.listsEndpoint,
bulkUpdatePath: this.bulkUpdatePath,
- boardId: this.boardId,
+ boardId: $boardApp.dataset.boardId,
fullPath: $boardApp.dataset.fullPath,
};
this.setInitialBoardData({
diff --git a/app/assets/javascripts/gfm_auto_complete.js b/app/assets/javascripts/gfm_auto_complete.js
index 3e6b223ca93..409733c73b9 100644
--- a/app/assets/javascripts/gfm_auto_complete.js
+++ b/app/assets/javascripts/gfm_auto_complete.js
@@ -52,6 +52,7 @@ export const defaultAutocompleteConfig = {
milestones: true,
labels: true,
snippets: true,
+ vulnerabilities: true,
};
class GfmAutoComplete {
@@ -647,7 +648,8 @@ class GfmAutoComplete {
// https://github.com/ichord/At.js
const atSymbolsWithBar = Object.keys(controllers)
.join('|')
- .replace(/[$]/, '\\$&');
+ .replace(/[$]/, '\\$&')
+ .replace(/[+]/, '\\+');
const atSymbolsWithoutBar = Object.keys(controllers).join('');
const targetSubtext = subtext.split(GfmAutoComplete.regexSubtext).pop();
const resultantFlag = flag.replace(/[-[\]/{}()*+?.\\^$|]/g, '\\$&');
@@ -678,6 +680,7 @@ GfmAutoComplete.atTypeMap = {
'~': 'labels',
'%': 'milestones',
'/': 'commands',
+ '+': 'vulnerabilities',
$: 'snippets',
};
diff --git a/app/assets/javascripts/ide/components/ide_status_list.vue b/app/assets/javascripts/ide/components/ide_status_list.vue
index 1354fdc3d98..caa122f6ed2 100644
--- a/app/assets/javascripts/ide/components/ide_status_list.vue
+++ b/app/assets/javascripts/ide/components/ide_status_list.vue
@@ -2,7 +2,7 @@
import { mapGetters } from 'vuex';
import { GlLink, GlTooltipDirective } from '@gitlab/ui';
import TerminalSyncStatusSafe from './terminal_sync/terminal_sync_status_safe.vue';
-import { getFileEOL } from '../utils';
+import { isTextFile, getFileEOL } from '~/ide/utils';
export default {
components: {
@@ -17,6 +17,9 @@ export default {
activeFileEOL() {
return getFileEOL(this.activeFile.content);
},
+ activeFileIsText() {
+ return isTextFile(this.activeFile);
+ },
},
};
</script>
@@ -30,7 +33,7 @@ export default {
</gl-link>
</div>
<div>{{ activeFileEOL }}</div>
- <div v-if="!activeFile.binary">{{ activeFile.editorRow }}:{{ activeFile.editorColumn }}</div>
+ <div v-if="activeFileIsText">{{ activeFile.editorRow }}:{{ activeFile.editorColumn }}</div>
<div>{{ activeFile.fileLanguage }}</div>
</template>
<terminal-sync-status-safe />
diff --git a/app/assets/javascripts/ide/components/new_dropdown/upload.vue b/app/assets/javascripts/ide/components/new_dropdown/upload.vue
index b2141c13d9f..84ff05c9750 100644
--- a/app/assets/javascripts/ide/components/new_dropdown/upload.vue
+++ b/app/assets/javascripts/ide/components/new_dropdown/upload.vue
@@ -28,14 +28,13 @@ export default {
const { name } = file;
const encodedContent = target.result.split('base64,')[1];
const rawContent = encodedContent ? atob(encodedContent) : '';
- const isText = isTextFile(rawContent, file.type, name);
+ const isText = isTextFile({ content: rawContent, mimeType: file.type, name });
const emitCreateEvent = content =>
this.$emit('create', {
name: `${this.path ? `${this.path}/` : ''}${name}`,
type: 'blob',
content,
- binary: !isText,
rawPath: !isText ? target.result : '',
});
diff --git a/app/assets/javascripts/ide/components/repo_editor.vue b/app/assets/javascripts/ide/components/repo_editor.vue
index 52e4f6704c6..f342ce1739c 100644
--- a/app/assets/javascripts/ide/components/repo_editor.vue
+++ b/app/assets/javascripts/ide/components/repo_editor.vue
@@ -14,7 +14,7 @@ import Editor from '../lib/editor';
import FileTemplatesBar from './file_templates/bar.vue';
import { __ } from '~/locale';
import { extractMarkdownImagesFromEntries } from '../stores/utils';
-import { getPathParent, readFileAsDataURL, registerSchema } from '../utils';
+import { getPathParent, readFileAsDataURL, registerSchema, isTextFile } from '../utils';
import { getRulesWithTraversal } from '../lib/editorconfig/parser';
import mapRulesToMonaco from '../lib/editorconfig/rules_mapper';
@@ -60,7 +60,7 @@ export default {
]),
...mapGetters('fileTemplates', ['showFileTemplatesBar']),
shouldHideEditor() {
- return this.file && this.file.binary;
+ return this.file && !isTextFile(this.file);
},
showContentViewer() {
return (
diff --git a/app/assets/javascripts/ide/lib/files.js b/app/assets/javascripts/ide/lib/files.js
index b02f213d922..789e09fa8f2 100644
--- a/app/assets/javascripts/ide/lib/files.js
+++ b/app/assets/javascripts/ide/lib/files.js
@@ -1,4 +1,3 @@
-import { viewerInformationForPath } from '~/vue_shared/components/content_viewer/lib/viewer_utils';
import { decorateData, sortTree } from '../stores/utils';
export const splitParent = path => {
@@ -13,13 +12,7 @@ export const splitParent = path => {
/**
* Create file objects from a list of file paths.
*/
-export const decorateFiles = ({
- data,
- tempFile = false,
- content = '',
- binary = false,
- rawPath = '',
-}) => {
+export const decorateFiles = ({ data, tempFile = false, content = '', rawPath = '' }) => {
const treeList = [];
const entries = {};
@@ -68,7 +61,6 @@ export const decorateFiles = ({
const fileFolder = parent && insertParent(parent);
if (name) {
- const previewMode = viewerInformationForPath(name);
parentPath = fileFolder && fileFolder.path;
file = decorateData({
@@ -79,7 +71,6 @@ export const decorateFiles = ({
tempFile,
changed: tempFile,
content,
- binary: (previewMode && previewMode.binary) || binary,
rawPath,
parentPath,
});
diff --git a/app/assets/javascripts/ide/stores/actions.js b/app/assets/javascripts/ide/stores/actions.js
index a8fccdc7695..b8d59f8bd36 100644
--- a/app/assets/javascripts/ide/stores/actions.js
+++ b/app/assets/javascripts/ide/stores/actions.js
@@ -25,15 +25,7 @@ export const setResizingStatus = ({ commit }, resizing) => {
export const createTempEntry = (
{ state, commit, dispatch, getters },
- {
- name,
- type,
- content = '',
- binary = false,
- rawPath = '',
- openFile = true,
- makeFileActive = true,
- },
+ { name, type, content = '', rawPath = '', openFile = true, makeFileActive = true },
) => {
const fullName = name.slice(-1) !== '/' && type === 'tree' ? `${name}/` : name;
@@ -57,7 +49,6 @@ export const createTempEntry = (
type,
tempFile: true,
content,
- binary,
rawPath,
});
const { file, parentPath } = data;
@@ -84,7 +75,6 @@ export const addTempImage = ({ dispatch, getters }, { name, rawPath = '' }) =>
name: getters.getAvailableFileName(name),
type: 'blob',
content: rawPath.split('base64,')[1],
- binary: true,
rawPath,
openFile: false,
makeFileActive: false,
diff --git a/app/assets/javascripts/ide/stores/utils.js b/app/assets/javascripts/ide/stores/utils.js
index 56e2e2aa53f..d9cdc7727ad 100644
--- a/app/assets/javascripts/ide/stores/utils.js
+++ b/app/assets/javascripts/ide/stores/utils.js
@@ -23,7 +23,6 @@ export const dataStructure = () => ({
staged: false,
lastCommitSha: '',
rawPath: '',
- binary: false,
raw: '',
content: '',
editorRow: 1,
@@ -49,7 +48,6 @@ export const decorateData = entity => {
active = false,
opened = false,
changed = false,
- binary = false,
rawPath = '',
file_lock,
parentPath = '',
@@ -66,7 +64,6 @@ export const decorateData = entity => {
active,
changed,
content,
- binary,
rawPath,
file_lock,
parentPath,
diff --git a/app/assets/javascripts/ide/utils.js b/app/assets/javascripts/ide/utils.js
index ffc6e4c66f0..cde53e1ef00 100644
--- a/app/assets/javascripts/ide/utils.js
+++ b/app/assets/javascripts/ide/utils.js
@@ -1,5 +1,5 @@
import { languages } from 'monaco-editor';
-import { flatten } from 'lodash';
+import { flatten, isString } from 'lodash';
import { SIDE_LEFT, SIDE_RIGHT } from './constants';
const toLowerCase = x => x.toLowerCase();
@@ -42,15 +42,16 @@ const KNOWN_TYPES = [
},
];
-export function isTextFile(content, mimeType, fileName) {
- const knownType = KNOWN_TYPES.find(type => type.isMatch(mimeType, fileName));
+export function isTextFile({ name, content, mimeType = '' }) {
+ const knownType = KNOWN_TYPES.find(type => type.isMatch(mimeType, name));
if (knownType) return knownType.isText;
// does the string contain ascii characters only (ranges from space to tilde, tabs and new lines)
const asciiRegex = /^[ -~\t\n\r]+$/;
+
// for unknown types, determine the type by evaluating the file contents
- return asciiRegex.test(content);
+ return isString(content) && (content === '' || asciiRegex.test(content));
}
export const createPathWithExt = p => {
diff --git a/app/assets/javascripts/integrations/edit/components/integration_form.vue b/app/assets/javascripts/integrations/edit/components/integration_form.vue
index 17f910a9d75..0460ed6791e 100644
--- a/app/assets/javascripts/integrations/edit/components/integration_form.vue
+++ b/app/assets/javascripts/integrations/edit/components/integration_form.vue
@@ -25,7 +25,7 @@ export default {
mixins: [glFeatureFlagsMixin()],
computed: {
...mapGetters(['currentKey', 'propsSource', 'isSavingOrTesting']),
- ...mapState(['adminState', 'override', 'isSaving', 'isTesting']),
+ ...mapState(['defaultState', 'override', 'isSaving', 'isTesting']),
isEditable() {
return this.propsSource.editable;
},
@@ -53,8 +53,8 @@ export default {
<template>
<div>
<override-dropdown
- v-if="adminState !== null"
- :inherit-from-id="adminState.id"
+ v-if="defaultState !== null"
+ :inherit-from-id="defaultState.id"
:override="override"
:learn-more-path="propsSource.learnMorePath"
@change="setOverride"
diff --git a/app/assets/javascripts/integrations/edit/components/override_dropdown.vue b/app/assets/javascripts/integrations/edit/components/override_dropdown.vue
index 5e1ff96c5d7..c31dada8d2f 100644
--- a/app/assets/javascripts/integrations/edit/components/override_dropdown.vue
+++ b/app/assets/javascripts/integrations/edit/components/override_dropdown.vue
@@ -44,9 +44,9 @@ export default {
};
},
computed: {
- ...mapState(['adminState']),
+ ...mapState(['defaultState']),
description() {
- const level = this.adminState.integrationLevel;
+ const level = this.defaultState.integrationLevel;
return (
overrideDropdownDescriptions[level] || overrideDropdownDescriptions[defaultIntegrationLevel]
diff --git a/app/assets/javascripts/integrations/edit/index.js b/app/assets/javascripts/integrations/edit/index.js
index 5413226078d..248ee62d43a 100644
--- a/app/assets/javascripts/integrations/edit/index.js
+++ b/app/assets/javascripts/integrations/edit/index.js
@@ -72,7 +72,7 @@ function parseDatasetToProps(data) {
};
}
-export default (el, adminEl) => {
+export default (el, defaultEl) => {
if (!el) {
return null;
}
@@ -80,12 +80,12 @@ export default (el, adminEl) => {
const props = parseDatasetToProps(el.dataset);
const initialState = {
- adminState: null,
+ defaultState: null,
customState: props,
};
- if (adminEl) {
- initialState.adminState = Object.freeze(parseDatasetToProps(adminEl.dataset));
+ if (defaultEl) {
+ initialState.defaultState = Object.freeze(parseDatasetToProps(defaultEl.dataset));
}
return new Vue({
diff --git a/app/assets/javascripts/integrations/edit/store/getters.js b/app/assets/javascripts/integrations/edit/store/getters.js
index 4b494c30845..4ee5f11855c 100644
--- a/app/assets/javascripts/integrations/edit/store/getters.js
+++ b/app/assets/javascripts/integrations/edit/store/getters.js
@@ -1,8 +1,8 @@
-export const isInheriting = state => (state.adminState === null ? false : !state.override);
+export const isInheriting = state => (state.defaultState === null ? false : !state.override);
export const isSavingOrTesting = state => state.isSaving || state.isTesting;
export const propsSource = (state, getters) =>
- getters.isInheriting ? state.adminState : state.customState;
+ getters.isInheriting ? state.defaultState : state.customState;
export const currentKey = (state, getters) => (getters.isInheriting ? 'admin' : 'custom');
diff --git a/app/assets/javascripts/integrations/edit/store/state.js b/app/assets/javascripts/integrations/edit/store/state.js
index 7f1cd5c6e4d..a9ecee6c539 100644
--- a/app/assets/javascripts/integrations/edit/store/state.js
+++ b/app/assets/javascripts/integrations/edit/store/state.js
@@ -1,9 +1,9 @@
-export default ({ adminState = null, customState = {} } = {}) => {
- const override = adminState !== null ? adminState.id !== customState.inheritFromId : false;
+export default ({ defaultState = null, customState = {} } = {}) => {
+ const override = defaultState !== null ? defaultState.id !== customState.inheritFromId : false;
return {
override,
- adminState,
+ defaultState,
customState,
isSaving: false,
isTesting: false,
diff --git a/app/assets/javascripts/integrations/integration_settings_form.js b/app/assets/javascripts/integrations/integration_settings_form.js
index 529358a5f0b..1d0814125e6 100644
--- a/app/assets/javascripts/integrations/integration_settings_form.js
+++ b/app/assets/javascripts/integrations/integration_settings_form.js
@@ -20,7 +20,7 @@ export default class IntegrationSettingsForm {
// Init Vue component
this.vue = initForm(
document.querySelector('.js-vue-integration-settings'),
- document.querySelector('.js-vue-admin-integration-settings'),
+ document.querySelector('.js-vue-default-integration-settings'),
);
eventHub.$on('toggle', active => {
this.formActive = active;
diff --git a/app/assets/javascripts/issuable_create/components/issuable_form.vue b/app/assets/javascripts/issuable_create/components/issuable_form.vue
index eac4050b53d..17e51b3dbac 100644
--- a/app/assets/javascripts/issuable_create/components/issuable_form.vue
+++ b/app/assets/javascripts/issuable_create/components/issuable_form.vue
@@ -53,7 +53,12 @@ export default {
<div data-testid="issuable-title" class="form-group row">
<label for="issuable-title" class="col-form-label col-sm-2">{{ __('Title') }}</label>
<div class="col-sm-10">
- <gl-form-input id="issuable-title" v-model="issuableTitle" :placeholder="__('Title')" />
+ <gl-form-input
+ id="issuable-title"
+ v-model="issuableTitle"
+ :autofocus="true"
+ :placeholder="__('Title')"
+ />
</div>
</div>
<div data-testid="issuable-description" class="form-group row">
diff --git a/app/assets/javascripts/issuable_list/components/issuable_item.vue b/app/assets/javascripts/issuable_list/components/issuable_item.vue
new file mode 100644
index 00000000000..d8cb1ab07cd
--- /dev/null
+++ b/app/assets/javascripts/issuable_list/components/issuable_item.vue
@@ -0,0 +1,140 @@
+<script>
+import { GlLink, GlLabel, GlTooltipDirective } from '@gitlab/ui';
+
+import { __, sprintf } from '~/locale';
+import { getTimeago } from '~/lib/utils/datetime_utility';
+import { isScopedLabel } from '~/lib/utils/common_utils';
+import timeagoMixin from '~/vue_shared/mixins/timeago';
+
+export default {
+ components: {
+ GlLink,
+ GlLabel,
+ },
+ directives: {
+ GlTooltip: GlTooltipDirective,
+ },
+ mixins: [timeagoMixin],
+ props: {
+ issuableSymbol: {
+ type: String,
+ required: true,
+ },
+ issuable: {
+ type: Object,
+ required: true,
+ },
+ },
+ computed: {
+ author() {
+ return this.issuable.author;
+ },
+ authorId() {
+ const id = parseInt(this.author.id, 10);
+
+ if (Number.isNaN(id)) {
+ return this.author.id.includes('gid')
+ ? this.author.id.split('gid://gitlab/User/').pop()
+ : '';
+ }
+
+ return id;
+ },
+ labels() {
+ return this.issuable.labels?.nodes || this.issuable.labels || [];
+ },
+ createdAt() {
+ return sprintf(__('created %{timeAgo}'), {
+ timeAgo: getTimeago().format(this.issuable.createdAt),
+ });
+ },
+ updatedAt() {
+ return sprintf(__('updated %{timeAgo}'), {
+ timeAgo: getTimeago().format(this.issuable.updatedAt),
+ });
+ },
+ },
+ methods: {
+ scopedLabel(label) {
+ return isScopedLabel(label);
+ },
+ /**
+ * This is needed as an independent method since
+ * when user changes current page, `$refs.authorLink`
+ * will be null until next page results are loaded & rendered.
+ */
+ getAuthorPopoverTarget() {
+ if (this.$refs.authorLink) {
+ return this.$refs.authorLink.$el;
+ }
+ return '';
+ },
+ },
+};
+</script>
+
+<template>
+ <li class="issue">
+ <div class="issue-box">
+ <div class="issuable-info-container">
+ <div class="issuable-main-info">
+ <div data-testid="issuable-title" class="issue-title title">
+ <span class="issue-title-text" dir="auto">
+ <gl-link :href="issuable.webUrl">{{ issuable.title }}</gl-link>
+ </span>
+ </div>
+ <div class="issuable-info">
+ <span data-testid="issuable-reference" class="issuable-reference"
+ >{{ issuableSymbol }}{{ issuable.iid }}</span
+ >
+ <span class="issuable-authored d-none d-sm-inline-block">
+ &middot;
+ <span
+ v-gl-tooltip:tooltipcontainer.bottom
+ data-testid="issuable-created-at"
+ :title="tooltipTitle(issuable.createdAt)"
+ >{{ createdAt }}</span
+ >
+ {{ __('by') }}
+ <gl-link
+ :data-user-id="authorId"
+ :data-username="author.username"
+ :data-name="author.name"
+ :data-avatar-url="author.avatarUrl"
+ :href="author.webUrl"
+ data-testid="issuable-author"
+ class="author-link js-user-link"
+ >
+ <span class="author">{{ author.name }}</span>
+ </gl-link>
+ </span>
+ &nbsp;
+ <gl-label
+ v-for="(label, index) in labels"
+ :key="index"
+ :background-color="label.color"
+ :title="label.title"
+ :description="label.description"
+ :scoped="scopedLabel(label)"
+ :class="{ 'gl-ml-2': index }"
+ size="sm"
+ />
+ </div>
+ </div>
+ <div class="issuable-meta">
+ <div
+ data-testid="issuable-updated-at"
+ class="float-right issuable-updated-at d-none d-sm-inline-block"
+ >
+ <span
+ v-gl-tooltip:tooltipcontainer.bottom
+ :title="tooltipTitle(issuable.updatedAt)"
+ class="issuable-updated-at"
+ >{{ updatedAt }}</span
+ >
+ </div>
+ </div>
+ </div>
+ </div>
+ </li>
+</template>
diff --git a/app/assets/javascripts/issuable_list/components/issuable_list_root.vue b/app/assets/javascripts/issuable_list/components/issuable_list_root.vue
new file mode 100644
index 00000000000..7535203dea1
--- /dev/null
+++ b/app/assets/javascripts/issuable_list/components/issuable_list_root.vue
@@ -0,0 +1,153 @@
+<script>
+import { GlLoadingIcon, GlPagination } from '@gitlab/ui';
+
+import FilteredSearchBar from '~/vue_shared/components/filtered_search_bar/filtered_search_bar_root.vue';
+
+import IssuableTabs from './issuable_tabs.vue';
+import IssuableItem from './issuable_item.vue';
+
+export default {
+ components: {
+ GlLoadingIcon,
+ IssuableTabs,
+ FilteredSearchBar,
+ IssuableItem,
+ GlPagination,
+ },
+ props: {
+ namespace: {
+ type: String,
+ required: true,
+ },
+ recentSearchesStorageKey: {
+ type: String,
+ required: true,
+ },
+ searchInputPlaceholder: {
+ type: String,
+ required: true,
+ },
+ searchTokens: {
+ type: Array,
+ required: true,
+ },
+ sortOptions: {
+ type: Array,
+ required: true,
+ },
+ initialFilterValue: {
+ type: Array,
+ required: false,
+ default: () => [],
+ },
+ initialSortBy: {
+ type: String,
+ required: false,
+ default: 'created_desc',
+ },
+ issuables: {
+ type: Array,
+ required: true,
+ },
+ tabs: {
+ type: Array,
+ required: true,
+ },
+ tabCounts: {
+ type: Object,
+ required: true,
+ },
+ currentTab: {
+ type: String,
+ required: true,
+ },
+ issuableSymbol: {
+ type: String,
+ required: false,
+ default: '#',
+ },
+ issuablesLoading: {
+ type: Boolean,
+ required: false,
+ default: false,
+ },
+ showPaginationControls: {
+ type: Boolean,
+ required: false,
+ default: false,
+ },
+ defaultPageSize: {
+ type: Number,
+ required: false,
+ default: 20,
+ },
+ currentPage: {
+ type: Number,
+ required: false,
+ default: 1,
+ },
+ previousPage: {
+ type: Number,
+ required: false,
+ default: 0,
+ },
+ nextPage: {
+ type: Number,
+ required: false,
+ default: 2,
+ },
+ },
+};
+</script>
+
+<template>
+ <div class="issuable-list-container">
+ <issuable-tabs
+ :tabs="tabs"
+ :tab-counts="tabCounts"
+ :current-tab="currentTab"
+ @click="$emit('click-tab', $event)"
+ >
+ <template #nav-actions>
+ <slot name="nav-actions"></slot>
+ </template>
+ </issuable-tabs>
+ <filtered-search-bar
+ :namespace="namespace"
+ :recent-searches-storage-key="recentSearchesStorageKey"
+ :search-input-placeholder="searchInputPlaceholder"
+ :tokens="searchTokens"
+ :sort-options="sortOptions"
+ :initial-filter-value="initialFilterValue"
+ :initial-sort-by="initialSortBy"
+ class="gl-flex-grow-1 row-content-block"
+ @onFilter="$emit('filter', $event)"
+ @onSort="$emit('sort', $event)"
+ />
+ <div class="issuables-holder">
+ <gl-loading-icon v-if="issuablesLoading" size="md" class="gl-mt-5" />
+ <ul
+ v-if="!issuablesLoading && issuables.length"
+ class="content-list issuable-list issues-list"
+ >
+ <issuable-item
+ v-for="issuable in issuables"
+ :key="issuable.id"
+ :issuable-symbol="issuableSymbol"
+ :issuable="issuable"
+ />
+ </ul>
+ <slot v-if="!issuablesLoading && !issuables.length" name="empty-state"></slot>
+ <gl-pagination
+ v-if="showPaginationControls"
+ :per-page="defaultPageSize"
+ :value="currentPage"
+ :prev-page="previousPage"
+ :next-page="nextPage"
+ align="center"
+ class="gl-pagination gl-mt-3"
+ @input="$emit('page-change', $event)"
+ />
+ </div>
+ </div>
+</template>
diff --git a/app/assets/javascripts/issuable_list/components/issuable_tabs.vue b/app/assets/javascripts/issuable_list/components/issuable_tabs.vue
new file mode 100644
index 00000000000..df544ce69e7
--- /dev/null
+++ b/app/assets/javascripts/issuable_list/components/issuable_tabs.vue
@@ -0,0 +1,53 @@
+<script>
+import { GlTabs, GlTab, GlBadge } from '@gitlab/ui';
+
+export default {
+ components: {
+ GlTabs,
+ GlTab,
+ GlBadge,
+ },
+ props: {
+ tabs: {
+ type: Array,
+ required: true,
+ },
+ tabCounts: {
+ type: Object,
+ required: true,
+ },
+ currentTab: {
+ type: String,
+ required: true,
+ },
+ },
+ methods: {
+ isTabActive(tabName) {
+ return tabName === this.currentTab;
+ },
+ },
+};
+</script>
+
+<template>
+ <div class="top-area">
+ <gl-tabs class="nav-links mobile-separator issuable-state-filters">
+ <gl-tab
+ v-for="tab in tabs"
+ :key="tab.id"
+ :active="isTabActive(tab.name)"
+ @click="$emit('click', tab.name)"
+ >
+ <template #title>
+ <span :title="tab.titleTooltip">{{ tab.title }}</span>
+ <gl-badge variant="neutral" size="sm" class="gl-px-2 gl-py-1!">{{
+ tabCounts[tab.name]
+ }}</gl-badge>
+ </template>
+ </gl-tab>
+ </gl-tabs>
+ <div class="nav-controls">
+ <slot name="nav-actions"></slot>
+ </div>
+ </div>
+</template>
diff --git a/app/assets/javascripts/shared/milestones/form.js b/app/assets/javascripts/shared/milestones/form.js
index 0ff84dc4667..9ee02f923d5 100644
--- a/app/assets/javascripts/shared/milestones/form.js
+++ b/app/assets/javascripts/shared/milestones/form.js
@@ -16,5 +16,6 @@ export default (initGFM = true) => {
milestones: initGFM,
labels: initGFM,
snippets: initGFM,
+ vulnerabilities: initGFM,
});
};
diff --git a/app/assets/javascripts/snippet/snippet_edit.js b/app/assets/javascripts/snippet/snippet_edit.js
index b0d373b1a4b..3dc74922a77 100644
--- a/app/assets/javascripts/snippet/snippet_edit.js
+++ b/app/assets/javascripts/snippet/snippet_edit.js
@@ -14,6 +14,7 @@ document.addEventListener('DOMContentLoaded', () => {
milestones: false,
labels: false,
snippets: false,
+ vulnerabilities: false,
};
const projectSnippetOptions = {};
diff --git a/app/assets/javascripts/vue_shared/components/markdown/field.vue b/app/assets/javascripts/vue_shared/components/markdown/field.vue
index 9e1943f88b0..a48c279d0e3 100644
--- a/app/assets/javascripts/vue_shared/components/markdown/field.vue
+++ b/app/assets/javascripts/vue_shared/components/markdown/field.vue
@@ -174,6 +174,7 @@ export default {
milestones: this.enableAutocomplete,
labels: this.enableAutocomplete && !this.glFeatures.tributeAutocomplete,
snippets: this.enableAutocomplete,
+ vulnerabilities: this.enableAutocomplete,
});
},
beforeDestroy() {
diff --git a/app/controllers/groups/settings/integrations_controller.rb b/app/controllers/groups/settings/integrations_controller.rb
index 38f6b8dee65..e8551a7f270 100644
--- a/app/controllers/groups/settings/integrations_controller.rb
+++ b/app/controllers/groups/settings/integrations_controller.rb
@@ -12,7 +12,7 @@ module Groups
end
def edit
- @admin_integration = Service.default_integration(integration.type, group)
+ @default_integration = Service.default_integration(integration.type, group)
super
end
diff --git a/app/controllers/projects/issues_controller.rb b/app/controllers/projects/issues_controller.rb
index d353062b6dc..54d17701a6e 100644
--- a/app/controllers/projects/issues_controller.rb
+++ b/app/controllers/projects/issues_controller.rb
@@ -344,10 +344,12 @@ class Projects::IssuesController < Projects::ApplicationController
def finder_options
options = super
- return options unless service_desk?
+ options[:issue_types] = Issue::TYPES_FOR_LIST
- options.reject! { |key| key == 'author_username' || key == 'author_id' }
- options[:author_id] = User.support_bot
+ if service_desk?
+ options.reject! { |key| key == 'author_username' || key == 'author_id' }
+ options[:author_id] = User.support_bot
+ end
options
end
diff --git a/app/controllers/projects/services_controller.rb b/app/controllers/projects/services_controller.rb
index f2fdb9defac..9a69ef991dd 100644
--- a/app/controllers/projects/services_controller.rb
+++ b/app/controllers/projects/services_controller.rb
@@ -20,7 +20,7 @@ class Projects::ServicesController < Projects::ApplicationController
layout "project_settings"
def edit
- @admin_integration = Service.default_integration(service.type, project)
+ @default_integration = Service.default_integration(service.type, project)
end
def update
diff --git a/app/helpers/boards_helper.rb b/app/helpers/boards_helper.rb
index f8c00f3a4cd..6a4a7a8dfb2 100644
--- a/app/helpers/boards_helper.rb
+++ b/app/helpers/boards_helper.rb
@@ -11,13 +11,13 @@ module BoardsHelper
lists_endpoint: board_lists_path(board),
board_id: board.id,
disabled: (!can?(current_user, :create_non_backlog_issues, board)).to_s,
- issue_link_base: build_issue_link_base,
root_path: root_path,
full_path: full_path,
bulk_update_path: @bulk_issues_path,
time_tracking_limit_to_hours: Gitlab::CurrentSettings.time_tracking_limit_to_hours.to_s,
recent_boards_endpoint: recent_boards_path,
- parent: current_board_parent.model_name.param_key
+ parent: current_board_parent.model_name.param_key,
+ group_id: @group&.id
}
end
diff --git a/app/models/ci/daily_build_group_report_result.rb b/app/models/ci/daily_build_group_report_result.rb
index d6617b8c2eb..e6f02f2e4f3 100644
--- a/app/models/ci/daily_build_group_report_result.rb
+++ b/app/models/ci/daily_build_group_report_result.rb
@@ -11,6 +11,8 @@ module Ci
validates :data, json_schema: { filename: "daily_build_group_report_result_data" }
+ scope :with_included_projects, -> { includes(:project) }
+
def self.upsert_reports(data)
upsert_all(data, unique_by: :index_daily_build_group_report_results_unique_columns) if data.any?
end
diff --git a/app/models/issue.rb b/app/models/issue.rb
index 73e22a00db0..5a5de371301 100644
--- a/app/models/issue.rb
+++ b/app/models/issue.rb
@@ -30,6 +30,11 @@ class Issue < ApplicationRecord
SORTING_PREFERENCE_FIELD = :issues_sort
+ # Types of issues that should be displayed on lists across the app
+ # for example, project issues list, group issues list and issue boards.
+ # Some issue types, like test cases, should be hidden by default.
+ TYPES_FOR_LIST = %w(issue incident).freeze
+
belongs_to :project
has_one :namespace, through: :project
diff --git a/app/models/protected_branch.rb b/app/models/protected_branch.rb
index 594c822c18f..599c174ddd7 100644
--- a/app/models/protected_branch.rb
+++ b/app/models/protected_branch.rb
@@ -38,9 +38,9 @@ class ProtectedBranch < ApplicationRecord
project.protected_branches
end
+ # overridden in EE
def self.branch_requires_code_owner_approval?(project, branch_name)
- # NOOP
- #
+ false
end
def self.by_name(query)
diff --git a/app/services/projects/open_issues_count_service.rb b/app/services/projects/open_issues_count_service.rb
index 82632d63e5b..dc450311db2 100644
--- a/app/services/projects/open_issues_count_service.rb
+++ b/app/services/projects/open_issues_count_service.rb
@@ -68,10 +68,12 @@ module Projects
# Check https://gitlab.com/gitlab-org/gitlab-foss/issues/38418 description.
# rubocop: disable CodeReuse/ActiveRecord
def self.query(projects, public_only: true)
+ issues_filtered_by_type = Issue.opened.with_issue_type(Issue::TYPES_FOR_LIST)
+
if public_only
- Issue.opened.public_only.where(project: projects)
+ issues_filtered_by_type.public_only.where(project: projects)
else
- Issue.opened.where(project: projects)
+ issues_filtered_by_type.where(project: projects)
end
end
# rubocop: enable CodeReuse/ActiveRecord
diff --git a/app/views/projects/issues/_design_management.html.haml b/app/views/projects/issues/_design_management.html.haml
index 4697e030b5c..6fc2f41b122 100644
--- a/app/views/projects/issues/_design_management.html.haml
+++ b/app/views/projects/issues/_design_management.html.haml
@@ -3,7 +3,7 @@
- requirements_link_url = help_page_path('user/project/issues/design_management', anchor: 'requirements')
- requirements_link_start = '<a href="%{url}" target="_blank" rel="noopener noreferrer">'.html_safe % { url: requirements_link_url }
- link_end = '</a>'.html_safe
-- enable_lfs_message = s_("DesignManagement|To upload designs, you'll need to enable LFS. %{requirements_link_start}More information%{requirements_link_end}").html_safe % { requirements_link_start: requirements_link_start, requirements_link_end: link_end }
+- enable_lfs_message = s_("DesignManagement|To upload designs, you'll need to enable LFS and have admin enable hashed storage. %{requirements_link_start}More information%{requirements_link_end}").html_safe % { requirements_link_start: requirements_link_start, requirements_link_end: link_end }
- if @project.design_management_enabled?
.js-design-management{ data: { project_path: @project.full_path, issue_iid: @issue.iid, issue_path: project_issue_path(@project, @issue) } }
diff --git a/app/views/projects/packages/packages/show.html.haml b/app/views/projects/packages/packages/show.html.haml
index 9e68dbc4de1..97a3c6e7092 100644
--- a/app/views/projects/packages/packages/show.html.haml
+++ b/app/views/projects/packages/packages/show.html.haml
@@ -13,7 +13,7 @@
npm_help_path: help_page_path('user/packages/npm_registry/index'),
maven_path: package_registry_project_url(@project.id, :maven),
maven_help_path: help_page_path('user/packages/maven_repository/index'),
- conan_path: package_registry_instance_url(:conan),
+ conan_path: package_registry_project_url(@project.id, :conan),
conan_help_path: help_page_path('user/packages/conan_repository/index'),
nuget_path: nuget_package_registry_url(@project.id),
nuget_help_path: help_page_path('user/packages/nuget_repository/index'),
diff --git a/app/views/shared/_service_settings.html.haml b/app/views/shared/_service_settings.html.haml
index 2425bcf61d9..647421a8fbe 100644
--- a/app/views/shared/_service_settings.html.haml
+++ b/app/views/shared/_service_settings.html.haml
@@ -8,6 +8,6 @@
= markdown integration.help
.service-settings
- - if @admin_integration
- .js-vue-admin-integration-settings{ data: integration_form_data(@admin_integration) }
+ - if @default_integration
+ .js-vue-default-integration-settings{ data: integration_form_data(@default_integration) }
.js-vue-integration-settings{ data: integration_form_data(integration) }
diff --git a/app/views/shared/boards/_show.html.haml b/app/views/shared/boards/_show.html.haml
index 86c73e36317..d5b1245abb3 100644
--- a/app/views/shared/boards/_show.html.haml
+++ b/app/views/shared/boards/_show.html.haml
@@ -2,7 +2,6 @@
- group = local_assigns.fetch(:group, false)
-# TODO: Move group_id and can_admin_list to the board store
See: https://gitlab.com/gitlab-org/gitlab/-/issues/213082
-- group_id = @group&.id || "null"
- can_admin_list = can?(current_user, :admin_list, current_board_parent) == true
- @no_breadcrumb_container = true
- @no_container = true
@@ -24,11 +23,7 @@
"ref" => "board_content",
":lists" => "state.lists",
":can-admin-list" => can_admin_list,
- ":group-id" => group_id,
- ":disabled" => "disabled",
- ":issue-link-base" => "issueLinkBase",
- ":root-path" => "rootPath",
- ":board-id" => "boardId" }
+ ":disabled" => "disabled" }
- else
.boards-list.w-100.py-3.px-2.text-nowrap{ data: { qa_selector: "boards_list" } }
.boards-app-loading.w-100.text-center{ "v-if" => "loading" }
@@ -37,12 +32,8 @@
"v-for" => "list in state.lists",
"ref" => "board",
":can-admin-list" => can_admin_list,
- ":group-id" => group_id,
":list" => "list",
":disabled" => "disabled",
- ":issue-link-base" => "issueLinkBase",
- ":root-path" => "rootPath",
- ":board-id" => "boardId",
":key" => "list.id" }
= render "shared/boards/components/sidebar", group: group
= render_if_exists 'shared/boards/components/board_settings_sidebar'
@@ -51,6 +42,4 @@
"milestone-path" => milestones_filter_dropdown_path,
"label-path" => labels_filter_path_with_defaults,
"empty-state-svg" => image_path('illustrations/issues.svg'),
- ":issue-link-base" => "issueLinkBase",
- ":root-path" => "rootPath",
":project-id" => @project.id }
diff --git a/app/workers/git_garbage_collect_worker.rb b/app/workers/git_garbage_collect_worker.rb
index 6e4feea1b26..b0307571448 100644
--- a/app/workers/git_garbage_collect_worker.rb
+++ b/app/workers/git_garbage_collect_worker.rb
@@ -37,10 +37,7 @@ class GitGarbageCollectWorker # rubocop:disable Scalability/IdempotentWorker
# Refresh the branch cache in case garbage collection caused a ref lookup to fail
flush_ref_caches(project) if task == :gc
- if task != :pack_refs
- project.repository.expire_statistics_caches
- Projects::UpdateStatisticsService.new(project, nil, statistics: [:repository_size, :lfs_objects_size]).execute
- end
+ update_repository_statistics(project) if task != :pack_refs
# In case pack files are deleted, release libgit2 cache and open file
# descriptors ASAP instead of waiting for Ruby garbage collection
@@ -106,6 +103,13 @@ class GitGarbageCollectWorker # rubocop:disable Scalability/IdempotentWorker
project.repository.has_visible_content?
end
+ def update_repository_statistics(project)
+ project.repository.expire_statistics_caches
+ return if Gitlab::Database.read_only? # GitGarbageCollectWorker may be run on a Geo secondary
+
+ Projects::UpdateStatisticsService.new(project, nil, statistics: [:repository_size, :lfs_objects_size]).execute
+ end
+
def bitmaps_enabled?
Gitlab::CurrentSettings.housekeeping_bitmaps_enabled
end
diff --git a/changelogs/unreleased/246655-hashed-storage-trigger-migration-to-hashed-storage-via-post-deploy.yml b/changelogs/unreleased/246655-hashed-storage-trigger-migration-to-hashed-storage-via-post-deploy.yml
new file mode 100644
index 00000000000..472dde6b9f0
--- /dev/null
+++ b/changelogs/unreleased/246655-hashed-storage-trigger-migration-to-hashed-storage-via-post-deploy.yml
@@ -0,0 +1,5 @@
+---
+title: 'Hashed Storage: forced automatic migration of legacy projects via background jobs'
+merge_request: 42313
+author:
+type: changed
diff --git a/changelogs/unreleased/249910-does-not-update-repository-statistics-when-running-on-a-read-only-.yml b/changelogs/unreleased/249910-does-not-update-repository-statistics-when-running-on-a-read-only-.yml
new file mode 100644
index 00000000000..187b6dddef0
--- /dev/null
+++ b/changelogs/unreleased/249910-does-not-update-repository-statistics-when-running-on-a-read-only-.yml
@@ -0,0 +1,6 @@
+---
+title: Does not update repository statistics when running housekeeping and repository
+ cleanup on a read-only instance
+merge_request: 42409
+author:
+type: fixed
diff --git a/changelogs/unreleased/41818.yml b/changelogs/unreleased/41818.yml
new file mode 100644
index 00000000000..cecfb223f8d
--- /dev/null
+++ b/changelogs/unreleased/41818.yml
@@ -0,0 +1,5 @@
+---
+title: Improve design management not available message
+merge_request: 41818
+author: Ben Bodenmiller @bbodenmiller
+type: fixed
diff --git a/changelogs/unreleased/add_deduplicated_column_to_security_findings.yml b/changelogs/unreleased/add_deduplicated_column_to_security_findings.yml
new file mode 100644
index 00000000000..4740f9b6dbf
--- /dev/null
+++ b/changelogs/unreleased/add_deduplicated_column_to_security_findings.yml
@@ -0,0 +1,6 @@
+---
+title: Add `deduplicated` column to `security_findings` table along with the compound
+ index on `scan_id` and `deduplicated` and remove the index on `scan_id`
+merge_request: 42270
+author:
+type: added
diff --git a/changelogs/unreleased/improve-group-coverage-query-performance.yml b/changelogs/unreleased/improve-group-coverage-query-performance.yml
new file mode 100644
index 00000000000..4fc099d9047
--- /dev/null
+++ b/changelogs/unreleased/improve-group-coverage-query-performance.yml
@@ -0,0 +1,5 @@
+---
+title: Preload projects to prevent N+1 when populating project name
+merge_request: 40769
+author:
+type: performance
diff --git a/changelogs/unreleased/update-conan-installation-snippet.yml b/changelogs/unreleased/update-conan-installation-snippet.yml
new file mode 100644
index 00000000000..09c92dfa44e
--- /dev/null
+++ b/changelogs/unreleased/update-conan-installation-snippet.yml
@@ -0,0 +1,5 @@
+---
+title: Update conan remote instructions snippet to show project-level remote
+merge_request: 42526
+author:
+type: changed
diff --git a/db/migrate/20200914155135_add_deduplicated_flag_into_security_findings_table.rb b/db/migrate/20200914155135_add_deduplicated_flag_into_security_findings_table.rb
new file mode 100644
index 00000000000..f403c2d9a07
--- /dev/null
+++ b/db/migrate/20200914155135_add_deduplicated_flag_into_security_findings_table.rb
@@ -0,0 +1,9 @@
+# frozen_string_literal: true
+
+class AddDeduplicatedFlagIntoSecurityFindingsTable < ActiveRecord::Migration[6.0]
+ DOWNTIME = false
+
+ def change
+ add_column :security_findings, :deduplicated, :boolean, default: false, null: false
+ end
+end
diff --git a/db/migrate/20200914183227_add_index_on_deduplicated_column_of_security_findings.rb b/db/migrate/20200914183227_add_index_on_deduplicated_column_of_security_findings.rb
new file mode 100644
index 00000000000..1d0f656df4d
--- /dev/null
+++ b/db/migrate/20200914183227_add_index_on_deduplicated_column_of_security_findings.rb
@@ -0,0 +1,18 @@
+# frozen_string_literal: true
+
+class AddIndexOnDeduplicatedColumnOfSecurityFindings < ActiveRecord::Migration[6.0]
+ include Gitlab::Database::MigrationHelpers
+
+ DOWNTIME = false
+ INDEX_NAME = 'index_security_findings_on_scan_id_and_deduplicated'
+
+ disable_ddl_transaction!
+
+ def up
+ add_concurrent_index :security_findings, [:scan_id, :deduplicated], name: INDEX_NAME
+ end
+
+ def down
+ remove_concurrent_index_by_name :security_findings, INDEX_NAME
+ end
+end
diff --git a/db/migrate/20200914184212_remove_index_on_security_findings_scan_id.rb b/db/migrate/20200914184212_remove_index_on_security_findings_scan_id.rb
new file mode 100644
index 00000000000..342dba537d5
--- /dev/null
+++ b/db/migrate/20200914184212_remove_index_on_security_findings_scan_id.rb
@@ -0,0 +1,18 @@
+# frozen_string_literal: true
+
+class RemoveIndexOnSecurityFindingsScanId < ActiveRecord::Migration[6.0]
+ include Gitlab::Database::MigrationHelpers
+
+ DOWNTIME = false
+ INDEX_NAME = 'index_security_findings_on_scan_id'
+
+ disable_ddl_transaction!
+
+ def up
+ remove_concurrent_index_by_name :security_findings, INDEX_NAME
+ end
+
+ def down
+ add_concurrent_index :security_findings, :scan_id, name: INDEX_NAME
+ end
+end
diff --git a/db/post_migrate/20200915044225_schedule_migration_to_hashed_storage.rb b/db/post_migrate/20200915044225_schedule_migration_to_hashed_storage.rb
new file mode 100644
index 00000000000..553d060dc58
--- /dev/null
+++ b/db/post_migrate/20200915044225_schedule_migration_to_hashed_storage.rb
@@ -0,0 +1,17 @@
+# frozen_string_literal: true
+
+class ScheduleMigrationToHashedStorage < ActiveRecord::Migration[6.0]
+ include Gitlab::Database::MigrationHelpers
+ DOWNTIME = false
+ MIGRATION = 'MigrateToHashedStorage'
+
+ disable_ddl_transaction!
+
+ def up
+ migrate_async(MIGRATION)
+ end
+
+ def down
+ # NO-OP
+ end
+end
diff --git a/db/schema_migrations/20200914155135 b/db/schema_migrations/20200914155135
new file mode 100644
index 00000000000..17cd6c67033
--- /dev/null
+++ b/db/schema_migrations/20200914155135
@@ -0,0 +1 @@
+0b01a251eb736eb9e9986214c69ea5f4a11d8293bc4083af1585ce265c8f69de \ No newline at end of file
diff --git a/db/schema_migrations/20200914183227 b/db/schema_migrations/20200914183227
new file mode 100644
index 00000000000..3fb2d461452
--- /dev/null
+++ b/db/schema_migrations/20200914183227
@@ -0,0 +1 @@
+570a7de82da3dabfd12f3099cdc028128d4b61a4ecf6edd4b3102031d914316c \ No newline at end of file
diff --git a/db/schema_migrations/20200914184212 b/db/schema_migrations/20200914184212
new file mode 100644
index 00000000000..1053454b960
--- /dev/null
+++ b/db/schema_migrations/20200914184212
@@ -0,0 +1 @@
+dcfd1f006aa6695e0fd8f392c7288f2d80bddfe53827d3a2f079bd039f4fe0de \ No newline at end of file
diff --git a/db/schema_migrations/20200915044225 b/db/schema_migrations/20200915044225
new file mode 100644
index 00000000000..440fddf4080
--- /dev/null
+++ b/db/schema_migrations/20200915044225
@@ -0,0 +1 @@
+0c659c7cbbda4d2fbbbd344f0fca50860ccf54ec8b666772d53a4c6fa602d097 \ No newline at end of file
diff --git a/db/structure.sql b/db/structure.sql
index 1bf6b05da5e..899c5de8ae6 100644
--- a/db/structure.sql
+++ b/db/structure.sql
@@ -15495,6 +15495,7 @@ CREATE TABLE public.security_findings (
severity smallint NOT NULL,
confidence smallint NOT NULL,
project_fingerprint text NOT NULL,
+ deduplicated boolean DEFAULT false NOT NULL,
CONSTRAINT check_b9508c6df8 CHECK ((char_length(project_fingerprint) <= 40))
);
@@ -21016,7 +21017,7 @@ CREATE INDEX index_security_findings_on_confidence ON public.security_findings U
CREATE INDEX index_security_findings_on_project_fingerprint ON public.security_findings USING btree (project_fingerprint);
-CREATE INDEX index_security_findings_on_scan_id ON public.security_findings USING btree (scan_id);
+CREATE INDEX index_security_findings_on_scan_id_and_deduplicated ON public.security_findings USING btree (scan_id, deduplicated);
CREATE INDEX index_security_findings_on_scanner_id ON public.security_findings USING btree (scanner_id);
diff --git a/doc/ci/parent_child_pipelines.md b/doc/ci/parent_child_pipelines.md
index 3e3af9427c3..45b472bfb91 100644
--- a/doc/ci/parent_child_pipelines.md
+++ b/doc/ci/parent_child_pipelines.md
@@ -150,12 +150,36 @@ We also have an [example project using Dynamic Child Pipelines with Jsonnet](htt
In GitLab 12.9, the child pipeline could fail to be created in certain cases, causing the parent pipeline to fail.
This is [resolved in GitLab 12.10](https://gitlab.com/gitlab-org/gitlab/-/issues/209070).
-## Limitations
+## Nested child pipelines
-In GitLab 13.3 and older, a parent pipeline can trigger many child pipelines, but
-those child pipeline cannot trigger further child pipelines.
+> - [Introduced](https://gitlab.com/gitlab-org/gitlab/-/issues/29651) in GitLab 13.4.
+> - It's [deployed behind a feature flag](../user/feature_flags.md), disabled by default.
+> - It's disabled on GitLab.com.
+> - It's not recommended for production use.
+> - To use it in GitLab self-managed instances, ask a GitLab administrator to [enable it](#enable-or-disable-nested-child-pipelines). **(CORE ONLY)**
-In GitLab 13.4 and newer, the [maximum depth of child pipelines was increased to 2](https://gitlab.com/gitlab-org/gitlab/-/issues/29651).
-A parent pipeline can trigger many child pipelines. These child pipelines can trigger
-their own child pipelines. This second layer of child pipelines cannot trigger further
-child pipelines.
+Parent and child pipelines were introduced with a maximum depth of one level of child
+pipelines, which was later increased to two. A parent pipeline can trigger many child
+pipelines, and these child pipelines can trigger their own child pipelines. It's not
+possible to trigger another level of child pipelines.
+
+### Enable or disable nested child pipelines **(CORE ONLY)**
+
+Nested child pipelines with a depth of two are under development and not ready for
+production use. This feature is deployed behind a feature flag that is **disabled by default**.
+Enabling this feature allows child pipelines to trigger one more level of child pipelines.
+The second level of child pipelines cannot trigger any further child pipelines.
+[GitLab administrators with access to the GitLab Rails console](../administration/feature_flags.md)
+can enable it.
+
+To enable it:
+
+```ruby
+Feature.enable(:ci_child_of_child_pipeline)
+```
+
+To disable it:
+
+```ruby
+Feature.disable(:ci_child_of_child_pipeline)
+```
diff --git a/doc/install/azure/index.md b/doc/install/azure/index.md
index 00c868c6c41..b6e3025a0e0 100644
--- a/doc/install/azure/index.md
+++ b/doc/install/azure/index.md
@@ -1,15 +1,23 @@
---
+stage: Enablement
+group: Distribution
+info: To determine the technical writer assigned to the Stage/Group associated with this page, see https://about.gitlab.com/handbook/engineering/ux/technical-writing/#designated-technical-writers
description: 'Learn how to spin up a pre-configured GitLab VM on Microsoft Azure.'
type: howto
---
# Install GitLab on Microsoft Azure
-Azure is Microsoft's business cloud and GitLab is a pre-configured offering on the Azure Marketplace.
-Hopefully, you aren't surprised to hear that Microsoft and Azure have embraced open source software
-like Ubuntu, Red Hat Enterprise Linux, and of course - GitLab! This means that you can spin up a
-pre-configured GitLab VM and have your very own private GitLab up and running in around 30 minutes.
-Let's get started.
+CAUTION: **Deprecated:**
+The GitLab image in the Azure Marketplace is deprecated. You can track GitLab's
+efforts to [post a new image](https://gitlab.com/gitlab-com/alliances/microsoft/gitlab-tracker/-/issues/2).
+
+Azure is Microsoft's business cloud and GitLab is a pre-configured offering on
+the Azure Marketplace. Hopefully, you aren't surprised to hear that Microsoft
+and Azure have embraced open source software like Ubuntu, Red Hat Enterprise Linux,
+and of course - GitLab! This means that you can spin up a pre-configured
+GitLab VM and have your very own private GitLab up and running in around 30
+minutes. Let's get started.
## Getting started
diff --git a/lib/gitlab/background_migration/migrate_to_hashed_storage.rb b/lib/gitlab/background_migration/migrate_to_hashed_storage.rb
new file mode 100644
index 00000000000..4054db4fb87
--- /dev/null
+++ b/lib/gitlab/background_migration/migrate_to_hashed_storage.rb
@@ -0,0 +1,61 @@
+# frozen_string_literal: true
+
+module Gitlab
+ module BackgroundMigration
+ # Background migration to move any legacy project to Hashed Storage
+ class MigrateToHashedStorage
+ def perform
+ batch_size = helper.batch_size
+ legacy_projects_count = Project.with_unmigrated_storage.count
+
+ if storage_migrator.rollback_pending?
+ logger.warn(
+ migrator: 'MigrateToHashedStorage',
+ message: 'Aborting an storage rollback operation currently in progress'
+ )
+
+ storage_migrator.abort_rollback!
+ end
+
+ if legacy_projects_count == 0
+ logger.info(
+ migrator: 'MigrateToHashedStorage',
+ message: 'There are no projects requiring migration to Hashed Storage'
+ )
+
+ return
+ end
+
+ logger.info(
+ migrator: 'MigrateToHashedStorage',
+ message: "Enqueuing migration of #{legacy_projects_count} projects in batches of #{batch_size}"
+ )
+
+ helper.project_id_batches_migration do |start, finish|
+ storage_migrator.bulk_schedule_migration(start: start, finish: finish)
+
+ logger.info(
+ migrator: 'MigrateToHashedStorage',
+ message: "Enqueuing migration of projects in batches of #{batch_size} from ID=#{start} to ID=#{finish}",
+ batch_from: start,
+ batch_to: finish
+ )
+ end
+ end
+
+ private
+
+ def helper
+ Gitlab::HashedStorage::RakeHelper
+ end
+
+ def storage_migrator
+ @storage_migrator ||= Gitlab::HashedStorage::Migrator.new
+ end
+
+ def logger
+ @logger ||= ::Gitlab::BackgroundMigration::Logger.build
+ end
+ end
+ end
+end
diff --git a/lib/gitlab/hashed_storage/migrator.rb b/lib/gitlab/hashed_storage/migrator.rb
index d30e5587a64..b57560544c8 100644
--- a/lib/gitlab/hashed_storage/migrator.rb
+++ b/lib/gitlab/hashed_storage/migrator.rb
@@ -93,6 +93,14 @@ module Gitlab
any_non_empty_queue?(::HashedStorage::RollbackerWorker, ::HashedStorage::ProjectRollbackWorker)
end
+ # Remove all remaining scheduled rollback operations
+ #
+ def abort_rollback!
+ [::HashedStorage::RollbackerWorker, ::HashedStorage::ProjectRollbackWorker].each do |worker|
+ Sidekiq::Queue.new(worker.queue).clear
+ end
+ end
+
private
def any_non_empty_queue?(*workers)
diff --git a/locale/gitlab.pot b/locale/gitlab.pot
index a59786d97ca..0cbdd2ebf67 100644
--- a/locale/gitlab.pot
+++ b/locale/gitlab.pot
@@ -8800,7 +8800,7 @@ msgstr ""
msgid "DesignManagement|The maximum number of designs allowed to be uploaded is %{upload_limit}. Please try again."
msgstr ""
-msgid "DesignManagement|To upload designs, you'll need to enable LFS. %{requirements_link_start}More information%{requirements_link_end}"
+msgid "DesignManagement|To upload designs, you'll need to enable LFS and have admin enable hashed storage. %{requirements_link_start}More information%{requirements_link_end}"
msgstr ""
msgid "DesignManagement|Unresolve thread"
@@ -16843,6 +16843,9 @@ msgstr ""
msgid "New Snippet"
msgstr ""
+msgid "New Test Case"
+msgstr ""
+
msgid "New User"
msgstr ""
@@ -23622,6 +23625,9 @@ msgstr ""
msgid "Something went wrong while creating a requirement."
msgstr ""
+msgid "Something went wrong while creating a test case."
+msgstr ""
+
msgid "Something went wrong while deleting description changes. Please try again."
msgstr ""
@@ -24360,6 +24366,9 @@ msgstr ""
msgid "Submit search"
msgstr ""
+msgid "Submit test case"
+msgstr ""
+
msgid "Submit the current review."
msgstr ""
@@ -26696,6 +26705,9 @@ msgstr ""
msgid "Too many projects enabled. You will need to manage them via the console or the API."
msgstr ""
+msgid "Too much data"
+msgstr ""
+
msgid "Topics (optional)"
msgstr ""
@@ -27872,6 +27884,9 @@ msgstr ""
msgid "Value Stream Name"
msgstr ""
+msgid "ValueStreamAnalyticsStage|We don't have enough data to show this stage."
+msgstr ""
+
msgid "ValueStreamAnalytics|%{days}d"
msgstr ""
diff --git a/qa/qa/page/project/issue/show.rb b/qa/qa/page/project/issue/show.rb
index 90b3dc54064..826acaa2e0a 100644
--- a/qa/qa/page/project/issue/show.rb
+++ b/qa/qa/page/project/issue/show.rb
@@ -70,7 +70,10 @@ module QA
end
def click_remove_related_issue_button
- click_element(:remove_related_issue_button)
+ retry_until(sleep_interval: 5) do
+ click_element(:remove_related_issue_button)
+ has_no_element?(:remove_related_issue_button, wait: QA::Support::Repeater::DEFAULT_MAX_WAIT_TIME)
+ end
end
def click_close_issue_button
diff --git a/qa/qa/specs/features/browser_ui/2_plan/related_issues/related_issues_spec.rb b/qa/qa/specs/features/browser_ui/2_plan/related_issues/related_issues_spec.rb
index c9cb8d4aba6..a2190a8cf41 100644
--- a/qa/qa/specs/features/browser_ui/2_plan/related_issues/related_issues_spec.rb
+++ b/qa/qa/specs/features/browser_ui/2_plan/related_issues/related_issues_spec.rb
@@ -1,7 +1,7 @@
# frozen_string_literal: true
module QA
- RSpec.describe 'Plan', :reliable do
+ RSpec.describe 'Plan' do
describe 'Related issues' do
let(:project) do
Resource::Project.fabricate_via_api! do |project|
@@ -30,19 +30,14 @@ module QA
Page::Project::Issue::Show.perform do |show|
max_wait = 60
- wait_interval = 1
show.relate_issue(issue_2)
- show.wait_until(reload: false, max_duration: max_wait, sleep_interval: wait_interval) do
- expect(show.related_issuable_item).to have_content(issue_2.title)
- end
+ expect(show.related_issuable_item).to have_text(issue_2.title, wait: max_wait)
show.click_remove_related_issue_button
- show.wait_until(reload: false, max_duration: max_wait, sleep_interval: wait_interval) do
- expect(show).not_to have_content(issue_2.title)
- end
+ expect(show).to have_no_text(issue_2.title, wait: max_wait)
end
end
end
diff --git a/spec/controllers/projects/issues_controller_spec.rb b/spec/controllers/projects/issues_controller_spec.rb
index 0be90a09565..ef72416fc71 100644
--- a/spec/controllers/projects/issues_controller_spec.rb
+++ b/spec/controllers/projects/issues_controller_spec.rb
@@ -82,6 +82,16 @@ RSpec.describe Projects::IssuesController do
expect(response).to have_gitlab_http_status(:ok)
end
+ it 'returns only list type issues' do
+ issue = create(:issue, project: project)
+ incident = create(:issue, project: project, issue_type: 'incident')
+ create(:issue, project: project, issue_type: 'test_case')
+
+ get :index, params: { namespace_id: project.namespace, project_id: project }
+
+ expect(assigns(:issues)).to contain_exactly(issue, incident)
+ end
+
it "returns 301 if request path doesn't match project path" do
get :index, params: { namespace_id: project.namespace, project_id: project.path.upcase }
diff --git a/spec/features/projects/issues/design_management/user_uploads_designs_spec.rb b/spec/features/projects/issues/design_management/user_uploads_designs_spec.rb
index 8998cae621d..de1fcc9d787 100644
--- a/spec/features/projects/issues/design_management/user_uploads_designs_spec.rb
+++ b/spec/features/projects/issues/design_management/user_uploads_designs_spec.rb
@@ -38,7 +38,7 @@ RSpec.describe 'User uploads new design', :js do
let(:feature_enabled) { false }
it 'shows the message about requirements' do
- expect(page).to have_content("To upload designs, you'll need to enable LFS.")
+ expect(page).to have_content("To upload designs, you'll need to enable LFS and have admin enable hashed storage.")
end
end
diff --git a/spec/frontend/boards/board_list_helper.js b/spec/frontend/boards/board_list_helper.js
index b51a82f2a35..80d7a72151d 100644
--- a/spec/frontend/boards/board_list_helper.js
+++ b/spec/frontend/boards/board_list_helper.js
@@ -52,10 +52,12 @@ export default function createComponent({
list,
issues: list.issues,
loading: false,
- issueLinkBase: '/issues',
- rootPath: '/',
...componentProps,
},
+ provide: {
+ groupId: null,
+ rootPath: '/',
+ },
}).$mount();
Vue.nextTick(() => {
diff --git a/spec/frontend/boards/board_list_spec.js b/spec/frontend/boards/board_list_spec.js
index 3a64b004847..88883ae61d4 100644
--- a/spec/frontend/boards/board_list_spec.js
+++ b/spec/frontend/boards/board_list_spec.js
@@ -45,10 +45,12 @@ const createComponent = ({ done, listIssueProps = {}, componentProps = {}, listP
list,
issues: list.issues,
loading: false,
- issueLinkBase: '/issues',
- rootPath: '/',
...componentProps,
},
+ provide: {
+ groupId: null,
+ rootPath: '/',
+ },
}).$mount();
Vue.nextTick(() => {
diff --git a/spec/frontend/boards/board_new_issue_spec.js b/spec/frontend/boards/board_new_issue_spec.js
index 94afc8a2b45..3eebfeca965 100644
--- a/spec/frontend/boards/board_new_issue_spec.js
+++ b/spec/frontend/boards/board_new_issue_spec.js
@@ -1,6 +1,7 @@
/* global List */
import Vue from 'vue';
+import { mount } from '@vue/test-utils';
import MockAdapter from 'axios-mock-adapter';
import axios from '~/lib/utils/axios_utils';
import boardNewIssue from '~/boards/components/board_new_issue.vue';
@@ -10,6 +11,7 @@ import '~/boards/models/list';
import { listObj, boardsMockInterceptor } from './mock_data';
describe('Issue boards new issue form', () => {
+ let wrapper;
let vm;
let list;
let mock;
@@ -24,13 +26,11 @@ describe('Issue boards new issue form', () => {
const dummySubmitEvent = {
preventDefault() {},
};
- vm.$refs.submitButton = vm.$el.querySelector('.btn-success');
- return vm.submit(dummySubmitEvent);
+ wrapper.vm.$refs.submitButton = wrapper.find({ ref: 'submitButton' });
+ return wrapper.vm.submit(dummySubmitEvent);
};
beforeEach(() => {
- setFixtures('<div class="test-container"></div>');
-
const BoardNewIssueComp = Vue.extend(boardNewIssue);
mock = new MockAdapter(axios);
@@ -43,46 +43,52 @@ describe('Issue boards new issue form', () => {
newIssueMock = Promise.resolve(promiseReturn);
jest.spyOn(list, 'newIssue').mockImplementation(() => newIssueMock);
- vm = new BoardNewIssueComp({
+ wrapper = mount(BoardNewIssueComp, {
propsData: {
+ disabled: false,
list,
},
- }).$mount(document.querySelector('.test-container'));
+ provide: {
+ groupId: null,
+ },
+ });
+
+ vm = wrapper.vm;
return Vue.nextTick();
});
afterEach(() => {
- vm.$destroy();
+ wrapper.destroy();
mock.restore();
});
it('calls submit if submit button is clicked', () => {
- jest.spyOn(vm, 'submit').mockImplementation(e => e.preventDefault());
+ jest.spyOn(wrapper.vm, 'submit').mockImplementation();
vm.title = 'Testing Title';
- return Vue.nextTick().then(() => {
- vm.$el.querySelector('.btn-success').click();
-
- expect(vm.submit.mock.calls.length).toBe(1);
- });
+ return Vue.nextTick()
+ .then(submitIssue)
+ .then(() => {
+ expect(wrapper.vm.submit).toHaveBeenCalled();
+ });
});
it('disables submit button if title is empty', () => {
- expect(vm.$el.querySelector('.btn-success').disabled).toBe(true);
+ expect(wrapper.find({ ref: 'submitButton' }).props().disabled).toBe(true);
});
it('enables submit button if title is not empty', () => {
- vm.title = 'Testing Title';
+ wrapper.setData({ title: 'Testing Title' });
return Vue.nextTick().then(() => {
- expect(vm.$el.querySelector('.form-control').value).toBe('Testing Title');
- expect(vm.$el.querySelector('.btn-success').disabled).not.toBe(true);
+ expect(wrapper.find({ ref: 'input' }).element.value).toBe('Testing Title');
+ expect(wrapper.find({ ref: 'submitButton' }).props().disabled).toBe(false);
});
});
it('clears title after clicking cancel', () => {
- vm.$el.querySelector('.btn-default').click();
+ wrapper.find({ ref: 'cancelButton' }).trigger('click');
return Vue.nextTick().then(() => {
expect(vm.title).toBe('');
@@ -97,7 +103,7 @@ describe('Issue boards new issue form', () => {
describe('submit success', () => {
it('creates new issue', () => {
- vm.title = 'submit title';
+ wrapper.setData({ title: 'submit issue' });
return Vue.nextTick()
.then(submitIssue)
@@ -107,17 +113,18 @@ describe('Issue boards new issue form', () => {
});
it('enables button after submit', () => {
- vm.title = 'submit issue';
+ jest.spyOn(wrapper.vm, 'submit').mockImplementation();
+ wrapper.setData({ title: 'submit issue' });
return Vue.nextTick()
.then(submitIssue)
.then(() => {
- expect(vm.$el.querySelector('.btn-success').disabled).toBe(false);
+ expect(wrapper.vm.$refs.submitButton.props().disabled).toBe(false);
});
});
it('clears title after submit', () => {
- vm.title = 'submit issue';
+ wrapper.setData({ title: 'submit issue' });
return Vue.nextTick()
.then(submitIssue)
@@ -128,7 +135,7 @@ describe('Issue boards new issue form', () => {
it('sets detail issue after submit', () => {
expect(boardsStore.detail.issue.title).toBe(undefined);
- vm.title = 'submit issue';
+ wrapper.setData({ title: 'submit issue' });
return Vue.nextTick()
.then(submitIssue)
@@ -138,7 +145,7 @@ describe('Issue boards new issue form', () => {
});
it('sets detail list after submit', () => {
- vm.title = 'submit issue';
+ wrapper.setData({ title: 'submit issue' });
return Vue.nextTick()
.then(submitIssue)
@@ -149,7 +156,7 @@ describe('Issue boards new issue form', () => {
it('sets detail weight after submit', () => {
boardsStore.weightFeatureAvailable = true;
- vm.title = 'submit issue';
+ wrapper.setData({ title: 'submit issue' });
return Vue.nextTick()
.then(submitIssue)
@@ -160,7 +167,7 @@ describe('Issue boards new issue form', () => {
it('does not set detail weight after submit', () => {
boardsStore.weightFeatureAvailable = false;
- vm.title = 'submit issue';
+ wrapper.setData({ title: 'submit issue' });
return Vue.nextTick()
.then(submitIssue)
diff --git a/spec/frontend/boards/components/board_card_layout_spec.js b/spec/frontend/boards/components/board_card_layout_spec.js
index 2926f7d7e57..80f649a1a96 100644
--- a/spec/frontend/boards/components/board_card_layout_spec.js
+++ b/spec/frontend/boards/components/board_card_layout_spec.js
@@ -31,12 +31,14 @@ describe('Board card layout', () => {
propsData: {
list,
issue: list.issues[0],
- issueLinkBase: '/',
disabled: false,
index: 0,
- rootPath: '/',
...propsData,
},
+ provide: {
+ groupId: null,
+ rootPath: '/',
+ },
});
};
diff --git a/spec/frontend/boards/components/board_card_spec.js b/spec/frontend/boards/components/board_card_spec.js
index 1615a9d4df2..a3ddcdf01b7 100644
--- a/spec/frontend/boards/components/board_card_spec.js
+++ b/spec/frontend/boards/components/board_card_spec.js
@@ -38,12 +38,14 @@ describe('BoardCard', () => {
propsData: {
list,
issue: list.issues[0],
- issueLinkBase: '/',
disabled: false,
index: 0,
- rootPath: '/',
...propsData,
},
+ provide: {
+ groupId: null,
+ rootPath: '/',
+ },
});
};
diff --git a/spec/frontend/boards/components/board_column_spec.js b/spec/frontend/boards/components/board_column_spec.js
index c06b7aceaad..2a4dbbb989e 100644
--- a/spec/frontend/boards/components/board_column_spec.js
+++ b/spec/frontend/boards/components/board_column_spec.js
@@ -59,10 +59,11 @@ describe('Board Column Component', () => {
propsData: {
boardId,
disabled: false,
- issueLinkBase: '/',
- rootPath: '/',
list,
},
+ provide: {
+ boardId,
+ },
});
};
diff --git a/spec/frontend/boards/components/board_content_spec.js b/spec/frontend/boards/components/board_content_spec.js
index 2bf16b6b950..df117d06cdf 100644
--- a/spec/frontend/boards/components/board_content_spec.js
+++ b/spec/frontend/boards/components/board_content_spec.js
@@ -39,11 +39,7 @@ describe('BoardContent', () => {
propsData: {
lists: mockListsWithModel,
canAdminList: true,
- groupId: 1,
disabled: false,
- issueLinkBase: '/',
- rootPath: '/',
- boardId: '1',
},
store,
});
diff --git a/spec/frontend/boards/components/board_list_header_spec.js b/spec/frontend/boards/components/board_list_header_spec.js
index 85666d47cb0..2439c347bf0 100644
--- a/spec/frontend/boards/components/board_list_header_spec.js
+++ b/spec/frontend/boards/components/board_list_header_spec.js
@@ -57,12 +57,12 @@ describe('Board List Header Component', () => {
wrapper = shallowMount(BoardListHeader, {
propsData: {
- boardId,
disabled: false,
- issueLinkBase: '/',
- rootPath: '/',
list,
},
+ provide: {
+ boardId,
+ },
});
};
diff --git a/spec/frontend/boards/issue_card_spec.js b/spec/frontend/boards/issue_card_spec.js
index dee8cb7b6e5..7e22e9647f0 100644
--- a/spec/frontend/boards/issue_card_spec.js
+++ b/spec/frontend/boards/issue_card_spec.js
@@ -47,13 +47,15 @@ describe('Issue card component', () => {
propsData: {
list,
issue,
- issueLinkBase: '/test',
- rootPath: '/',
},
store,
stubs: {
GlLabel: true,
},
+ provide: {
+ groupId: null,
+ rootPath: '/',
+ },
});
});
diff --git a/spec/frontend/gfm_auto_complete_spec.js b/spec/frontend/gfm_auto_complete_spec.js
index 581120344de..6c40b1ba3a7 100644
--- a/spec/frontend/gfm_auto_complete_spec.js
+++ b/spec/frontend/gfm_auto_complete_spec.js
@@ -119,7 +119,7 @@ describe('GfmAutoComplete', () => {
const defaultMatcher = (context, flag, subtext) =>
gfmAutoCompleteCallbacks.matcher.call(context, flag, subtext);
- const flagsUseDefaultMatcher = ['@', '#', '!', '~', '%', '$'];
+ const flagsUseDefaultMatcher = ['@', '#', '!', '~', '%', '$', '+'];
const otherFlags = ['/', ':'];
const flags = flagsUseDefaultMatcher.concat(otherFlags);
@@ -153,7 +153,6 @@ describe('GfmAutoComplete', () => {
'я',
'.',
"'",
- '+',
'-',
'_',
];
diff --git a/spec/frontend/ide/components/ide_status_list_spec.js b/spec/frontend/ide/components/ide_status_list_spec.js
index fed61233e55..bb8165d1a52 100644
--- a/spec/frontend/ide/components/ide_status_list_spec.js
+++ b/spec/frontend/ide/components/ide_status_list_spec.js
@@ -75,7 +75,8 @@ describe('ide/components/ide_status_list', () => {
describe('with binary file', () => {
beforeEach(() => {
- activeFile.binary = true;
+ activeFile.name = 'abc.dat';
+ activeFile.content = '🐱'; // non-ascii binary content
createComponent();
});
diff --git a/spec/frontend/ide/components/new_dropdown/upload_spec.js b/spec/frontend/ide/components/new_dropdown/upload_spec.js
index ad27954cd10..ae497106f73 100644
--- a/spec/frontend/ide/components/new_dropdown/upload_spec.js
+++ b/spec/frontend/ide/components/new_dropdown/upload_spec.js
@@ -85,7 +85,6 @@ describe('new dropdown upload', () => {
name: textFile.name,
type: 'blob',
content: 'plain text',
- binary: false,
rawPath: '',
});
})
@@ -102,7 +101,6 @@ describe('new dropdown upload', () => {
name: binaryFile.name,
type: 'blob',
content: binaryTarget.result.split('base64,')[1],
- binary: true,
rawPath: binaryTarget.result,
});
});
diff --git a/spec/frontend/ide/components/repo_editor_spec.js b/spec/frontend/ide/components/repo_editor_spec.js
index f0ae2ba732b..9f4c9c1622a 100644
--- a/spec/frontend/ide/components/repo_editor_spec.js
+++ b/spec/frontend/ide/components/repo_editor_spec.js
@@ -45,7 +45,7 @@ describe('RepoEditor', () => {
const createOpenFile = path => {
const origFile = store.state.openFiles[0];
- const newFile = { ...origFile, path, key: path };
+ const newFile = { ...origFile, path, key: path, name: 'myfile.txt', content: 'hello world' };
store.state.entries[path] = newFile;
@@ -54,8 +54,9 @@ describe('RepoEditor', () => {
beforeEach(() => {
const f = {
- ...file(),
+ ...file('file.txt'),
viewMode: FILE_VIEW_MODE_EDITOR,
+ content: 'hello world',
};
const storeOptions = createStoreOptions();
@@ -142,6 +143,7 @@ describe('RepoEditor', () => {
...vm.file,
projectId: 'namespace/project',
path: 'sample.md',
+ name: 'sample.md',
content: 'testing 123',
});
@@ -200,7 +202,8 @@ describe('RepoEditor', () => {
describe('when open file is binary and not raw', () => {
beforeEach(done => {
- vm.file.binary = true;
+ vm.file.name = 'file.dat';
+ vm.file.content = '🐱'; // non-ascii binary content
vm.$nextTick(done);
});
@@ -407,6 +410,9 @@ describe('RepoEditor', () => {
beforeEach(done => {
jest.spyOn(vm.editor, 'updateDimensions').mockImplementation();
vm.file.viewMode = FILE_VIEW_MODE_PREVIEW;
+ vm.file.name = 'myfile.md';
+ vm.file.content = 'hello world';
+
vm.$nextTick(done);
});
@@ -650,7 +656,6 @@ describe('RepoEditor', () => {
path: 'foo/foo.png',
type: 'blob',
content: 'Zm9v',
- binary: true,
rawPath: 'data:image/png;base64,Zm9v',
});
});
diff --git a/spec/frontend/ide/lib/files_spec.js b/spec/frontend/ide/lib/files_spec.js
index b223cf64278..8ca6f01d9a6 100644
--- a/spec/frontend/ide/lib/files_spec.js
+++ b/spec/frontend/ide/lib/files_spec.js
@@ -1,11 +1,9 @@
-import { viewerInformationForPath } from '~/vue_shared/components/content_viewer/lib/viewer_utils';
import { decorateFiles, splitParent } from '~/ide/lib/files';
import { decorateData } from '~/ide/stores/utils';
const createEntries = paths => {
const createEntry = (acc, { path, type, children }) => {
const { name, parent } = splitParent(path);
- const previewMode = viewerInformationForPath(name);
acc[path] = {
...decorateData({
@@ -13,8 +11,6 @@ const createEntries = paths => {
name,
path,
type,
- previewMode,
- binary: (previewMode && previewMode.binary) || false,
parentPath: parent,
}),
tree: children.map(childName => expect.objectContaining({ name: childName })),
diff --git a/spec/frontend/ide/stores/actions/file_spec.js b/spec/frontend/ide/stores/actions/file_spec.js
index 5166e07abcb..974c0715c06 100644
--- a/spec/frontend/ide/stores/actions/file_spec.js
+++ b/spec/frontend/ide/stores/actions/file_spec.js
@@ -241,7 +241,6 @@ describe('IDE store file actions', () => {
200,
{
raw_path: 'raw_path',
- binary: false,
},
{
'page-title': 'testing getFileData',
@@ -305,7 +304,6 @@ describe('IDE store file actions', () => {
200,
{
raw_path: 'raw_path',
- binary: false,
},
{
'page-title': 'testing old-dull-file',
diff --git a/spec/frontend/ide/stores/mutations/file_spec.js b/spec/frontend/ide/stores/mutations/file_spec.js
index ff904bbc9cd..b53e40be980 100644
--- a/spec/frontend/ide/stores/mutations/file_spec.js
+++ b/spec/frontend/ide/stores/mutations/file_spec.js
@@ -61,13 +61,11 @@ describe('IDE store file mutations', () => {
mutations.SET_FILE_DATA(localState, {
data: {
raw_path: 'raw',
- binary: true,
},
file: localFile,
});
expect(localFile.rawPath).toBe('raw');
- expect(localFile.binary).toBeTruthy();
expect(localFile.raw).toBeNull();
expect(localFile.baseRaw).toBeNull();
});
diff --git a/spec/frontend/ide/utils_spec.js b/spec/frontend/ide/utils_spec.js
index 4a1e1f3c45e..97dc8217ecc 100644
--- a/spec/frontend/ide/utils_spec.js
+++ b/spec/frontend/ide/utils_spec.js
@@ -13,60 +13,78 @@ import {
describe('WebIDE utils', () => {
describe('isTextFile', () => {
- it('returns false for known binary types', () => {
- expect(isTextFile('file content', 'image/png', 'my.png')).toBeFalsy();
- // mime types are case insensitive
- expect(isTextFile('file content', 'IMAGE/PNG', 'my.png')).toBeFalsy();
+ it.each`
+ mimeType | name | type | result
+ ${'image/png'} | ${'my.png'} | ${'binary'} | ${false}
+ ${'IMAGE/PNG'} | ${'my.png'} | ${'binary'} | ${false}
+ ${'text/plain'} | ${'my.txt'} | ${'text'} | ${true}
+ ${'TEXT/PLAIN'} | ${'my.txt'} | ${'text'} | ${true}
+ `('returns $result for known $type types', ({ mimeType, name, result }) => {
+ expect(isTextFile({ content: 'file content', mimeType, name })).toBe(result);
});
- it('returns true for known text types', () => {
- expect(isTextFile('file content', 'text/plain', 'my.txt')).toBeTruthy();
- // mime types are case insensitive
- expect(isTextFile('file content', 'TEXT/PLAIN', 'my.txt')).toBeTruthy();
- });
+ it.each`
+ content | mimeType | name
+ ${'{"éêė":"value"}'} | ${'application/json'} | ${'my.json'}
+ ${'{"éêė":"value"}'} | ${'application/json'} | ${'.tsconfig'}
+ ${'SELECT "éêė" from tablename'} | ${'application/sql'} | ${'my.sql'}
+ ${'{"éêė":"value"}'} | ${'application/json'} | ${'MY.JSON'}
+ ${'SELECT "éêė" from tablename'} | ${'application/sql'} | ${'MY.SQL'}
+ ${'var code = "something"'} | ${'application/javascript'} | ${'Gruntfile'}
+ ${'MAINTAINER Александр "a21283@me.com"'} | ${'application/octet-stream'} | ${'dockerfile'}
+ `(
+ 'returns true for file extensions that Monaco supports syntax highlighting for',
+ ({ content, mimeType, name }) => {
+ expect(isTextFile({ content, mimeType, name })).toBe(true);
+ },
+ );
- it('returns true for file extensions that Monaco supports syntax highlighting for', () => {
- // test based on both MIME and extension
- expect(isTextFile('{"éêė":"value"}', 'application/json', 'my.json')).toBeTruthy();
- expect(isTextFile('{"éêė":"value"}', 'application/json', '.tsconfig')).toBeTruthy();
- expect(isTextFile('SELECT "éêė" from tablename', 'application/sql', 'my.sql')).toBeTruthy();
+ it('returns false if filename is same as the expected extension', () => {
+ expect(
+ isTextFile({
+ name: 'sql',
+ content: 'SELECT "éêė" from tablename',
+ mimeType: 'application/sql',
+ }),
+ ).toBeFalsy();
});
- it('returns true even irrespective of whether the mimes, extensions or file names are lowercase or upper case', () => {
- expect(isTextFile('{"éêė":"value"}', 'application/json', 'MY.JSON')).toBeTruthy();
- expect(isTextFile('SELECT "éêė" from tablename', 'application/sql', 'MY.SQL')).toBeTruthy();
- expect(
- isTextFile('var code = "something"', 'application/javascript', 'Gruntfile'),
- ).toBeTruthy();
+ it('returns true for ASCII only content for unknown types', () => {
expect(
- isTextFile(
- 'MAINTAINER Александр "alexander11354322283@me.com"',
- 'application/octet-stream',
- 'dockerfile',
- ),
+ isTextFile({
+ name: 'hello.mytype',
+ content: 'plain text',
+ mimeType: 'application/x-new-type',
+ }),
).toBeTruthy();
});
- it('returns false if filename is same as the expected extension', () => {
- expect(isTextFile('SELECT "éêė" from tablename', 'application/sql', 'sql')).toBeFalsy();
- });
-
- it('returns true for ASCII only content for unknown types', () => {
- expect(isTextFile('plain text', 'application/x-new-type', 'hello.mytype')).toBeTruthy();
+ it('returns false for non-ASCII content for unknown types', () => {
+ expect(
+ isTextFile({
+ name: 'my.random',
+ content: '{"éêė":"value"}',
+ mimeType: 'application/octet-stream',
+ }),
+ ).toBeFalsy();
});
- it('returns true for relevant filenames', () => {
- expect(
- isTextFile(
- 'MAINTAINER Александр "alexander11354322283@me.com"',
- 'application/octet-stream',
- 'Dockerfile',
- ),
- ).toBeTruthy();
+ it.each`
+ name | result
+ ${'myfile.txt'} | ${true}
+ ${'Dockerfile'} | ${true}
+ ${'img.png'} | ${false}
+ ${'abc.js'} | ${true}
+ ${'abc.random'} | ${false}
+ ${'image.jpeg'} | ${false}
+ `('returns $result for $filename when no content or mimeType is passed', ({ name, result }) => {
+ expect(isTextFile({ name })).toBe(result);
});
- it('returns false for non-ASCII content for unknown types', () => {
- expect(isTextFile('{"éêė":"value"}', 'application/octet-stream', 'my.random')).toBeFalsy();
+ it('returns true if content is empty string but false if content is not passed', () => {
+ expect(isTextFile({ name: 'abc.dat' })).toBe(false);
+ expect(isTextFile({ name: 'abc.dat', content: '' })).toBe(true);
+ expect(isTextFile({ name: 'abc.dat', content: ' ' })).toBe(true);
});
});
diff --git a/spec/frontend/integrations/edit/components/integration_form_spec.js b/spec/frontend/integrations/edit/components/integration_form_spec.js
index fccb56f4597..eeb5d21d62c 100644
--- a/spec/frontend/integrations/edit/components/integration_form_spec.js
+++ b/spec/frontend/integrations/edit/components/integration_form_spec.js
@@ -137,13 +137,13 @@ describe('IntegrationForm', () => {
});
});
- describe('adminState state is null', () => {
+ describe('defaultState state is null', () => {
it('does not render OverrideDropdown', () => {
createComponent(
{},
{},
{
- adminState: null,
+ defaultState: null,
},
);
@@ -151,13 +151,13 @@ describe('IntegrationForm', () => {
});
});
- describe('adminState state is an object', () => {
+ describe('defaultState state is an object', () => {
it('renders OverrideDropdown', () => {
createComponent(
{},
{},
{
- adminState: {
+ defaultState: {
...mockIntegrationProps,
},
},
diff --git a/spec/frontend/integrations/edit/components/override_dropdown_spec.js b/spec/frontend/integrations/edit/components/override_dropdown_spec.js
index 44e14cb3760..f312c456d5f 100644
--- a/spec/frontend/integrations/edit/components/override_dropdown_spec.js
+++ b/spec/frontend/integrations/edit/components/override_dropdown_spec.js
@@ -13,15 +13,15 @@ describe('OverrideDropdown', () => {
override: true,
};
- const defaultAdminStateProps = {
+ const defaultDefaultStateProps = {
integrationLevel: 'group',
};
- const createComponent = (props = {}, adminStateProps = {}) => {
+ const createComponent = (props = {}, defaultStateProps = {}) => {
wrapper = shallowMount(OverrideDropdown, {
propsData: { ...defaultProps, ...props },
store: createStore({
- adminState: { ...defaultAdminStateProps, ...adminStateProps },
+ defaultState: { ...defaultDefaultStateProps, ...defaultStateProps },
}),
});
};
diff --git a/spec/frontend/integrations/edit/store/getters_spec.js b/spec/frontend/integrations/edit/store/getters_spec.js
index 700d36edaad..3353e0c84cc 100644
--- a/spec/frontend/integrations/edit/store/getters_spec.js
+++ b/spec/frontend/integrations/edit/store/getters_spec.js
@@ -5,22 +5,22 @@ import { mockIntegrationProps } from '../mock_data';
describe('Integration form store getters', () => {
let state;
const customState = { ...mockIntegrationProps, type: 'CustomState' };
- const adminState = { ...mockIntegrationProps, type: 'AdminState' };
+ const defaultState = { ...mockIntegrationProps, type: 'DefaultState' };
beforeEach(() => {
state = createState({ customState });
});
describe('isInheriting', () => {
- describe('when adminState is null', () => {
+ describe('when defaultState is null', () => {
it('returns false', () => {
expect(isInheriting(state)).toBe(false);
});
});
- describe('when adminState is an object', () => {
+ describe('when defaultState is an object', () => {
beforeEach(() => {
- state.adminState = adminState;
+ state.defaultState = defaultState;
});
describe('when override is false', () => {
@@ -47,11 +47,11 @@ describe('Integration form store getters', () => {
describe('propsSource', () => {
beforeEach(() => {
- state.adminState = adminState;
+ state.defaultState = defaultState;
});
- it('equals adminState if inheriting', () => {
- expect(propsSource(state, { isInheriting: true })).toEqual(adminState);
+ it('equals defaultState if inheriting', () => {
+ expect(propsSource(state, { isInheriting: true })).toEqual(defaultState);
});
it('equals customState if not inheriting', () => {
diff --git a/spec/frontend/integrations/edit/store/state_spec.js b/spec/frontend/integrations/edit/store/state_spec.js
index 2518e5c7b93..fc193850a94 100644
--- a/spec/frontend/integrations/edit/store/state_spec.js
+++ b/spec/frontend/integrations/edit/store/state_spec.js
@@ -3,7 +3,7 @@ import createState from '~/integrations/edit/store/state';
describe('Integration form state factory', () => {
it('states default to null', () => {
expect(createState()).toEqual({
- adminState: null,
+ defaultState: null,
customState: {},
isSaving: false,
isTesting: false,
@@ -19,9 +19,9 @@ describe('Integration form state factory', () => {
[null, { inheritFromId: null }, false],
[null, { inheritFromId: 25 }, false],
])(
- 'for adminState: %p, customState: %p: override = `%p`',
- (adminState, customState, expected) => {
- expect(createState({ adminState, customState }).override).toEqual(expected);
+ 'for defaultState: %p, customState: %p: override = `%p`',
+ (defaultState, customState, expected) => {
+ expect(createState({ defaultState, customState }).override).toEqual(expected);
},
);
});
diff --git a/spec/frontend/issuable_create/components/issuable_form_spec.js b/spec/frontend/issuable_create/components/issuable_form_spec.js
index 0d922727209..e2c6b4d9521 100644
--- a/spec/frontend/issuable_create/components/issuable_form_spec.js
+++ b/spec/frontend/issuable_create/components/issuable_form_spec.js
@@ -65,6 +65,7 @@ describe('IssuableForm', () => {
expect(titleFieldEl.find('label').text()).toBe('Title');
expect(titleFieldEl.find(GlFormInput).exists()).toBe(true);
expect(titleFieldEl.find(GlFormInput).attributes('placeholder')).toBe('Title');
+ expect(titleFieldEl.find(GlFormInput).attributes('autofocus')).toBe('true');
});
it('renders issuable description input field', () => {
diff --git a/spec/frontend/issuable_list/components/issuable_item_spec.js b/spec/frontend/issuable_list/components/issuable_item_spec.js
new file mode 100644
index 00000000000..a96a4e15e6c
--- /dev/null
+++ b/spec/frontend/issuable_list/components/issuable_item_spec.js
@@ -0,0 +1,185 @@
+import { shallowMount } from '@vue/test-utils';
+import { GlLink, GlLabel } from '@gitlab/ui';
+
+import IssuableItem from '~/issuable_list/components/issuable_item.vue';
+
+import { mockIssuable, mockRegularLabel, mockScopedLabel } from '../mock_data';
+
+const createComponent = ({ issuableSymbol = '#', issuable = mockIssuable } = {}) =>
+ shallowMount(IssuableItem, {
+ propsData: {
+ issuableSymbol,
+ issuable,
+ },
+ });
+
+describe('IssuableItem', () => {
+ const mockLabels = mockIssuable.labels.nodes;
+ const mockAuthor = mockIssuable.author;
+ let wrapper;
+
+ beforeEach(() => {
+ wrapper = createComponent();
+ });
+
+ afterEach(() => {
+ wrapper.destroy();
+ });
+
+ describe('computed', () => {
+ describe('author', () => {
+ it('returns `issuable.author` reference', () => {
+ expect(wrapper.vm.author).toEqual(mockIssuable.author);
+ });
+ });
+
+ describe('authorId', () => {
+ it.each`
+ authorId | returnValue
+ ${1} | ${1}
+ ${'1'} | ${1}
+ ${'gid://gitlab/User/1'} | ${'1'}
+ ${'foo'} | ${''}
+ `(
+ 'returns $returnValue when value of `issuable.author.id` is $authorId',
+ async ({ authorId, returnValue }) => {
+ wrapper.setProps({
+ issuable: {
+ ...mockIssuable,
+ author: {
+ ...mockAuthor,
+ id: authorId,
+ },
+ },
+ });
+
+ await wrapper.vm.$nextTick();
+
+ expect(wrapper.vm.authorId).toBe(returnValue);
+ },
+ );
+ });
+
+ describe('labels', () => {
+ it('returns `issuable.labels.nodes` reference when it is available', () => {
+ expect(wrapper.vm.labels).toEqual(mockLabels);
+ });
+
+ it('returns `issuable.labels` reference when it is available', async () => {
+ wrapper.setProps({
+ issuable: {
+ ...mockIssuable,
+ labels: mockLabels,
+ },
+ });
+
+ await wrapper.vm.$nextTick();
+
+ expect(wrapper.vm.labels).toEqual(mockLabels);
+ });
+
+ it('returns empty array when none of `issuable.labels.nodes` or `issuable.labels` are available', async () => {
+ wrapper.setProps({
+ issuable: {
+ ...mockIssuable,
+ labels: null,
+ },
+ });
+
+ await wrapper.vm.$nextTick();
+
+ expect(wrapper.vm.labels).toEqual([]);
+ });
+ });
+
+ describe('createdAt', () => {
+ it('returns string containing timeago string based on `issuable.createdAt`', () => {
+ expect(wrapper.vm.createdAt).toContain('created');
+ expect(wrapper.vm.createdAt).toContain('ago');
+ });
+ });
+
+ describe('updatedAt', () => {
+ it('returns string containing timeago string based on `issuable.updatedAt`', () => {
+ expect(wrapper.vm.updatedAt).toContain('updated');
+ expect(wrapper.vm.updatedAt).toContain('ago');
+ });
+ });
+ });
+
+ describe('methods', () => {
+ describe('scopedLabel', () => {
+ it.each`
+ label | labelType | returnValue
+ ${mockRegularLabel} | ${'regular'} | ${false}
+ ${mockScopedLabel} | ${'scoped'} | ${true}
+ `(
+ 'return $returnValue when provided label param is a $labelType label',
+ ({ label, returnValue }) => {
+ expect(wrapper.vm.scopedLabel(label)).toBe(returnValue);
+ },
+ );
+ });
+ });
+
+ describe('template', () => {
+ it('renders issuable title', () => {
+ const titleEl = wrapper.find('[data-testid="issuable-title"]');
+
+ expect(titleEl.exists()).toBe(true);
+ expect(titleEl.find(GlLink).attributes('href')).toBe(mockIssuable.webUrl);
+ expect(titleEl.find(GlLink).text()).toBe(mockIssuable.title);
+ });
+
+ it('renders issuable reference', () => {
+ const referenceEl = wrapper.find('[data-testid="issuable-reference"]');
+
+ expect(referenceEl.exists()).toBe(true);
+ expect(referenceEl.text()).toBe(`#${mockIssuable.iid}`);
+ });
+
+ it('renders issuable createdAt info', () => {
+ const createdAtEl = wrapper.find('[data-testid="issuable-created-at"]');
+
+ expect(createdAtEl.exists()).toBe(true);
+ expect(createdAtEl.attributes('title')).toBe('Jun 29, 2020 1:52pm GMT+0000');
+ expect(createdAtEl.text()).toBe(wrapper.vm.createdAt);
+ });
+
+ it('renders issuable author info', () => {
+ const authorEl = wrapper.find('[data-testid="issuable-author"]');
+
+ expect(authorEl.exists()).toBe(true);
+ expect(authorEl.attributes()).toMatchObject({
+ 'data-user-id': wrapper.vm.authorId,
+ 'data-username': mockAuthor.username,
+ 'data-name': mockAuthor.name,
+ 'data-avatar-url': mockAuthor.avatarUrl,
+ href: mockAuthor.webUrl,
+ });
+ expect(authorEl.text()).toBe(mockAuthor.name);
+ });
+
+ it('renders gl-label component for each label present within `issuable` prop', () => {
+ const labelsEl = wrapper.findAll(GlLabel);
+
+ expect(labelsEl.exists()).toBe(true);
+ expect(labelsEl).toHaveLength(mockLabels.length);
+ expect(labelsEl.at(0).props()).toMatchObject({
+ backgroundColor: mockLabels[0].color,
+ title: mockLabels[0].title,
+ description: mockLabels[0].description,
+ scoped: false,
+ size: 'sm',
+ });
+ });
+
+ it('renders issuable updatedAt info', () => {
+ const updatedAtEl = wrapper.find('[data-testid="issuable-updated-at"]');
+
+ expect(updatedAtEl.exists()).toBe(true);
+ expect(updatedAtEl.find('span').attributes('title')).toBe('Sep 10, 2020 11:41am GMT+0000');
+ expect(updatedAtEl.text()).toBe(wrapper.vm.updatedAt);
+ });
+ });
+});
diff --git a/spec/frontend/issuable_list/components/issuable_list_root_spec.js b/spec/frontend/issuable_list/components/issuable_list_root_spec.js
new file mode 100644
index 00000000000..34184522b55
--- /dev/null
+++ b/spec/frontend/issuable_list/components/issuable_list_root_spec.js
@@ -0,0 +1,160 @@
+import { mount } from '@vue/test-utils';
+import { GlLoadingIcon, GlPagination } from '@gitlab/ui';
+
+import IssuableListRoot from '~/issuable_list/components/issuable_list_root.vue';
+import IssuableTabs from '~/issuable_list/components/issuable_tabs.vue';
+import IssuableItem from '~/issuable_list/components/issuable_item.vue';
+import FilteredSearchBar from '~/vue_shared/components/filtered_search_bar/filtered_search_bar_root.vue';
+
+import { mockIssuableListProps } from '../mock_data';
+
+const createComponent = (propsData = mockIssuableListProps) =>
+ mount(IssuableListRoot, {
+ propsData,
+ slots: {
+ 'nav-actions': `
+ <button class="js-new-issuable">New issuable</button>
+ `,
+ 'empty-state': `
+ <p class="js-issuable-empty-state">Issuable empty state</p>
+ `,
+ },
+ });
+
+describe('IssuableListRoot', () => {
+ let wrapper;
+
+ beforeEach(() => {
+ wrapper = createComponent();
+ });
+
+ afterEach(() => {
+ wrapper.destroy();
+ });
+
+ describe('template', () => {
+ it('renders component container element with class "issuable-list-container"', () => {
+ expect(wrapper.classes()).toContain('issuable-list-container');
+ });
+
+ it('renders issuable-tabs component', () => {
+ const tabsEl = wrapper.find(IssuableTabs);
+
+ expect(tabsEl.exists()).toBe(true);
+ expect(tabsEl.props()).toMatchObject({
+ tabs: wrapper.vm.tabs,
+ tabCounts: wrapper.vm.tabCounts,
+ currentTab: wrapper.vm.currentTab,
+ });
+ });
+
+ it('renders contents for slot "nav-actions" within issuable-tab component', () => {
+ const buttonEl = wrapper.find(IssuableTabs).find('button.js-new-issuable');
+
+ expect(buttonEl.exists()).toBe(true);
+ expect(buttonEl.text()).toBe('New issuable');
+ });
+
+ it('renders filtered-search-bar component', () => {
+ const searchEl = wrapper.find(FilteredSearchBar);
+ const {
+ namespace,
+ recentSearchesStorageKey,
+ searchInputPlaceholder,
+ searchTokens,
+ sortOptions,
+ initialFilterValue,
+ initialSortBy,
+ } = wrapper.vm;
+
+ expect(searchEl.exists()).toBe(true);
+ expect(searchEl.props()).toMatchObject({
+ namespace,
+ recentSearchesStorageKey,
+ searchInputPlaceholder,
+ tokens: searchTokens,
+ sortOptions,
+ initialFilterValue,
+ initialSortBy,
+ });
+ });
+
+ it('renders gl-loading-icon when `issuablesLoading` prop is true', async () => {
+ wrapper.setProps({
+ issuablesLoading: true,
+ });
+
+ await wrapper.vm.$nextTick();
+
+ expect(wrapper.find(GlLoadingIcon).exists()).toBe(true);
+ });
+
+ it('renders issuable-item component for each item within `issuables` array', () => {
+ const itemsEl = wrapper.findAll(IssuableItem);
+ const mockIssuable = mockIssuableListProps.issuables[0];
+
+ expect(itemsEl).toHaveLength(mockIssuableListProps.issuables.length);
+ expect(itemsEl.at(0).props()).toMatchObject({
+ issuableSymbol: wrapper.vm.issuableSymbol,
+ issuable: mockIssuable,
+ });
+ });
+
+ it('renders contents for slot "empty-state" when `issuablesLoading` is false and `issuables` is empty', async () => {
+ wrapper.setProps({
+ issuables: [],
+ });
+
+ await wrapper.vm.$nextTick();
+
+ expect(wrapper.find('p.js-issuable-empty-state').exists()).toBe(true);
+ expect(wrapper.find('p.js-issuable-empty-state').text()).toBe('Issuable empty state');
+ });
+
+ it('renders gl-pagination when `showPaginationControls` prop is true', async () => {
+ wrapper.setProps({
+ showPaginationControls: true,
+ });
+
+ await wrapper.vm.$nextTick();
+
+ const paginationEl = wrapper.find(GlPagination);
+ expect(paginationEl.exists()).toBe(true);
+ expect(paginationEl.props()).toMatchObject({
+ perPage: 20,
+ value: 1,
+ prevPage: 0,
+ nextPage: 2,
+ align: 'center',
+ });
+ });
+ });
+
+ describe('events', () => {
+ it('issuable-tabs component emits `click-tab` event on `click-tab` event', () => {
+ wrapper.find(IssuableTabs).vm.$emit('click');
+
+ expect(wrapper.emitted('click-tab')).toBeTruthy();
+ });
+
+ it('filtered-search-bar component emits `filter` event on `onFilter` & `sort` event on `onSort` events', () => {
+ const searchEl = wrapper.find(FilteredSearchBar);
+
+ searchEl.vm.$emit('onFilter');
+ expect(wrapper.emitted('filter')).toBeTruthy();
+ searchEl.vm.$emit('onSort');
+ expect(wrapper.emitted('sort')).toBeTruthy();
+ });
+
+ it('gl-pagination component emits `page-change` event on `input` event', async () => {
+ wrapper.setProps({
+ showPaginationControls: true,
+ });
+
+ await wrapper.vm.$nextTick();
+
+ wrapper.find(GlPagination).vm.$emit('input');
+ expect(wrapper.emitted('page-change')).toBeTruthy();
+ });
+ });
+});
diff --git a/spec/frontend/issuable_list/components/issuable_tabs_spec.js b/spec/frontend/issuable_list/components/issuable_tabs_spec.js
new file mode 100644
index 00000000000..12611400084
--- /dev/null
+++ b/spec/frontend/issuable_list/components/issuable_tabs_spec.js
@@ -0,0 +1,91 @@
+import { mount } from '@vue/test-utils';
+import { GlTab, GlBadge } from '@gitlab/ui';
+
+import IssuableTabs from '~/issuable_list/components/issuable_tabs.vue';
+
+import { mockIssuableListProps } from '../mock_data';
+
+const createComponent = ({
+ tabs = mockIssuableListProps.tabs,
+ tabCounts = mockIssuableListProps.tabCounts,
+ currentTab = mockIssuableListProps.currentTab,
+} = {}) =>
+ mount(IssuableTabs, {
+ propsData: {
+ tabs,
+ tabCounts,
+ currentTab,
+ },
+ slots: {
+ 'nav-actions': `
+ <button class="js-new-issuable">New issuable</button>
+ `,
+ },
+ });
+
+describe('IssuableTabs', () => {
+ let wrapper;
+
+ beforeEach(() => {
+ wrapper = createComponent();
+ });
+
+ afterEach(() => {
+ wrapper.destroy();
+ });
+
+ describe('methods', () => {
+ describe('isTabActive', () => {
+ it.each`
+ tabName | currentTab | returnValue
+ ${'opened'} | ${'opened'} | ${true}
+ ${'opened'} | ${'closed'} | ${false}
+ `(
+ 'returns $returnValue when tab name is "$tabName" is current tab is "$currentTab"',
+ async ({ tabName, currentTab, returnValue }) => {
+ wrapper.setProps({
+ currentTab,
+ });
+
+ await wrapper.vm.$nextTick();
+
+ expect(wrapper.vm.isTabActive(tabName)).toBe(returnValue);
+ },
+ );
+ });
+ });
+
+ describe('template', () => {
+ it('renders gl-tab for each tab within `tabs` array', () => {
+ const tabsEl = wrapper.findAll(GlTab);
+
+ expect(tabsEl.exists()).toBe(true);
+ expect(tabsEl).toHaveLength(mockIssuableListProps.tabs.length);
+ });
+
+ it('renders gl-badge component within a tab', () => {
+ const badgeEl = wrapper.findAll(GlBadge).at(0);
+
+ expect(badgeEl.exists()).toBe(true);
+ expect(badgeEl.text()).toBe(`${mockIssuableListProps.tabCounts.opened}`);
+ });
+
+ it('renders contents for slot "nav-actions"', () => {
+ const buttonEl = wrapper.find('button.js-new-issuable');
+
+ expect(buttonEl.exists()).toBe(true);
+ expect(buttonEl.text()).toBe('New issuable');
+ });
+ });
+
+ describe('events', () => {
+ it('gl-tab component emits `click` event on `click` event', () => {
+ const tabEl = wrapper.findAll(GlTab).at(0);
+
+ tabEl.vm.$emit('click', 'opened');
+
+ expect(wrapper.emitted('click')).toBeTruthy();
+ expect(wrapper.emitted('click')[0]).toEqual(['opened']);
+ });
+ });
+});
diff --git a/spec/frontend/issuable_list/mock_data.js b/spec/frontend/issuable_list/mock_data.js
new file mode 100644
index 00000000000..f6f914a595d
--- /dev/null
+++ b/spec/frontend/issuable_list/mock_data.js
@@ -0,0 +1,135 @@
+import {
+ mockAuthorToken,
+ mockLabelToken,
+ mockSortOptions,
+} from 'jest/vue_shared/components/filtered_search_bar/mock_data';
+
+export const mockAuthor = {
+ id: 'gid://gitlab/User/1',
+ avatarUrl: 'https://www.gravatar.com/avatar/e64c7d89f26bd1972efa854d13d7dd61?s=80&d=identicon',
+ name: 'Administrator',
+ username: 'root',
+ webUrl: 'http://0.0.0.0:3000/root',
+};
+
+export const mockRegularLabel = {
+ id: 'gid://gitlab/GroupLabel/2048',
+ title: 'Documentation Update',
+ description: null,
+ color: '#F0AD4E',
+ textColor: '#FFFFFF',
+};
+
+export const mockScopedLabel = {
+ id: 'gid://gitlab/ProjectLabel/2049',
+ title: 'status::confirmed',
+ description: null,
+ color: '#D9534F',
+ textColor: '#FFFFFF',
+};
+
+export const mockLabels = [mockRegularLabel, mockScopedLabel];
+
+export const mockIssuable = {
+ iid: '30',
+ title: 'Dismiss Cipher with no integrity',
+ description: null,
+ createdAt: '2020-06-29T13:52:56Z',
+ updatedAt: '2020-09-10T11:41:13Z',
+ webUrl: 'http://0.0.0.0:3000/gitlab-org/gitlab-shell/-/issues/30',
+ author: mockAuthor,
+ labels: {
+ nodes: mockLabels,
+ },
+};
+
+export const mockIssuables = [
+ mockIssuable,
+ {
+ iid: '28',
+ title: 'Dismiss Cipher with no integrity',
+ description: null,
+ createdAt: '2020-06-29T13:52:56Z',
+ updatedAt: '2020-06-29T13:52:56Z',
+ webUrl: 'http://0.0.0.0:3000/gitlab-org/gitlab-shell/-/issues/28',
+ author: mockAuthor,
+ labels: {
+ nodes: [],
+ },
+ },
+ {
+ iid: '7',
+ title: 'Temporibus in veritatis labore explicabo velit molestiae sed.',
+ description: 'Quo consequatur rem aliquid laborum quibusdam molestiae saepe.',
+ createdAt: '2020-06-25T13:50:14Z',
+ updatedAt: '2020-08-25T06:09:27Z',
+ webUrl: 'http://0.0.0.0:3000/gitlab-org/gitlab-shell/-/issues/7',
+ author: mockAuthor,
+ labels: {
+ nodes: mockLabels,
+ },
+ },
+ {
+ iid: '17',
+ title: 'Vel voluptatem quaerat est hic incidunt qui ut aliquid sit exercitationem.',
+ description: 'Incidunt accusamus perspiciatis aut excepturi.',
+ createdAt: '2020-06-19T13:51:36Z',
+ updatedAt: '2020-08-11T13:36:35Z',
+ webUrl: 'http://0.0.0.0:3000/gitlab-org/gitlab-shell/-/issues/17',
+ author: mockAuthor,
+ labels: {
+ nodes: [],
+ },
+ },
+ {
+ iid: '16',
+ title: 'Vero qui quo labore libero omnis quisquam et cumque.',
+ description: 'Ipsa ipsum magni nostrum alias aut exercitationem.',
+ createdAt: '2020-06-19T13:51:36Z',
+ updatedAt: '2020-06-19T13:51:36Z',
+ webUrl: 'http://0.0.0.0:3000/gitlab-org/gitlab-shell/-/issues/16',
+ author: mockAuthor,
+ labels: {
+ nodes: [],
+ },
+ },
+];
+
+export const mockTabs = [
+ {
+ id: 'state-opened',
+ name: 'opened',
+ title: 'Open',
+ titleTooltip: 'Filter by issuables that are currently opened.',
+ },
+ {
+ id: 'state-archived',
+ name: 'closed',
+ title: 'Closed',
+ titleTooltip: 'Filter by issuables that are currently archived.',
+ },
+ {
+ id: 'state-all',
+ name: 'all',
+ title: 'All',
+ titleTooltip: 'Show all issuables.',
+ },
+];
+
+export const mockTabCounts = {
+ opened: 5,
+ closed: 0,
+ all: 5,
+};
+
+export const mockIssuableListProps = {
+ namespace: 'gitlab-org/gitlab-test',
+ recentSearchesStorageKey: 'issues',
+ searchInputPlaceholder: 'Search issues',
+ searchTokens: [mockAuthorToken, mockLabelToken],
+ sortOptions: mockSortOptions,
+ issuables: mockIssuables,
+ tabs: mockTabs,
+ tabCounts: mockTabCounts,
+ currentTab: 'opened',
+};
diff --git a/spec/lib/gitlab/background_migration/migrate_to_hashed_storage_spec.rb b/spec/lib/gitlab/background_migration/migrate_to_hashed_storage_spec.rb
new file mode 100644
index 00000000000..0f7bb06e830
--- /dev/null
+++ b/spec/lib/gitlab/background_migration/migrate_to_hashed_storage_spec.rb
@@ -0,0 +1,43 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+# rubocop:disable RSpec/FactoriesInMigrationSpecs
+RSpec.describe Gitlab::BackgroundMigration::MigrateToHashedStorage, :sidekiq, :redis do
+ let(:migrator) { Gitlab::HashedStorage::Migrator.new }
+
+ subject(:background_migration) { described_class.new }
+
+ describe '#perform' do
+ let!(:project) { create(:project, :empty_repo, :legacy_storage) }
+
+ context 'with pending rollback' do
+ it 'aborts rollback operation' do
+ Sidekiq::Testing.disable! do
+ Sidekiq::Client.push(
+ 'queue' => ::HashedStorage::ProjectRollbackWorker.queue,
+ 'class' => ::HashedStorage::ProjectRollbackWorker,
+ 'args' => [project.id]
+ )
+
+ expect { background_migration.perform }.to change { migrator.rollback_pending? }.from(true).to(false)
+ end
+ end
+ end
+
+ it 'enqueues legacy projects to be migrated' do
+ Sidekiq::Testing.fake! do
+ expect { background_migration.perform }.to change { Sidekiq::Queues[::HashedStorage::MigratorWorker.queue].size }.by(1)
+ end
+ end
+
+ context 'when executing all jobs' do
+ it 'migrates legacy projects' do
+ Sidekiq::Testing.inline! do
+ expect { background_migration.perform }.to change { project.reload.legacy_storage? }.from(true).to(false)
+ end
+ end
+ end
+ end
+end
+# rubocop:enable RSpec/FactoriesInMigrationSpecs
diff --git a/spec/lib/gitlab/hashed_storage/migrator_spec.rb b/spec/lib/gitlab/hashed_storage/migrator_spec.rb
index 0549b3128c7..f4f15cab05a 100644
--- a/spec/lib/gitlab/hashed_storage/migrator_spec.rb
+++ b/spec/lib/gitlab/hashed_storage/migrator_spec.rb
@@ -232,4 +232,16 @@ RSpec.describe Gitlab::HashedStorage::Migrator, :redis do
expect(subject.rollback_pending?).to be_falsey
end
end
+
+ describe 'abort_rollback!' do
+ let_it_be(:project) { create(:project, :empty_repo) }
+
+ it 'removes any rollback related scheduled job' do
+ Sidekiq::Testing.disable! do
+ ::HashedStorage::RollbackerWorker.perform_async(1, 5)
+
+ expect { subject.abort_rollback! }.to change { subject.rollback_pending? }.from(true).to(false)
+ end
+ end
+ end
end
diff --git a/spec/lib/gitlab/import_export/all_models.yml b/spec/lib/gitlab/import_export/all_models.yml
index 5f691799e9c..b3836e5dec7 100644
--- a/spec/lib/gitlab/import_export/all_models.yml
+++ b/spec/lib/gitlab/import_export/all_models.yml
@@ -299,6 +299,7 @@ protected_branches:
- merge_access_levels
- push_access_levels
- unprotect_access_levels
+- approval_project_rules
protected_tags:
- project
- create_access_levels
diff --git a/spec/migrations/20200915044225_schedule_migration_to_hashed_storage_spec.rb b/spec/migrations/20200915044225_schedule_migration_to_hashed_storage_spec.rb
new file mode 100644
index 00000000000..20ba2fbccea
--- /dev/null
+++ b/spec/migrations/20200915044225_schedule_migration_to_hashed_storage_spec.rb
@@ -0,0 +1,14 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+require Rails.root.join('db', 'post_migrate', '20200915044225_schedule_migration_to_hashed_storage.rb')
+
+RSpec.describe ScheduleMigrationToHashedStorage, :sidekiq do
+ describe '#up' do
+ it 'schedules background migration job' do
+ Sidekiq::Testing.fake! do
+ expect { migrate! }.to change { BackgroundMigrationWorker.jobs.size }.by(1)
+ end
+ end
+ end
+end
diff --git a/spec/services/projects/open_issues_count_service_spec.rb b/spec/services/projects/open_issues_count_service_spec.rb
index c739fea5ecf..294c9adcc92 100644
--- a/spec/services/projects/open_issues_count_service_spec.rb
+++ b/spec/services/projects/open_issues_count_service_spec.rb
@@ -10,6 +10,14 @@ RSpec.describe Projects::OpenIssuesCountService, :use_clean_rails_memory_store_c
it_behaves_like 'a counter caching service'
describe '#count' do
+ it 'does not count test cases' do
+ create(:issue, :opened, project: project)
+ create(:incident, :opened, project: project)
+ create(:quality_test_case, :opened, project: project)
+
+ expect(described_class.new(project).count).to eq(2)
+ end
+
context 'when user is nil' do
it 'does not include confidential issues in the issue count' do
create(:issue, :opened, project: project)
diff --git a/spec/workers/git_garbage_collect_worker_spec.rb b/spec/workers/git_garbage_collect_worker_spec.rb
index 223f5aea813..1be6e86b650 100644
--- a/spec/workers/git_garbage_collect_worker_spec.rb
+++ b/spec/workers/git_garbage_collect_worker_spec.rb
@@ -25,12 +25,18 @@ RSpec.describe GitGarbageCollectWorker do
end
shared_examples 'it updates the project statistics' do
- specify do
- expect_any_instance_of(Projects::UpdateStatisticsService).to receive(:execute).and_call_original
- expect(Projects::UpdateStatisticsService)
- .to receive(:new)
- .with(project, nil, statistics: [:repository_size, :lfs_objects_size])
- .and_call_original
+ it 'updates the project statistics' do
+ expect_next_instance_of(Projects::UpdateStatisticsService, project, nil, statistics: [:repository_size, :lfs_objects_size]) do |service|
+ expect(service).to receive(:execute).and_call_original
+ end
+
+ subject.perform(*params)
+ end
+
+ it 'does nothing if the database is read-only' do
+ allow(Gitlab::Database).to receive(:read_only?) { true }
+
+ expect_any_instance_of(Projects::UpdateStatisticsService).not_to receive(:execute)
subject.perform(*params)
end
@@ -141,7 +147,8 @@ RSpec.describe GitGarbageCollectWorker do
end
it 'does nothing if the database is read-only' do
- expect(Gitlab::Database).to receive(:read_only?) { true }
+ allow(Gitlab::Database).to receive(:read_only?) { true }
+
expect_any_instance_of(Gitlab::Cleanup::OrphanLfsFileReferences).not_to receive(:run!)
subject.perform(*params)